Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55d83c12b0 | ||
|
|
41c4838dab | ||
|
|
35e57c303f | ||
|
|
e0fbd26207 | ||
|
|
b430c798dc | ||
|
|
135519ae5e | ||
|
|
d564561a4e | ||
|
|
0427c73e8a | ||
|
|
151e3bbe95 | ||
|
|
c11adf19e8 | ||
|
|
6d60f89be2 | ||
|
|
f2cb6343f7 | ||
|
|
58af735082 | ||
|
|
4e61426be7 | ||
|
|
1ef6cca149 | ||
|
|
4fc2828aaf | ||
|
|
db6e73bde5 | ||
|
|
376252e263 | ||
|
|
c675eb766e | ||
|
|
27cbc9f449 | ||
|
|
4c09a0dcb4 | ||
|
|
8bb23ee563 | ||
|
|
20571c9e4b | ||
|
|
fdde16b113 | ||
|
|
33b627f328 | ||
|
|
d96b4be9e0 | ||
|
|
088e518db1 | ||
|
|
789c547bc7 | ||
|
|
e5bf546ae7 | ||
|
|
6dd13e5d7d | ||
|
|
b1f2bb60e3 | ||
|
|
f7e97f55e9 | ||
|
|
70b712ff3b | ||
|
|
fdc3af01f3 | ||
|
|
3d54140280 | ||
|
|
bfdbbbfc8f | ||
|
|
40a5771a4b | ||
|
|
9f5a585717 | ||
|
|
7cbd74111d | ||
|
|
cb221a8039 | ||
|
|
ece1beb87f | ||
|
|
d155d1cbab |
@@ -46,6 +46,7 @@ public/userarea/last_url.txt
|
|||||||
public/userarea/class/curl_auth_debug.log
|
public/userarea/class/curl_auth_debug.log
|
||||||
public/userarea/class/curl_request_debug.log
|
public/userarea/class/curl_request_debug.log
|
||||||
|
|
||||||
|
public/userarea/uploads/cad_area/originals/*
|
||||||
# Ignora tutti i log
|
# Ignora tutti i log
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
@@ -69,3 +70,8 @@ public/userarea/logsapi/last_auth_url.txt
|
|||||||
|
|
||||||
# User uploaded files
|
# User uploaded files
|
||||||
/public/userarea/files/
|
/public/userarea/files/
|
||||||
|
|
||||||
|
# Documenti e allegati caricati dagli utenti (mescole, dipendenti, formazione, DPI)
|
||||||
|
/public/userarea/uploads/
|
||||||
|
!/public/userarea/uploads/.gitkeep
|
||||||
|
|
||||||
|
|||||||
@@ -113,12 +113,20 @@ class LoginController extends Controller
|
|||||||
return redirect()->to('userarea/production_dashboard.php');
|
return redirect()->to('userarea/production_dashboard.php');
|
||||||
} elseif ($user->hasRole('HR')) {
|
} elseif ($user->hasRole('HR')) {
|
||||||
return redirect()->to('userarea/production_dashboard.php');
|
return redirect()->to('userarea/production_dashboard.php');
|
||||||
|
} elseif ($user->hasRole('employee-hr')) {
|
||||||
|
return redirect()->to('userarea/production_dashboard.php');
|
||||||
} elseif ($user->hasRole('SuperUser')) {
|
} elseif ($user->hasRole('SuperUser')) {
|
||||||
return redirect()->to('userarea/production_dashboard.php');
|
return redirect()->to('userarea/production_dashboard.php');
|
||||||
} elseif ($user->hasRole('Management')) {
|
} elseif ($user->hasRole('Management')) {
|
||||||
return redirect()->to('userarea/production_dashboard.php');
|
return redirect()->to('userarea/production_dashboard.php');
|
||||||
|
} elseif ($user->hasRole('manager')) {
|
||||||
|
return redirect()->to('userarea/production_dashboard.php');
|
||||||
|
} elseif ($user->hasRole('employee')) {
|
||||||
|
return redirect()->to('userarea/production_dashboard.php');
|
||||||
} elseif ($user->hasRole('Quality')) {
|
} elseif ($user->hasRole('Quality')) {
|
||||||
return redirect()->to('userarea/production_dashboard.php');
|
return redirect()->to('userarea/production_dashboard.php');
|
||||||
|
} elseif ($user->hasRole('UserDatabase')) {
|
||||||
|
return redirect()->to('userarea/production_dashboard.php');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se il ruolo non è specificato, reindirizza alla home predefinita
|
// Se il ruolo non è specificato, reindirizza alla home predefinita
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateJobSubRolesTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('job_roles')) {
|
||||||
|
$rolesTable = $this->table('job_roles', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$rolesTable
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('name', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('description', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('sort_order', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 999,
|
||||||
|
])
|
||||||
|
->addColumn('is_active', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => 1,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['is_active'], [
|
||||||
|
'name' => 'idx_job_roles_is_active',
|
||||||
|
])
|
||||||
|
->addIndex(['sort_order'], [
|
||||||
|
'name' => 'idx_job_roles_sort_order',
|
||||||
|
])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->hasTable('job_sub_roles')) {
|
||||||
|
$table = $this->table('job_sub_roles', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('job_role_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('name', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('description', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('sort_order', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 999,
|
||||||
|
])
|
||||||
|
->addColumn('is_active', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => 1,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['job_role_id'], [
|
||||||
|
'name' => 'idx_job_sub_roles_job_role_id',
|
||||||
|
])
|
||||||
|
->addIndex(['is_active'], [
|
||||||
|
'name' => 'idx_job_sub_roles_is_active',
|
||||||
|
])
|
||||||
|
->addIndex(['sort_order'], [
|
||||||
|
'name' => 'idx_job_sub_roles_sort_order',
|
||||||
|
])
|
||||||
|
->addForeignKey(
|
||||||
|
'job_role_id',
|
||||||
|
'job_roles',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_job_sub_roles_job_role',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreatePpeItemsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('ppe_items', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('name', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('description', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('category', 'string', [
|
||||||
|
'limit' => 100,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'comment' => 'PPE category, for example Head, Hands, Eyes, Feet, Respiratory',
|
||||||
|
])
|
||||||
|
->addColumn('photo', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'comment' => 'PPE image path or filename',
|
||||||
|
])
|
||||||
|
->addColumn('standard_reference', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'comment' => 'Reference standard, for example EN ISO 20345',
|
||||||
|
])
|
||||||
|
->addColumn('validity_months', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'comment' => 'Default validity in months after assignment',
|
||||||
|
])
|
||||||
|
->addColumn('sort_order', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 999,
|
||||||
|
])
|
||||||
|
->addColumn('is_active', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => 1,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['category'], [
|
||||||
|
'name' => 'idx_ppe_items_category',
|
||||||
|
])
|
||||||
|
->addIndex(['is_active'], [
|
||||||
|
'name' => 'idx_ppe_items_is_active',
|
||||||
|
])
|
||||||
|
->addIndex(['sort_order'], [
|
||||||
|
'name' => 'idx_ppe_items_sort_order',
|
||||||
|
])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateEmployeePpeItemsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('employee_ppe_items', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('employee_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('ppe_item_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('assigned_date', 'date', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('expiry_date', 'date', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('quantity', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 1,
|
||||||
|
])
|
||||||
|
->addColumn('status', 'enum', [
|
||||||
|
'values' => [
|
||||||
|
'assigned',
|
||||||
|
'returned',
|
||||||
|
'expired',
|
||||||
|
'lost',
|
||||||
|
'damaged',
|
||||||
|
],
|
||||||
|
'null' => false,
|
||||||
|
'default' => 'assigned',
|
||||||
|
])
|
||||||
|
->addColumn('notes', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['employee_id'], [
|
||||||
|
'name' => 'idx_employee_ppe_items_employee_id',
|
||||||
|
])
|
||||||
|
->addIndex(['ppe_item_id'], [
|
||||||
|
'name' => 'idx_employee_ppe_items_ppe_item_id',
|
||||||
|
])
|
||||||
|
->addIndex(['status'], [
|
||||||
|
'name' => 'idx_employee_ppe_items_status',
|
||||||
|
])
|
||||||
|
->addIndex(['expiry_date'], [
|
||||||
|
'name' => 'idx_employee_ppe_items_expiry_date',
|
||||||
|
])
|
||||||
|
->addForeignKey(
|
||||||
|
'employee_id',
|
||||||
|
'employees',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_employee_ppe_items_employee',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->addForeignKey(
|
||||||
|
'ppe_item_id',
|
||||||
|
'ppe_items',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'RESTRICT',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_employee_ppe_items_ppe_item',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateJobSubRolePpeItemsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('job_sub_role_ppe_items', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('job_sub_role_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('ppe_item_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('requirement_type', 'enum', [
|
||||||
|
'values' => [
|
||||||
|
'mandatory',
|
||||||
|
'recommended',
|
||||||
|
'optional',
|
||||||
|
],
|
||||||
|
'null' => false,
|
||||||
|
'default' => 'mandatory',
|
||||||
|
'comment' => 'Defines if the PPE is mandatory, recommended or optional for the sub role',
|
||||||
|
])
|
||||||
|
->addColumn('notes', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('sort_order', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 999,
|
||||||
|
])
|
||||||
|
->addColumn('is_active', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => 1,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['job_sub_role_id'], [
|
||||||
|
'name' => 'idx_job_sub_role_ppe_items_sub_role_id',
|
||||||
|
])
|
||||||
|
->addIndex(['ppe_item_id'], [
|
||||||
|
'name' => 'idx_job_sub_role_ppe_items_ppe_item_id',
|
||||||
|
])
|
||||||
|
->addIndex(['requirement_type'], [
|
||||||
|
'name' => 'idx_job_sub_role_ppe_items_requirement_type',
|
||||||
|
])
|
||||||
|
->addIndex(['is_active'], [
|
||||||
|
'name' => 'idx_job_sub_role_ppe_items_is_active',
|
||||||
|
])
|
||||||
|
->addIndex(['job_sub_role_id', 'ppe_item_id'], [
|
||||||
|
'unique' => true,
|
||||||
|
'name' => 'uq_job_sub_role_ppe_item',
|
||||||
|
])
|
||||||
|
->addForeignKey(
|
||||||
|
'job_sub_role_id',
|
||||||
|
'job_sub_roles',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_job_sub_role_ppe_items_sub_role',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->addForeignKey(
|
||||||
|
'ppe_item_id',
|
||||||
|
'ppe_items',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_job_sub_role_ppe_items_ppe_item',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddJobSubRoleIdToEmployeesTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('employees')) {
|
||||||
|
throw new RuntimeException('Table employees does not exist.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('employees');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('job_role_id')) {
|
||||||
|
$table
|
||||||
|
->addColumn('job_role_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'department_id',
|
||||||
|
])
|
||||||
|
->addIndex(['job_role_id'], [
|
||||||
|
'name' => 'idx_employees_job_role_id',
|
||||||
|
])
|
||||||
|
->addForeignKey(
|
||||||
|
'job_role_id',
|
||||||
|
'job_roles',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'SET_NULL',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_employees_job_role',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('employees');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('job_sub_role_id')) {
|
||||||
|
$afterColumn = $table->hasColumn('job_role_id') ? 'job_role_id' : 'department_id';
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('job_sub_role_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => true,
|
||||||
|
'after' => $afterColumn,
|
||||||
|
])
|
||||||
|
->addIndex(['job_sub_role_id'], [
|
||||||
|
'name' => 'idx_employees_job_sub_role_id',
|
||||||
|
])
|
||||||
|
->addForeignKey(
|
||||||
|
'job_sub_role_id',
|
||||||
|
'job_sub_roles',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'SET_NULL',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_employees_job_sub_role',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('employees')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('employees');
|
||||||
|
|
||||||
|
if ($table->hasForeignKey('job_sub_role_id')) {
|
||||||
|
$table->dropForeignKey('job_sub_role_id')->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasForeignKey('job_role_id')) {
|
||||||
|
$table->dropForeignKey('job_role_id')->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('employees');
|
||||||
|
|
||||||
|
if ($table->hasIndexByName('idx_employees_job_sub_role_id')) {
|
||||||
|
$table->removeIndexByName('idx_employees_job_sub_role_id')->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasIndexByName('idx_employees_job_role_id')) {
|
||||||
|
$table->removeIndexByName('idx_employees_job_role_id')->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('employees');
|
||||||
|
|
||||||
|
if ($table->hasColumn('job_sub_role_id')) {
|
||||||
|
$table->removeColumn('job_sub_role_id')->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasColumn('job_role_id')) {
|
||||||
|
$table->removeColumn('job_role_id')->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddDeliveryFieldsToEmployeePpeItemsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('employee_ppe_items');
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('delivered_by', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'after' => 'expiry_date',
|
||||||
|
])
|
||||||
|
->addColumn('created_by', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'after' => 'notes',
|
||||||
|
])
|
||||||
|
->addIndex(['created_by'], [
|
||||||
|
'name' => 'idx_employee_ppe_items_created_by',
|
||||||
|
])
|
||||||
|
->addForeignKey(
|
||||||
|
'created_by',
|
||||||
|
'auth_users',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'SET_NULL',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_employee_ppe_items_created_by',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateEmployeeJobSubRolesTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('employee_job_sub_roles')) {
|
||||||
|
$table = $this->table('employee_job_sub_roles', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'signed' => false,
|
||||||
|
'collation' => 'utf8mb4_general_ci',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('employee_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('job_sub_role_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('is_primary', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => false,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['employee_id', 'job_sub_role_id'], [
|
||||||
|
'unique' => true,
|
||||||
|
'name' => 'uq_employee_subrole',
|
||||||
|
])
|
||||||
|
->addIndex(['employee_id'], [
|
||||||
|
'name' => 'idx_employee_job_sub_roles_employee',
|
||||||
|
])
|
||||||
|
->addIndex(['job_sub_role_id'], [
|
||||||
|
'name' => 'idx_employee_job_sub_roles_subrole',
|
||||||
|
])
|
||||||
|
->addForeignKey(
|
||||||
|
'employee_id',
|
||||||
|
'employees',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_employee_job_sub_roles_employee',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->addForeignKey(
|
||||||
|
'job_sub_role_id',
|
||||||
|
'job_sub_roles',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_employee_job_sub_roles_subrole',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import existing single sub-role assignments from employees.job_sub_role_id
|
||||||
|
// into the new bridge table.
|
||||||
|
$this->execute("
|
||||||
|
INSERT IGNORE INTO employee_job_sub_roles
|
||||||
|
(employee_id, job_sub_role_id, is_primary, created_at)
|
||||||
|
SELECT
|
||||||
|
e.id,
|
||||||
|
e.job_sub_role_id,
|
||||||
|
1,
|
||||||
|
NOW()
|
||||||
|
FROM employees e
|
||||||
|
WHERE e.job_sub_role_id IS NOT NULL
|
||||||
|
AND e.job_sub_role_id > 0
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if ($this->hasTable('employee_job_sub_roles')) {
|
||||||
|
$this->table('employee_job_sub_roles')->drop()->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateCompanyFunctionsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('company_functions')) {
|
||||||
|
$table = $this->table('company_functions', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'signed' => false,
|
||||||
|
'collation' => 'utf8mb4_general_ci',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('function_name', 'string', [
|
||||||
|
'limit' => 150,
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'Function name, for example RSPP, Medico del lavoro, RLS',
|
||||||
|
])
|
||||||
|
->addColumn('person_full_name', 'string', [
|
||||||
|
'limit' => 200,
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'Full name and surname of the person assigned to the function',
|
||||||
|
])
|
||||||
|
->addColumn('phone', 'string', [
|
||||||
|
'limit' => 80,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('email', 'string', [
|
||||||
|
'limit' => 190,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('notes', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('sort_order', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 0,
|
||||||
|
])
|
||||||
|
->addColumn('is_active', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => true,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['function_name'], [
|
||||||
|
'name' => 'idx_company_functions_function_name',
|
||||||
|
])
|
||||||
|
->addIndex(['person_full_name'], [
|
||||||
|
'name' => 'idx_company_functions_person_full_name',
|
||||||
|
])
|
||||||
|
->addIndex(['email'], [
|
||||||
|
'name' => 'idx_company_functions_email',
|
||||||
|
])
|
||||||
|
->addIndex(['is_active', 'sort_order'], [
|
||||||
|
'name' => 'idx_company_functions_active_sort',
|
||||||
|
])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->execute("
|
||||||
|
INSERT INTO company_functions
|
||||||
|
(function_name, person_full_name, phone, email, notes, sort_order, is_active, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
('RSPP', '', NULL, NULL, NULL, 10, 1, NOW(), NOW()),
|
||||||
|
('Medico del lavoro', '', NULL, NULL, NULL, 20, 1, NOW(), NOW()),
|
||||||
|
('RLS', '', NULL, NULL, NULL, 30, 1, NOW(), NOW())
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if ($this->hasTable('company_functions')) {
|
||||||
|
$this->table('company_functions')->drop()->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AlterScadFunctionsAddContactFields extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('scad_functions')) {
|
||||||
|
throw new RuntimeException('Table scad_functions does not exist.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('scad_functions');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('person_full_name')) {
|
||||||
|
$table->addColumn('person_full_name', 'string', [
|
||||||
|
'limit' => 200,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'description',
|
||||||
|
'comment' => 'Full name and surname of the person assigned to the function',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('phone')) {
|
||||||
|
$table->addColumn('phone', 'string', [
|
||||||
|
'limit' => 80,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'person_full_name',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('email')) {
|
||||||
|
$table->addColumn('email', 'string', [
|
||||||
|
'limit' => 190,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'phone',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('notes')) {
|
||||||
|
$table->addColumn('notes', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'email',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('sort_order')) {
|
||||||
|
$table->addColumn('sort_order', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 0,
|
||||||
|
'after' => 'status',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasIndexByName('idx_scad_functions_name')) {
|
||||||
|
$table->addIndex(['name'], [
|
||||||
|
'name' => 'idx_scad_functions_name',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasIndexByName('idx_scad_functions_person_full_name')) {
|
||||||
|
$table->addIndex(['person_full_name'], [
|
||||||
|
'name' => 'idx_scad_functions_person_full_name',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasIndexByName('idx_scad_functions_email')) {
|
||||||
|
$table->addIndex(['email'], [
|
||||||
|
'name' => 'idx_scad_functions_email',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasIndexByName('idx_scad_functions_status_sort')) {
|
||||||
|
$table->addIndex(['status', 'sort_order'], [
|
||||||
|
'name' => 'idx_scad_functions_status_sort',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->update();
|
||||||
|
|
||||||
|
// Set a default order for existing rows without changing their names.
|
||||||
|
$this->execute("
|
||||||
|
UPDATE scad_functions
|
||||||
|
SET sort_order = id * 10
|
||||||
|
WHERE sort_order = 0
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('scad_functions')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('scad_functions');
|
||||||
|
|
||||||
|
if ($table->hasIndexByName('idx_scad_functions_status_sort')) {
|
||||||
|
$table->removeIndexByName('idx_scad_functions_status_sort');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasIndexByName('idx_scad_functions_email')) {
|
||||||
|
$table->removeIndexByName('idx_scad_functions_email');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasIndexByName('idx_scad_functions_person_full_name')) {
|
||||||
|
$table->removeIndexByName('idx_scad_functions_person_full_name');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasIndexByName('idx_scad_functions_name')) {
|
||||||
|
$table->removeIndexByName('idx_scad_functions_name');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasColumn('sort_order')) {
|
||||||
|
$table->removeColumn('sort_order');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasColumn('notes')) {
|
||||||
|
$table->removeColumn('notes');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasColumn('email')) {
|
||||||
|
$table->removeColumn('email');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasColumn('phone')) {
|
||||||
|
$table->removeColumn('phone');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasColumn('person_full_name')) {
|
||||||
|
$table->removeColumn('person_full_name');
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateCadAreaJobsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('cad_area_jobs');
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('iduser', 'integer', [
|
||||||
|
'null' => true,
|
||||||
|
'signed' => false,
|
||||||
|
'limit' => 10,
|
||||||
|
])
|
||||||
|
->addColumn('original_filename', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('stored_filename', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('file_path', 'string', [
|
||||||
|
'limit' => 500,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('file_url', 'string', [
|
||||||
|
'limit' => 500,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('file_size', 'integer', [
|
||||||
|
'null' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('status', 'enum', [
|
||||||
|
'values' => [
|
||||||
|
'uploaded',
|
||||||
|
'processing',
|
||||||
|
'completed',
|
||||||
|
'error',
|
||||||
|
],
|
||||||
|
'default' => 'uploaded',
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('area_mm2', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('area_cm2', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('area_m2', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 9,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('scale_detected', 'string', [
|
||||||
|
'limit' => 50,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('confidence', 'string', [
|
||||||
|
'limit' => 50,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('message', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('python_response', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addIndex(['iduser'], [
|
||||||
|
'name' => 'idx_cad_area_jobs_iduser',
|
||||||
|
])
|
||||||
|
->addIndex(['status'], [
|
||||||
|
'name' => 'idx_cad_area_jobs_status',
|
||||||
|
])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddNotifyFunctionToScadDeadlines extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('scad_deadlines')) {
|
||||||
|
throw new RuntimeException('Table scad_deadlines does not exist.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('scad_deadlines');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('notify_function')) {
|
||||||
|
$table
|
||||||
|
->addColumn('notify_function', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => false,
|
||||||
|
'after' => 'function_id',
|
||||||
|
'comment' => 'Send deadline reminder also to the linked function email',
|
||||||
|
])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('scad_deadlines')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('scad_deadlines');
|
||||||
|
|
||||||
|
if ($table->hasColumn('notify_function')) {
|
||||||
|
$table
|
||||||
|
->removeColumn('notify_function')
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddRoiFieldsToCadAreaJobsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('cad_area_jobs');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('roi_x')) {
|
||||||
|
$table->addColumn('roi_x', 'decimal', [
|
||||||
|
'precision' => 12,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'file_size',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('roi_y')) {
|
||||||
|
$table->addColumn('roi_y', 'decimal', [
|
||||||
|
'precision' => 12,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'roi_x',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('roi_width')) {
|
||||||
|
$table->addColumn('roi_width', 'decimal', [
|
||||||
|
'precision' => 12,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'roi_y',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('roi_height')) {
|
||||||
|
$table->addColumn('roi_height', 'decimal', [
|
||||||
|
'precision' => 12,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'roi_width',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('roi_page')) {
|
||||||
|
$table->addColumn('roi_page', 'integer', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 1,
|
||||||
|
'after' => 'roi_height',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('calculation_mode')) {
|
||||||
|
$table->addColumn('calculation_mode', 'string', [
|
||||||
|
'limit' => 50,
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'auto_roi',
|
||||||
|
'after' => 'roi_page',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddResultDetailFieldsToCadAreaJobsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('cad_area_jobs');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('width_mm')) {
|
||||||
|
$table->addColumn('width_mm', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('height_mm')) {
|
||||||
|
$table->addColumn('height_mm', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('scale_used')) {
|
||||||
|
$table->addColumn('scale_used', 'decimal', [
|
||||||
|
'precision' => 12,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('strategy_used')) {
|
||||||
|
$table->addColumn('strategy_used', 'string', [
|
||||||
|
'limit' => 100,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddManualTracingFieldsToCadAreaJobsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('cad_area_jobs');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('width_mm')) {
|
||||||
|
$table->addColumn('width_mm', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('height_mm')) {
|
||||||
|
$table->addColumn('height_mm', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('scale_used')) {
|
||||||
|
$table->addColumn('scale_used', 'decimal', [
|
||||||
|
'precision' => 12,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('strategy_used')) {
|
||||||
|
$table->addColumn('strategy_used', 'string', [
|
||||||
|
'limit' => 100,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_calibration_px')) {
|
||||||
|
$table->addColumn('manual_calibration_px', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_calibration_mm')) {
|
||||||
|
$table->addColumn('manual_calibration_mm', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_mm_per_px')) {
|
||||||
|
$table->addColumn('manual_mm_per_px', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 10,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_polygon_json')) {
|
||||||
|
$table->addColumn('manual_polygon_json', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_area_mm2')) {
|
||||||
|
$table->addColumn('manual_area_mm2', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_area_cm2')) {
|
||||||
|
$table->addColumn('manual_area_cm2', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_width_mm')) {
|
||||||
|
$table->addColumn('manual_width_mm', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_height_mm')) {
|
||||||
|
$table->addColumn('manual_height_mm', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_status')) {
|
||||||
|
$table->addColumn('manual_status', 'string', [
|
||||||
|
'limit' => 50,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddManualHoleFieldsToCadAreaJobsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('cad_area_jobs');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_outer_area_mm2')) {
|
||||||
|
$table->addColumn('manual_outer_area_mm2', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_holes_area_mm2')) {
|
||||||
|
$table->addColumn('manual_holes_area_mm2', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_holes_json')) {
|
||||||
|
$table->addColumn('manual_holes_json', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateMescoleFilesTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('mescole_files', [
|
||||||
|
'id' => 'id',
|
||||||
|
'signed' => true,
|
||||||
|
'engine' => 'InnoDB',
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
'collation' => 'utf8mb4_general_ci',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('idmescola', 'integer', [
|
||||||
|
'signed' => true,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('categoria', 'string', [
|
||||||
|
'limit' => 100,
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'slug categoria: schede_tecniche, certificati_analisi, ...',
|
||||||
|
])
|
||||||
|
->addColumn('titolo', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('filename', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'nome fisico del file salvato su disco (random, non l\'originale)',
|
||||||
|
])
|
||||||
|
->addColumn('original_filename', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('mime_type', 'string', [
|
||||||
|
'limit' => 150,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('filesize', 'integer', [
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('uploaded_at', 'datetime', [
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['idmescola'])
|
||||||
|
->addIndex(['categoria'])
|
||||||
|
->addForeignKey('idmescola', 'mescole', 'id', [
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddDxfToCadAreaJobs extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$this->table('cad_area_jobs')
|
||||||
|
->addColumn('dxf_filename', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'after' => 'manual_holes_json'
|
||||||
|
])
|
||||||
|
->addColumn('dxf_url', 'string', [
|
||||||
|
'limit' => 500,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'after' => 'dxf_filename'
|
||||||
|
])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddVariazioniAndAttivaAlertToScadDeadlines extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$this->table('scad_deadlines')
|
||||||
|
->addColumn('variazioni', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'after' => 'notes'
|
||||||
|
])
|
||||||
|
->addColumn('attiva_alert', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => false,
|
||||||
|
'after' => 'variazioni'
|
||||||
|
])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class ChangeVariazioniToFlag extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$this->execute("ALTER TABLE `scad_deadlines`
|
||||||
|
CHANGE `variazioni` `variazioni` TINYINT(1) NOT NULL DEFAULT 0
|
||||||
|
COMMENT 'Flag: in caso di variazioni'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$this->execute("ALTER TABLE `scad_deadlines`
|
||||||
|
CHANGE `variazioni` `variazioni` TEXT NULL DEFAULT NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class SetAttivaAlertDefaultOn extends AbstractMigration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Change Method.
|
||||||
|
*
|
||||||
|
* Write your reversible migrations using this method.
|
||||||
|
*
|
||||||
|
* More information on writing migrations is available here:
|
||||||
|
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||||
|
*
|
||||||
|
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||||
|
* with the Table class.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$this->execute("ALTER TABLE `scad_deadlines`
|
||||||
|
CHANGE `attiva_alert` `attiva_alert` TINYINT(1) NOT NULL DEFAULT 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$this->execute("ALTER TABLE `scad_deadlines`
|
||||||
|
CHANGE `attiva_alert` `attiva_alert` TINYINT(1) NOT NULL DEFAULT 0");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class EnableAttivaAlertOnExisting extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$this->execute("UPDATE `scad_deadlines`
|
||||||
|
SET `attiva_alert` = 1
|
||||||
|
WHERE `status` = 'active'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$this->execute("UPDATE `scad_deadlines`
|
||||||
|
SET `attiva_alert` = 0
|
||||||
|
WHERE `status` = 'active'");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateTrainingTopicCoverage extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('training_topic_coverage', [
|
||||||
|
'id' => 'id',
|
||||||
|
'signed' => false,
|
||||||
|
'engine' => 'InnoDB',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('topic_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'Corso madre (es. RSPP)',
|
||||||
|
])
|
||||||
|
->addColumn('covered_topic_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'Corso assorbito (es. Antincendio)',
|
||||||
|
])
|
||||||
|
->addColumn('inherit_expiry', 'enum', [
|
||||||
|
'values' => ['own', 'source', 'none'],
|
||||||
|
'default' => 'own',
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'own = ricalcola con frequenza propria del corso coperto',
|
||||||
|
])
|
||||||
|
->addColumn('notes', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addIndex(['topic_id', 'covered_topic_id'], [
|
||||||
|
'unique' => true,
|
||||||
|
'name' => 'uq_topic_coverage',
|
||||||
|
])
|
||||||
|
->addIndex(['covered_topic_id'], ['name' => 'idx_covered_topic'])
|
||||||
|
->addForeignKey('topic_id', 'training_topics', 'id', [
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_cov_topic',
|
||||||
|
])
|
||||||
|
->addForeignKey('covered_topic_id', 'training_topics', 'id', [
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_cov_covered',
|
||||||
|
])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
# 1. Database migration
|
||||||
|
|
||||||
|
```mysql
|
||||||
|
ALTER TABLE employees
|
||||||
|
ADD COLUMN address varchar(500) DEFAULT NULL AFTER last_name,
|
||||||
|
ADD COLUMN phone varchar(255) DEFAULT NULL AFTER address,
|
||||||
|
ADD COLUMN email varchar(255) DEFAULT NULL AFTER phone,
|
||||||
|
ADD COLUMN job_role_id int(10) UNSIGNED DEFAULT NULL AFTER department_id;
|
||||||
|
|
||||||
|
-- Replace ENUM status with plain VARCHAR for easier maintenance.
|
||||||
|
ALTER TABLE employees
|
||||||
|
MODIFY status varchar(255) NOT NULL DEFAULT 'active';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS job_roles (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
name varchar(255) NOT NULL,
|
||||||
|
description text DEFAULT NULL,
|
||||||
|
sort_order int(10) UNSIGNED NOT NULL DEFAULT 999,
|
||||||
|
is_active tinyint(1) NOT NULL DEFAULT 1,
|
||||||
|
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uniq_job_roles_name (name),
|
||||||
|
KEY idx_job_roles_active (is_active),
|
||||||
|
KEY idx_job_roles_sort_order (sort_order)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
ALTER TABLE employees
|
||||||
|
ADD KEY idx_employees_job_role_id (job_role_id);
|
||||||
|
|
||||||
|
ALTER TABLE employees
|
||||||
|
ADD CONSTRAINT fk_employees_job_role
|
||||||
|
FOREIGN KEY (job_role_id) REFERENCES job_roles (id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- 1) Seed job_roles with every distinct non-empty value of employees.position.
|
||||||
|
INSERT IGNORE INTO job_roles (name, is_active, sort_order, created_at, updated_at)
|
||||||
|
SELECT DISTINCT TRIM(position), 1, 999, NOW(), NOW()
|
||||||
|
FROM employees
|
||||||
|
WHERE position IS NOT NULL AND TRIM(position) <> '';
|
||||||
|
|
||||||
|
-- 2) Backfill employees.job_role_id by matching position text to job_roles.name.
|
||||||
|
UPDATE employees e
|
||||||
|
JOIN job_roles jr ON jr.name = TRIM(e.position)
|
||||||
|
SET e.job_role_id = jr.id
|
||||||
|
WHERE e.position IS NOT NULL AND TRIM(e.position) <> '';
|
||||||
|
|
||||||
|
-- 3) Drop the legacy column.
|
||||||
|
ALTER TABLE employees DROP COLUMN position;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS training_topics (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
name varchar(255) NOT NULL,
|
||||||
|
description text DEFAULT NULL,
|
||||||
|
default_frequency_months int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
default_reminder_days int(10) UNSIGNED NOT NULL DEFAULT 30,
|
||||||
|
sort_order int(10) UNSIGNED NOT NULL DEFAULT 999,
|
||||||
|
is_active tinyint(1) NOT NULL DEFAULT 1,
|
||||||
|
is_mandatory tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uniq_training_topics_name (name),
|
||||||
|
KEY idx_training_topics_active (is_active),
|
||||||
|
KEY idx_training_topics_mandatory (is_mandatory),
|
||||||
|
KEY idx_training_topics_sort_order (sort_order)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS employee_documents (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
employee_id int(10) UNSIGNED NOT NULL,
|
||||||
|
category varchar(255) NOT NULL DEFAULT 'other',
|
||||||
|
original_name varchar(500) NOT NULL,
|
||||||
|
stored_name varchar(500) NOT NULL,
|
||||||
|
mime_type varchar(255) DEFAULT NULL,
|
||||||
|
size int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
notes text DEFAULT NULL,
|
||||||
|
uploaded_by int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_employee_documents_employee (employee_id),
|
||||||
|
KEY idx_employee_documents_category (category),
|
||||||
|
KEY idx_employee_documents_uploaded_by (uploaded_by),
|
||||||
|
CONSTRAINT fk_employee_documents_employee
|
||||||
|
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_employee_documents_uploaded_by
|
||||||
|
FOREIGN KEY (uploaded_by) REFERENCES auth_users (id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS employee_ppe (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
employee_id int(10) UNSIGNED NOT NULL,
|
||||||
|
item_name varchar(255) NOT NULL,
|
||||||
|
delivery_date date DEFAULT NULL,
|
||||||
|
delivered_by varchar(255) DEFAULT NULL,
|
||||||
|
notes text DEFAULT NULL,
|
||||||
|
created_by int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_employee_ppe_employee (employee_id),
|
||||||
|
KEY idx_employee_ppe_delivery_date (delivery_date),
|
||||||
|
CONSTRAINT fk_employee_ppe_employee
|
||||||
|
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_employee_ppe_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES auth_users (id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS employee_trainings (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
employee_id int(10) UNSIGNED NOT NULL,
|
||||||
|
training_topic_id int(10) UNSIGNED NOT NULL,
|
||||||
|
completed_date date NOT NULL,
|
||||||
|
delivered_by varchar(255) DEFAULT NULL,
|
||||||
|
description text DEFAULT NULL,
|
||||||
|
training_type varchar(255) NOT NULL DEFAULT 'initial',
|
||||||
|
update_frequency_months int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
reminder_days int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
next_due_date date DEFAULT NULL,
|
||||||
|
created_by int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_employee_trainings_employee (employee_id),
|
||||||
|
KEY idx_employee_trainings_topic (training_topic_id),
|
||||||
|
KEY idx_employee_trainings_next_due (next_due_date),
|
||||||
|
KEY idx_employee_trainings_employee_topic (employee_id, training_topic_id),
|
||||||
|
KEY idx_employee_trainings_created_by (created_by),
|
||||||
|
CONSTRAINT fk_employee_trainings_employee
|
||||||
|
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_employee_trainings_topic
|
||||||
|
FOREIGN KEY (training_topic_id) REFERENCES training_topics (id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_employee_trainings_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES auth_users (id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS employee_training_attachments (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
training_id int(10) UNSIGNED NOT NULL,
|
||||||
|
original_name varchar(500) NOT NULL,
|
||||||
|
stored_name varchar(500) NOT NULL,
|
||||||
|
mime_type varchar(255) DEFAULT NULL,
|
||||||
|
size int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
uploaded_by int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_employee_training_attachments_training (training_id),
|
||||||
|
KEY idx_employee_training_attachments_uploaded_by (uploaded_by),
|
||||||
|
CONSTRAINT fk_employee_training_attachments_training
|
||||||
|
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_employee_training_attachments_uploaded_by
|
||||||
|
FOREIGN KEY (uploaded_by) REFERENCES auth_users (id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS employee_training_log (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
employee_id int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
training_id int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
action varchar(255) NOT NULL,
|
||||||
|
field varchar(255) DEFAULT NULL,
|
||||||
|
old_value text DEFAULT NULL,
|
||||||
|
new_value text DEFAULT NULL,
|
||||||
|
changed_by int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
changed_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_employee_training_log_employee (employee_id),
|
||||||
|
KEY idx_employee_training_log_training (training_id),
|
||||||
|
KEY idx_employee_training_log_changed_at (changed_at),
|
||||||
|
CONSTRAINT fk_employee_training_log_employee
|
||||||
|
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_employee_training_log_training
|
||||||
|
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_employee_training_log_changed_by
|
||||||
|
FOREIGN KEY (changed_by) REFERENCES auth_users (id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT INTO auth_roles (name, display_name, description, removable, created_at, updated_at) VALUES
|
||||||
|
('employee', 'Employee', 'Read-only access to own employee profile.', 1, NOW(), NOW()),
|
||||||
|
('employee-hr', 'HR Manager', 'Can manage employee profiles, documents, PPE and training records.', 1, NOW(), NOW()),
|
||||||
|
('manager', 'Manager', 'Same permissions as HR Manager.', 1, NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
display_name = VALUES(display_name),
|
||||||
|
description = VALUES(description),
|
||||||
|
updated_at = NOW();
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS training_reminder_log (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
training_id int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
employee_id int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
training_topic_id int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
addressee_email varchar(255) NOT NULL,
|
||||||
|
next_due_date date DEFAULT NULL,
|
||||||
|
status_at_send varchar(255) NOT NULL,
|
||||||
|
sent_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_training_reminder_log_dedup (training_id, addressee_email, next_due_date),
|
||||||
|
KEY idx_training_reminder_log_dedup_missing (employee_id, training_topic_id, addressee_email),
|
||||||
|
KEY idx_training_reminder_log_sent_at (sent_at),
|
||||||
|
CONSTRAINT fk_training_reminder_log_training
|
||||||
|
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_training_reminder_log_employee
|
||||||
|
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_training_reminder_log_topic
|
||||||
|
FOREIGN KEY (training_topic_id) REFERENCES training_topics (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
# 2. Upload storage folder
|
||||||
|
|
||||||
|
Create the storage directory with the correct permissions for the web server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /var/www/zibo-dashboard/public/userarea/files/employees
|
||||||
|
chown -R www-data:www-data /var/www/zibo-dashboard/public/userarea/files
|
||||||
|
chmod -R 775 /var/www/zibo-dashboard/public/userarea/files
|
||||||
|
```
|
||||||
|
|
||||||
|
Uploaded files will be organized as:
|
||||||
|
|
||||||
|
```
|
||||||
|
files/employees/{employee_id}/documents/ # File Repository (HR)
|
||||||
|
files/employees/{employee_id}/trainings/ # Training certificates
|
||||||
|
```
|
||||||
|
|
||||||
|
# 3. Cron for automated emails
|
||||||
|
|
||||||
|
```cron
|
||||||
|
0 7 * * * /usr/bin/php /var/www/zibo-dashboard/public/userarea/cron/send_training_reminders.php \
|
||||||
|
>> /var/www/zibo-dashboard/storage/logs/training_reminders.log 2>&1
|
||||||
|
```
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Auth check for AJAX endpoints under /userarea/ajax/.
|
||||||
|
* Include this at the top of every ajax handler.
|
||||||
|
* Sets $currentUserId from session or returns 401 JSON.
|
||||||
|
*/
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_SESSION['iduserlogin'])) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Non autorizzato. Effettua il login.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentUserId = (int)$_SESSION['iduserlogin'];
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID documento non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT employee_id, stored_name FROM employee_documents WHERE id = :id LIMIT 1");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$doc) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Documento non trovato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$del = $pdo->prepare("DELETE FROM employee_documents WHERE id = :id");
|
||||||
|
$del->execute(['id' => $id]);
|
||||||
|
|
||||||
|
$path = __DIR__ . '/../../files/employees/' . (int)$doc['employee_id'] . '/documents/' . $doc['stored_name'];
|
||||||
|
if (is_file($path)) {
|
||||||
|
@unlink($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
include('../../include/headscript.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'ID DPI non valido.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE employee_ppe_items
|
||||||
|
SET status = 'returned',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'DPI rimosso correttamente.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$row = $pdo->prepare("SELECT employee_id FROM employee_trainings WHERE id = :id");
|
||||||
|
$row->execute(['id' => $id]);
|
||||||
|
$tr = $row->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$tr) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect attached files BEFORE deletion so we can unlink them after
|
||||||
|
$files = $pdo->prepare("SELECT stored_name FROM employee_training_attachments WHERE training_id = :id");
|
||||||
|
$files->execute(['id' => $id]);
|
||||||
|
$stored = $files->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
// Log BEFORE delete (FK on log allows SET NULL on training delete but we want a clean record)
|
||||||
|
$pdo->prepare("
|
||||||
|
INSERT INTO employee_training_log
|
||||||
|
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, NULL, 'deleted', NULL, NULL, NULL, :cb, NOW())
|
||||||
|
")->execute(['eid' => $tr['employee_id'], 'cb' => $currentUserId]);
|
||||||
|
|
||||||
|
$pdo->prepare("DELETE FROM employee_trainings WHERE id = :id")->execute(['id' => $id]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
foreach ($stored as $name) {
|
||||||
|
$path = __DIR__ . '/../../files/employees/' . (int)$tr['employee_id'] . '/trainings/' . $name;
|
||||||
|
if (is_file($path)) {
|
||||||
|
@unlink($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID allegato non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $pdo->prepare("
|
||||||
|
SELECT a.stored_name, a.original_name, a.training_id, t.employee_id
|
||||||
|
FROM employee_training_attachments a
|
||||||
|
JOIN employee_trainings t ON t.id = a.training_id
|
||||||
|
WHERE a.id = :id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$row->execute(['id' => $id]);
|
||||||
|
$att = $row->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$att) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Allegato non trovato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
$pdo->prepare("DELETE FROM employee_training_attachments WHERE id = :id")->execute(['id' => $id]);
|
||||||
|
$pdo->prepare("
|
||||||
|
INSERT INTO employee_training_log
|
||||||
|
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, 'attachment_deleted', 'attachment', :name, NULL, :cb, NOW())
|
||||||
|
")->execute([
|
||||||
|
'eid' => $att['employee_id'],
|
||||||
|
'tid' => $att['training_id'],
|
||||||
|
'name' => $att['original_name'],
|
||||||
|
'cb' => $currentUserId,
|
||||||
|
]);
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
$path = __DIR__ . '/../../files/employees/' . (int)$att['employee_id'] . '/trainings/' . $att['stored_name'];
|
||||||
|
if (is_file($path)) {
|
||||||
|
@unlink($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
include('../../include/headscript.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'ID DPI non valido.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
DELETE FROM employee_ppe_items
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
|
if ($stmt->rowCount() === 0) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Record non trovato o già eliminato.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Record DPI eliminato definitivamente.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT d.*, e.auth_user_id
|
||||||
|
FROM employee_documents d
|
||||||
|
JOIN employees e ON e.id = d.employee_id
|
||||||
|
WHERE d.id = :id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$doc) {
|
||||||
|
http_response_code(404);
|
||||||
|
exit('Documento non trovato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Access check: HR roles can download any; otherwise only own employee */
|
||||||
|
$roleStmt = $pdo->prepare("
|
||||||
|
SELECT r.name
|
||||||
|
FROM auth_users u
|
||||||
|
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||||
|
WHERE u.id = :id LIMIT 1
|
||||||
|
");
|
||||||
|
$roleStmt->execute(['id' => $currentUserId]);
|
||||||
|
$role = (string)$roleStmt->fetchColumn();
|
||||||
|
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||||
|
$isHr = in_array($role, $hrRoles, true);
|
||||||
|
|
||||||
|
if (!$isHr && (int)$doc['auth_user_id'] !== $currentUserId) {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('Accesso negato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = __DIR__ . '/../../files/employees/' . (int)$doc['employee_id'] . '/documents/' . $doc['stored_name'];
|
||||||
|
if (!is_file($path)) {
|
||||||
|
http_response_code(404);
|
||||||
|
exit('File non trovato sul server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ob_get_level() > 0) { ob_end_clean(); }
|
||||||
|
header('Content-Type: ' . (!empty($doc['mime_type']) ? $doc['mime_type'] : 'application/octet-stream'));
|
||||||
|
header('Content-Disposition: attachment; filename="' . rawurlencode($doc['original_name']) . '"');
|
||||||
|
header('Content-Length: ' . filesize($path));
|
||||||
|
header('Cache-Control: private, max-age=0, must-revalidate');
|
||||||
|
readfile($path);
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT a.*, t.employee_id, e.auth_user_id
|
||||||
|
FROM employee_training_attachments a
|
||||||
|
JOIN employee_trainings t ON t.id = a.training_id
|
||||||
|
JOIN employees e ON e.id = t.employee_id
|
||||||
|
WHERE a.id = :id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
$att = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$att) {
|
||||||
|
http_response_code(404);
|
||||||
|
exit('Allegato non trovato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Access: HR or owning employee */
|
||||||
|
$roleStmt = $pdo->prepare("
|
||||||
|
SELECT r.name FROM auth_users u
|
||||||
|
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||||
|
WHERE u.id = :id LIMIT 1
|
||||||
|
");
|
||||||
|
$roleStmt->execute(['id' => $currentUserId]);
|
||||||
|
$role = (string)$roleStmt->fetchColumn();
|
||||||
|
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||||
|
$isHr = in_array($role, $hrRoles, true);
|
||||||
|
|
||||||
|
if (!$isHr && (int)$att['auth_user_id'] !== $currentUserId) {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('Accesso negato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = __DIR__ . '/../../files/employees/' . (int)$att['employee_id'] . '/trainings/' . $att['stored_name'];
|
||||||
|
if (!is_file($path)) {
|
||||||
|
http_response_code(404);
|
||||||
|
exit('File non trovato sul server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ob_get_level() > 0) { ob_end_clean(); }
|
||||||
|
header('Content-Type: ' . (!empty($att['mime_type']) ? $att['mime_type'] : 'application/octet-stream'));
|
||||||
|
header('Content-Disposition: attachment; filename="' . rawurlencode($att['original_name']) . '"');
|
||||||
|
header('Content-Length: ' . filesize($path));
|
||||||
|
header('Cache-Control: private, max-age=0, must-revalidate');
|
||||||
|
readfile($path);
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$trainingId = (int)($_GET['training_id'] ?? 0);
|
||||||
|
if ($trainingId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
/* Access: HR or owner */
|
||||||
|
$ownerStmt = $pdo->prepare("
|
||||||
|
SELECT e.auth_user_id
|
||||||
|
FROM employee_trainings t
|
||||||
|
JOIN employees e ON e.id = t.employee_id
|
||||||
|
WHERE t.id = :id LIMIT 1
|
||||||
|
");
|
||||||
|
$ownerStmt->execute(['id' => $trainingId]);
|
||||||
|
$ownerAuthUserId = $ownerStmt->fetchColumn();
|
||||||
|
if ($ownerAuthUserId === false) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleStmt = $pdo->prepare("
|
||||||
|
SELECT r.name FROM auth_users u
|
||||||
|
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||||
|
WHERE u.id = :id LIMIT 1
|
||||||
|
");
|
||||||
|
$roleStmt->execute(['id' => $currentUserId]);
|
||||||
|
$role = (string)$roleStmt->fetchColumn();
|
||||||
|
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||||
|
$isHr = in_array($role, $hrRoles, true);
|
||||||
|
|
||||||
|
if (!$isHr && (int)$ownerAuthUserId !== $currentUserId) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Accesso negato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT id, original_name, mime_type, size, created_at
|
||||||
|
FROM employee_training_attachments
|
||||||
|
WHERE training_id = :tid
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
");
|
||||||
|
$stmt->execute(['tid' => $trainingId]);
|
||||||
|
$attachments = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'attachments' => $attachments,
|
||||||
|
'can_edit' => $isHr,
|
||||||
|
]);
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$trainingId = (int)($_GET['training_id'] ?? 0);
|
||||||
|
if ($trainingId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
/* Access: HR or owner */
|
||||||
|
$ownerStmt = $pdo->prepare("
|
||||||
|
SELECT e.auth_user_id
|
||||||
|
FROM employee_trainings t
|
||||||
|
JOIN employees e ON e.id = t.employee_id
|
||||||
|
WHERE t.id = :id LIMIT 1
|
||||||
|
");
|
||||||
|
$ownerStmt->execute(['id' => $trainingId]);
|
||||||
|
$ownerAuthUserId = $ownerStmt->fetchColumn();
|
||||||
|
if ($ownerAuthUserId === false) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleStmt = $pdo->prepare("
|
||||||
|
SELECT r.name FROM auth_users u
|
||||||
|
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||||
|
WHERE u.id = :id LIMIT 1
|
||||||
|
");
|
||||||
|
$roleStmt->execute(['id' => $currentUserId]);
|
||||||
|
$role = (string)$roleStmt->fetchColumn();
|
||||||
|
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||||
|
$isHr = in_array($role, $hrRoles, true);
|
||||||
|
|
||||||
|
if (!$isHr && (int)$ownerAuthUserId !== $currentUserId) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Accesso negato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT l.id, l.action, l.field, l.old_value, l.new_value, l.changed_at,
|
||||||
|
TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS changed_by_name,
|
||||||
|
u.email AS changed_by_email
|
||||||
|
FROM employee_training_log l
|
||||||
|
LEFT JOIN auth_users u ON u.id = l.changed_by
|
||||||
|
WHERE l.training_id = :tid
|
||||||
|
ORDER BY l.changed_at DESC, l.id DESC
|
||||||
|
");
|
||||||
|
$stmt->execute(['tid' => $trainingId]);
|
||||||
|
$entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'entries' => $entries]);
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Bulk-assign a single DPI (PPE) item to several employees at once:
|
||||||
|
* one employee_ppe row per selected employee, all sharing the same
|
||||||
|
* item name / delivery date / delivered-by / notes.
|
||||||
|
* Mirrors ajax/trainings/save_bulk_training.php. HR-only.
|
||||||
|
*/
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $pdo and $currentUserId from hr_auth_check.php
|
||||||
|
|
||||||
|
$itemName = trim($_POST['item_name'] ?? '');
|
||||||
|
$deliveryDate = trim($_POST['delivery_date'] ?? '');
|
||||||
|
$deliveredBy = trim($_POST['delivered_by'] ?? '');
|
||||||
|
$notes = trim($_POST['notes'] ?? '');
|
||||||
|
$employeeIds = $_POST['employee_ids'] ?? [];
|
||||||
|
|
||||||
|
if (!is_array($employeeIds)) {
|
||||||
|
$employeeIds = [];
|
||||||
|
}
|
||||||
|
$employeeIds = array_values(array_unique(array_filter(array_map('intval', $employeeIds), fn($v) => $v > 0)));
|
||||||
|
|
||||||
|
if ($itemName === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Il nome del DPI è obbligatorio.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($deliveryDate !== '' && !DateTime::createFromFormat('Y-m-d', $deliveryDate)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Data di consegna non valida.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (empty($employeeIds)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un dipendente.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deliveryDate = $deliveryDate === '' ? null : $deliveryDate;
|
||||||
|
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
|
||||||
|
$notes = $notes !== '' ? $notes : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// Only insert for employees that actually exist
|
||||||
|
$checkEmp = $pdo->prepare("SELECT id FROM employees WHERE id = :id");
|
||||||
|
|
||||||
|
$ins = $pdo->prepare("
|
||||||
|
INSERT INTO employee_ppe
|
||||||
|
(employee_id, item_name, delivery_date, delivered_by, notes, created_by, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(:employee_id, :item_name, :delivery_date, :delivered_by, :notes, :created_by, NOW(), NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
$created = 0;
|
||||||
|
foreach ($employeeIds as $eid) {
|
||||||
|
$checkEmp->execute(['id' => $eid]);
|
||||||
|
if (!$checkEmp->fetchColumn()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$ins->execute([
|
||||||
|
'employee_id' => $eid,
|
||||||
|
'item_name' => $itemName,
|
||||||
|
'delivery_date' => $deliveryDate,
|
||||||
|
'delivered_by' => $deliveredBy,
|
||||||
|
'notes' => $notes,
|
||||||
|
'created_by' => $currentUserId,
|
||||||
|
]);
|
||||||
|
$created++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'created' => $created,
|
||||||
|
'message' => 'DPI assegnato a ' . $created . ' dipendent' . ($created === 1 ? 'e' : 'i') . '.',
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$employeeId = (int)($_POST['employee_id'] ?? 0);
|
||||||
|
$firstName = trim($_POST['first_name'] ?? '');
|
||||||
|
$lastName = trim($_POST['last_name'] ?? '');
|
||||||
|
$employeeCode = trim($_POST['employee_code'] ?? '');
|
||||||
|
$address = trim($_POST['address'] ?? '');
|
||||||
|
$phone = trim($_POST['phone'] ?? '');
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
$hireDate = trim($_POST['hire_date'] ?? '');
|
||||||
|
$departmentId = $_POST['department_id'] ?? '';
|
||||||
|
$jobRoleId = $_POST['job_role_id'] ?? '';
|
||||||
|
$status = trim($_POST['status'] ?? '');
|
||||||
|
$authUserId = $_POST['auth_user_id'] ?? '';
|
||||||
|
$roleId = $_POST['role_id'] ?? '';
|
||||||
|
|
||||||
|
if ($employeeId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($firstName === '' || $lastName === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Nome e cognome sono obbligatori.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedStatus = ['active', 'inactive', 'suspended'];
|
||||||
|
if (!in_array($status, $allowedStatus, true)) {
|
||||||
|
$status = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
$departmentId = ($departmentId === '' || $departmentId === null) ? null : (int)$departmentId;
|
||||||
|
$jobRoleId = ($jobRoleId === '' || $jobRoleId === null) ? null : (int)$jobRoleId;
|
||||||
|
$authUserId = ($authUserId === '' || $authUserId === null) ? null : (int)$authUserId;
|
||||||
|
$roleId = ($roleId === '' || $roleId === null) ? null : (int)$roleId;
|
||||||
|
$hireDate = $hireDate === '' ? null : $hireDate;
|
||||||
|
|
||||||
|
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Email non valida.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($employeeCode !== '') {
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE employee_code = :code AND id <> :id");
|
||||||
|
$check->execute(['code' => $employeeCode, 'id' => $employeeId]);
|
||||||
|
if ((int)$check->fetchColumn() > 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Codice dipendente già in uso.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authUserId !== null) {
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE auth_user_id = :uid AND id <> :id");
|
||||||
|
$check->execute(['uid' => $authUserId, 'id' => $employeeId]);
|
||||||
|
if ((int)$check->fetchColumn() > 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Questo utente è già associato ad un altro dipendente.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE employees
|
||||||
|
SET first_name = :first_name,
|
||||||
|
last_name = :last_name,
|
||||||
|
employee_code = :employee_code,
|
||||||
|
address = :address,
|
||||||
|
phone = :phone,
|
||||||
|
email = :email,
|
||||||
|
hire_date = :hire_date,
|
||||||
|
department_id = :department_id,
|
||||||
|
job_role_id = :job_role_id,
|
||||||
|
status = :status,
|
||||||
|
auth_user_id = :auth_user_id,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
'first_name' => $firstName,
|
||||||
|
'last_name' => $lastName,
|
||||||
|
'employee_code' => $employeeCode !== '' ? $employeeCode : null,
|
||||||
|
'address' => $address !== '' ? $address : null,
|
||||||
|
'phone' => $phone !== '' ? $phone : null,
|
||||||
|
'email' => $email !== '' ? $email : null,
|
||||||
|
'hire_date' => $hireDate,
|
||||||
|
'department_id' => $departmentId,
|
||||||
|
'job_role_id' => $jobRoleId,
|
||||||
|
'status' => $status,
|
||||||
|
'auth_user_id' => $authUserId,
|
||||||
|
'id' => $employeeId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Optionally update Vanguard role for the linked auth_user
|
||||||
|
if ($authUserId !== null && $roleId !== null) {
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM auth_roles WHERE id = ?");
|
||||||
|
$check->execute([$roleId]);
|
||||||
|
if ((int)$check->fetchColumn() > 0) {
|
||||||
|
$upd = $pdo->prepare("UPDATE auth_users SET role_id = :role_id, updated_at = NOW() WHERE id = :uid");
|
||||||
|
$upd->execute(['role_id' => $roleId, 'uid' => $authUserId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
include('../../include/headscript.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = isset($_POST['id']) && $_POST['id'] !== '' ? (int)$_POST['id'] : null;
|
||||||
|
$employeeId = (int)($_POST['employee_id'] ?? 0);
|
||||||
|
$ppeItemId = (int)($_POST['ppe_item_id'] ?? 0);
|
||||||
|
$assignedDate = trim($_POST['assigned_date'] ?? '');
|
||||||
|
$expiryDate = trim($_POST['expiry_date'] ?? '');
|
||||||
|
$deliveredBy = trim($_POST['delivered_by'] ?? '');
|
||||||
|
$status = trim($_POST['status'] ?? 'assigned');
|
||||||
|
$notes = trim($_POST['notes'] ?? '');
|
||||||
|
|
||||||
|
$allowedStatuses = [
|
||||||
|
'assigned',
|
||||||
|
'returned',
|
||||||
|
'expired',
|
||||||
|
'lost',
|
||||||
|
'damaged',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($employeeId <= 0) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Dipendente non valido.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ppeItemId <= 0) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Selezionare un DPI.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($status, $allowedStatuses, true)) {
|
||||||
|
$status = 'assigned';
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkEmployee = $pdo->prepare("SELECT id FROM employees WHERE id = ? LIMIT 1");
|
||||||
|
$checkEmployee->execute([$employeeId]);
|
||||||
|
|
||||||
|
if (!$checkEmployee->fetchColumn()) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Dipendente non trovato.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkPpe = $pdo->prepare("SELECT id FROM ppe_items WHERE id = ? LIMIT 1");
|
||||||
|
$checkPpe->execute([$ppeItemId]);
|
||||||
|
|
||||||
|
if (!$checkPpe->fetchColumn()) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'DPI non trovato.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE employee_ppe_items
|
||||||
|
SET ppe_item_id = :ppe_item_id,
|
||||||
|
assigned_date = :assigned_date,
|
||||||
|
expiry_date = :expiry_date,
|
||||||
|
delivered_by = :delivered_by,
|
||||||
|
status = :status,
|
||||||
|
notes = :notes,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
AND employee_id = :employee_id
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'ppe_item_id' => $ppeItemId,
|
||||||
|
'assigned_date' => $assignedDate !== '' ? $assignedDate : null,
|
||||||
|
'expiry_date' => $expiryDate !== '' ? $expiryDate : null,
|
||||||
|
'delivered_by' => $deliveredBy !== '' ? $deliveredBy : null,
|
||||||
|
'status' => $status,
|
||||||
|
'notes' => $notes !== '' ? $notes : null,
|
||||||
|
'id' => $id,
|
||||||
|
'employee_id' => $employeeId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'DPI aggiornato.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO employee_ppe_items
|
||||||
|
(
|
||||||
|
employee_id,
|
||||||
|
ppe_item_id,
|
||||||
|
assigned_date,
|
||||||
|
expiry_date,
|
||||||
|
delivered_by,
|
||||||
|
quantity,
|
||||||
|
status,
|
||||||
|
notes,
|
||||||
|
created_by,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
:employee_id,
|
||||||
|
:ppe_item_id,
|
||||||
|
:assigned_date,
|
||||||
|
:expiry_date,
|
||||||
|
:delivered_by,
|
||||||
|
1,
|
||||||
|
:status,
|
||||||
|
:notes,
|
||||||
|
:created_by,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'employee_id' => $employeeId,
|
||||||
|
'ppe_item_id' => $ppeItemId,
|
||||||
|
'assigned_date' => $assignedDate !== '' ? $assignedDate : null,
|
||||||
|
'expiry_date' => $expiryDate !== '' ? $expiryDate : null,
|
||||||
|
'delivered_by' => $deliveredBy !== '' ? $deliveredBy : null,
|
||||||
|
'status' => $status,
|
||||||
|
'notes' => $notes !== '' ? $notes : null,
|
||||||
|
'created_by' => isset($iduserlogin) ? (int)$iduserlogin : null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'DPI assegnato.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$employeeId = (int)($_POST['employee_id'] ?? 0);
|
||||||
|
$topicId = (int)($_POST['training_topic_id'] ?? 0);
|
||||||
|
$completedDate = trim($_POST['completed_date'] ?? '');
|
||||||
|
$deliveredBy = trim($_POST['delivered_by'] ?? '');
|
||||||
|
$description = trim($_POST['description'] ?? '');
|
||||||
|
$trainingType = trim($_POST['training_type'] ?? 'initial');
|
||||||
|
$freqRaw = $_POST['update_frequency_months'] ?? '';
|
||||||
|
$remRaw = $_POST['reminder_days'] ?? '';
|
||||||
|
|
||||||
|
if ($employeeId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($topicId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Selezionare un corso.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($completedDate === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'La data di completamento è obbligatoria.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (!in_array($trainingType, ['initial', 'refresher'], true)) {
|
||||||
|
$trainingType = 'initial';
|
||||||
|
}
|
||||||
|
|
||||||
|
$topicStmt = $pdo->prepare("SELECT default_frequency_months, default_reminder_days FROM training_topics WHERE id = :id");
|
||||||
|
$topicStmt->execute(['id' => $topicId]);
|
||||||
|
$topic = $topicStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$topic) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Corso non trovato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
|
||||||
|
$rem = ($remRaw === '' || $remRaw === null) ? null : max(0, (int)$remRaw);
|
||||||
|
|
||||||
|
/* Effective frequency for next_due_date: explicit override or topic default */
|
||||||
|
$effFreq = $freq !== null ? $freq : ($topic['default_frequency_months'] !== null ? (int)$topic['default_frequency_months'] : null);
|
||||||
|
|
||||||
|
$nextDue = null;
|
||||||
|
if ($effFreq !== null && $effFreq > 0) {
|
||||||
|
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
|
||||||
|
if ($d) {
|
||||||
|
$d->modify('+' . (int)$effFreq . ' months');
|
||||||
|
$nextDue = $d->format('Y-m-d');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
|
||||||
|
$description = $description !== '' ? $description : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
if ($id > 0) {
|
||||||
|
$old = $pdo->prepare("SELECT * FROM employee_trainings WHERE id = :id");
|
||||||
|
$old->execute(['id' => $id]);
|
||||||
|
$oldRow = $old->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$oldRow) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$upd = $pdo->prepare("
|
||||||
|
UPDATE employee_trainings
|
||||||
|
SET training_topic_id = :topic_id,
|
||||||
|
completed_date = :completed_date,
|
||||||
|
delivered_by = :delivered_by,
|
||||||
|
description = :description,
|
||||||
|
training_type = :training_type,
|
||||||
|
update_frequency_months = :freq,
|
||||||
|
reminder_days = :rem,
|
||||||
|
next_due_date = :next_due,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
$upd->execute([
|
||||||
|
'topic_id' => $topicId,
|
||||||
|
'completed_date' => $completedDate,
|
||||||
|
'delivered_by' => $deliveredBy,
|
||||||
|
'description' => $description,
|
||||||
|
'training_type' => $trainingType,
|
||||||
|
'freq' => $freq,
|
||||||
|
'rem' => $rem,
|
||||||
|
'next_due' => $nextDue,
|
||||||
|
'id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'training_topic_id' => $topicId,
|
||||||
|
'completed_date' => $completedDate,
|
||||||
|
'delivered_by' => $deliveredBy,
|
||||||
|
'description' => $description,
|
||||||
|
'training_type' => $trainingType,
|
||||||
|
'update_frequency_months' => $freq,
|
||||||
|
'reminder_days' => $rem,
|
||||||
|
'next_due_date' => $nextDue,
|
||||||
|
];
|
||||||
|
$logStmt = $pdo->prepare("
|
||||||
|
INSERT INTO employee_training_log
|
||||||
|
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, 'updated', :field, :old_v, :new_v, :cb, NOW())
|
||||||
|
");
|
||||||
|
foreach ($fields as $f => $newV) {
|
||||||
|
$oldV = $oldRow[$f] ?? null;
|
||||||
|
if ((string)$oldV !== (string)$newV) {
|
||||||
|
$logStmt->execute([
|
||||||
|
'eid' => $employeeId,
|
||||||
|
'tid' => $id,
|
||||||
|
'field' => $f,
|
||||||
|
'old_v' => $oldV,
|
||||||
|
'new_v' => $newV,
|
||||||
|
'cb' => $currentUserId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true, 'id' => $id]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ins = $pdo->prepare("
|
||||||
|
INSERT INTO employee_trainings
|
||||||
|
(employee_id, training_topic_id, completed_date,
|
||||||
|
delivered_by, description,
|
||||||
|
training_type, update_frequency_months, reminder_days, next_due_date,
|
||||||
|
created_by, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, :completed_date,
|
||||||
|
:delivered_by, :description,
|
||||||
|
:training_type, :freq, :rem, :next_due,
|
||||||
|
:cb, NOW(), NOW())
|
||||||
|
");
|
||||||
|
$ins->execute([
|
||||||
|
'eid' => $employeeId,
|
||||||
|
'tid' => $topicId,
|
||||||
|
'completed_date' => $completedDate,
|
||||||
|
'delivered_by' => $deliveredBy,
|
||||||
|
'description' => $description,
|
||||||
|
'training_type' => $trainingType,
|
||||||
|
'freq' => $freq,
|
||||||
|
'rem' => $rem,
|
||||||
|
'next_due' => $nextDue,
|
||||||
|
'cb' => $currentUserId,
|
||||||
|
]);
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
$pdo->prepare("
|
||||||
|
INSERT INTO employee_training_log
|
||||||
|
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, 'created', NULL, NULL, NULL, :cb, NOW())
|
||||||
|
")->execute(['eid' => $employeeId, 'tid' => $newId, 'cb' => $currentUserId]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true, 'id' => $newId]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$employeeId = (int)($_POST['employee_id'] ?? 0);
|
||||||
|
$category = trim($_POST['category'] ?? 'other');
|
||||||
|
$notes = trim($_POST['notes'] ?? '');
|
||||||
|
|
||||||
|
$allowedCategories = ['job_description', 'contract', 'rules', 'other'];
|
||||||
|
if (!in_array($category, $allowedCategories, true)) {
|
||||||
|
$category = 'other';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($employeeId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE id = :id");
|
||||||
|
$check->execute(['id' => $employeeId]);
|
||||||
|
if ((int)$check->fetchColumn() === 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Dipendente non trovato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$errCode = $_FILES['file']['error'] ?? -1;
|
||||||
|
$msg = 'Errore nel caricamento del file.';
|
||||||
|
if ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) {
|
||||||
|
$msg = 'Il file supera la dimensione massima consentita.';
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => false, 'message' => $msg]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalName = $_FILES['file']['name'];
|
||||||
|
$tmpPath = $_FILES['file']['tmp_name'];
|
||||||
|
$size = (int)$_FILES['file']['size'];
|
||||||
|
$mimeType = mime_content_type($tmpPath) ?: ($_FILES['file']['type'] ?? null);
|
||||||
|
|
||||||
|
$dir = __DIR__ . '/../../files/employees/' . $employeeId . '/documents';
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
if (!mkdir($dir, 0775, true) && !is_dir($dir)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Impossibile creare la cartella di destinazione.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeOriginal = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
|
||||||
|
$storedName = uniqid('doc_') . '_' . $safeOriginal;
|
||||||
|
$destPath = $dir . '/' . $storedName;
|
||||||
|
|
||||||
|
if (!move_uploaded_file($tmpPath, $destPath)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file su disco.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO employee_documents
|
||||||
|
(employee_id, category, original_name, stored_name, mime_type, size, notes, uploaded_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(:employee_id, :category, :original_name, :stored_name, :mime_type, :size, :notes, :uploaded_by, NOW())
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
'employee_id' => $employeeId,
|
||||||
|
'category' => $category,
|
||||||
|
'original_name' => $originalName,
|
||||||
|
'stored_name' => $storedName,
|
||||||
|
'mime_type' => $mimeType,
|
||||||
|
'size' => $size,
|
||||||
|
'notes' => $notes !== '' ? $notes : null,
|
||||||
|
'uploaded_by' => $currentUserId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
@unlink($destPath);
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$trainingId = (int)($_POST['training_id'] ?? 0);
|
||||||
|
if ($trainingId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tr = $pdo->prepare("SELECT employee_id FROM employee_trainings WHERE id = :id");
|
||||||
|
$tr->execute(['id' => $trainingId]);
|
||||||
|
$trainingRow = $tr->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$trainingRow) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$employeeId = (int)$trainingRow['employee_id'];
|
||||||
|
|
||||||
|
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$errCode = $_FILES['file']['error'] ?? -1;
|
||||||
|
$msg = 'Errore nel caricamento del file.';
|
||||||
|
if ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) {
|
||||||
|
$msg = 'Il file supera la dimensione massima consentita.';
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => false, 'message' => $msg]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalName = $_FILES['file']['name'];
|
||||||
|
$tmpPath = $_FILES['file']['tmp_name'];
|
||||||
|
$size = (int)$_FILES['file']['size'];
|
||||||
|
$mimeType = mime_content_type($tmpPath) ?: ($_FILES['file']['type'] ?? null);
|
||||||
|
|
||||||
|
$dir = __DIR__ . '/../../files/employees/' . $employeeId . '/trainings';
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
if (!mkdir($dir, 0775, true) && !is_dir($dir)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Impossibile creare la cartella di destinazione.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeOriginal = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
|
||||||
|
$storedName = uniqid('tr_') . '_' . $safeOriginal;
|
||||||
|
$destPath = $dir . '/' . $storedName;
|
||||||
|
|
||||||
|
if (!move_uploaded_file($tmpPath, $destPath)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file su disco.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$ins = $pdo->prepare("
|
||||||
|
INSERT INTO employee_training_attachments
|
||||||
|
(training_id, original_name, stored_name, mime_type, size, uploaded_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(:tid, :original_name, :stored_name, :mime_type, :size, :uploaded_by, NOW())
|
||||||
|
");
|
||||||
|
$ins->execute([
|
||||||
|
'tid' => $trainingId,
|
||||||
|
'original_name' => $originalName,
|
||||||
|
'stored_name' => $storedName,
|
||||||
|
'mime_type' => $mimeType,
|
||||||
|
'size' => $size,
|
||||||
|
'uploaded_by' => $currentUserId,
|
||||||
|
]);
|
||||||
|
$attachmentId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
$pdo->prepare("
|
||||||
|
INSERT INTO employee_training_log
|
||||||
|
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, 'attachment_added', 'attachment', NULL, :name, :cb, NOW())
|
||||||
|
")->execute([
|
||||||
|
'eid' => $employeeId,
|
||||||
|
'tid' => $trainingId,
|
||||||
|
'name' => $originalName,
|
||||||
|
'cb' => $currentUserId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true, 'id' => $attachmentId]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
@unlink($destPath);
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* HR auth check for AJAX endpoints that require HR-management permissions.
|
||||||
|
* Allowed roles: Admin, User, Superuser, employee-hr, manager.
|
||||||
|
* Sets $currentUserId and $currentUserRole, or returns 401/403 JSON.
|
||||||
|
*/
|
||||||
|
require_once(__DIR__ . '/auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../class/db-functions.php');
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT r.name AS role_name
|
||||||
|
FROM auth_users u
|
||||||
|
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||||
|
WHERE u.id = :id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute(['id' => $currentUserId]);
|
||||||
|
$currentUserRole = (string)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$allowedHrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||||
|
|
||||||
|
if (!in_array($currentUserRole, $allowedHrRoles, true)) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Permessi insufficienti per questa operazione.',
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID mansione non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$usage = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE job_role_id = :id");
|
||||||
|
$usage->execute(['id' => $id]);
|
||||||
|
if ((int)$usage->fetchColumn() > 0) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Questa mansione è associata a uno o più dipendenti e non può essere cancellata.',
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM job_roles WHERE id = :id");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$description = trim($_POST['description'] ?? '');
|
||||||
|
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
|
||||||
|
$is_active = isset($_POST['is_active']) ? ((int)$_POST['is_active'] === 1 ? 1 : 0) : 1;
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Il nome della mansione è obbligatorio.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($id > 0) {
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM job_roles WHERE name = :name AND id <> :id");
|
||||||
|
$check->execute(['name' => $name, 'id' => $id]);
|
||||||
|
if ((int)$check->fetchColumn() > 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Esiste già un\'altra mansione con questo nome.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE job_roles
|
||||||
|
SET name = :name,
|
||||||
|
description = :description,
|
||||||
|
sort_order = :sort_order,
|
||||||
|
is_active = :is_active,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description !== '' ? $description : null,
|
||||||
|
'sort_order' => $sort_order,
|
||||||
|
'is_active' => $is_active,
|
||||||
|
'id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'id' => $id]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM job_roles WHERE name = :name");
|
||||||
|
$check->execute(['name' => $name]);
|
||||||
|
if ((int)$check->fetchColumn() > 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Esiste già una mansione con questo nome.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO job_roles (name, description, sort_order, is_active, created_at, updated_at)
|
||||||
|
VALUES (:name, :description, :sort_order, :is_active, NOW(), NOW())
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description !== '' ? $description : null,
|
||||||
|
'sort_order' => $sort_order,
|
||||||
|
'is_active' => $is_active,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID corso non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$usage = $pdo->prepare("SELECT COUNT(*) FROM employee_trainings WHERE training_topic_id = :id");
|
||||||
|
$usage->execute(['id' => $id]);
|
||||||
|
if ((int)$usage->fetchColumn() > 0) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Questo corso ha già delle registrazioni di formazione e non può essere cancellato.',
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM training_topics WHERE id = :id");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
require_once(__DIR__ . '/../../include/training_coverage.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$description = trim($_POST['description'] ?? '');
|
||||||
|
$freqRaw = $_POST['default_frequency_months'] ?? '';
|
||||||
|
$remRaw = $_POST['default_reminder_days'] ?? '';
|
||||||
|
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
|
||||||
|
$is_active = isset($_POST['is_active']) ? ((int)$_POST['is_active'] === 1 ? 1 : 0) : 1;
|
||||||
|
$is_mandatory = isset($_POST['is_mandatory']) && (int)$_POST['is_mandatory'] === 1 ? 1 : 0;
|
||||||
|
|
||||||
|
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
|
||||||
|
$rem = ($remRaw === '' || $remRaw === null) ? 30 : max(0, (int)$remRaw);
|
||||||
|
|
||||||
|
/* Coperture: corsi assorbiti da questo */
|
||||||
|
$coveredIds = isset($_POST['covered_ids']) ? (array)$_POST['covered_ids'] : [];
|
||||||
|
$coveredIds = array_values(array_unique(array_filter(array_map('intval', $coveredIds), function ($v) {
|
||||||
|
return $v > 0;
|
||||||
|
})));
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Il nome del corso è obbligatorio.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Riscrive le coperture del corso: cancella le vecchie e inserisce le nuove,
|
||||||
|
* scartando auto-copertura e relazioni che creerebbero un ciclo.
|
||||||
|
*/
|
||||||
|
function saveCoverage(PDO $pdo, int $topicId, array $coveredIds): array
|
||||||
|
{
|
||||||
|
$skipped = [];
|
||||||
|
|
||||||
|
$pdo->prepare("DELETE FROM training_topic_coverage WHERE topic_id = :tid")
|
||||||
|
->execute(['tid' => $topicId]);
|
||||||
|
|
||||||
|
if (!$coveredIds) return $skipped;
|
||||||
|
|
||||||
|
$ins = $pdo->prepare("
|
||||||
|
INSERT IGNORE INTO training_topic_coverage
|
||||||
|
(topic_id, covered_topic_id, inherit_expiry, created_at)
|
||||||
|
VALUES (:tid, :cid, 'own', NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
foreach ($coveredIds as $cid) {
|
||||||
|
if ($cid === $topicId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tc_wouldCreateCycle($pdo, $topicId, $cid)) {
|
||||||
|
$skipped[] = $cid;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$ins->execute(['tid' => $topicId, 'cid' => $cid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $skipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
if ($id > 0) {
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM training_topics WHERE name = :name AND id <> :id");
|
||||||
|
$check->execute(['name' => $name, 'id' => $id]);
|
||||||
|
if ((int)$check->fetchColumn() > 0) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Esiste già un altro corso con questo nome.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE training_topics
|
||||||
|
SET name = :name,
|
||||||
|
description = :description,
|
||||||
|
default_frequency_months = :freq,
|
||||||
|
default_reminder_days = :rem,
|
||||||
|
sort_order = :sort_order,
|
||||||
|
is_active = :is_active,
|
||||||
|
is_mandatory = :is_mandatory,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description !== '' ? $description : null,
|
||||||
|
'freq' => $freq,
|
||||||
|
'rem' => $rem,
|
||||||
|
'sort_order' => $sort_order,
|
||||||
|
'is_active' => $is_active,
|
||||||
|
'is_mandatory' => $is_mandatory,
|
||||||
|
'id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$skipped = saveCoverage($pdo, $id, $coveredIds);
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
$msg = $skipped
|
||||||
|
? 'Corso aggiornato. Alcune coperture sono state ignorate perché avrebbero creato un riferimento circolare.'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
echo json_encode(array_filter([
|
||||||
|
'success' => true,
|
||||||
|
'id' => $id,
|
||||||
|
'message' => $msg,
|
||||||
|
], function ($v) {
|
||||||
|
return $v !== null;
|
||||||
|
}));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM training_topics WHERE name = :name");
|
||||||
|
$check->execute(['name' => $name]);
|
||||||
|
if ((int)$check->fetchColumn() > 0) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Esiste già un corso con questo nome.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO training_topics
|
||||||
|
(name, description, default_frequency_months, default_reminder_days, sort_order, is_active, is_mandatory, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(:name, :description, :freq, :rem, :sort_order, :is_active, :is_mandatory, NOW(), NOW())
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description !== '' ? $description : null,
|
||||||
|
'freq' => $freq,
|
||||||
|
'rem' => $rem,
|
||||||
|
'sort_order' => $sort_order,
|
||||||
|
'is_active' => $is_active,
|
||||||
|
'is_mandatory' => $is_mandatory,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
saveCoverage($pdo, $newId, $coveredIds);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true, 'id' => $newId]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Bulk "renew": set a common completed_date on the selected training records
|
||||||
|
*/
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $pdo and $currentUserId from hr_auth_check.php
|
||||||
|
|
||||||
|
$completedDate = trim($_POST['completed_date'] ?? '');
|
||||||
|
$ids = $_POST['training_ids'] ?? [];
|
||||||
|
|
||||||
|
if (!is_array($ids)) {
|
||||||
|
$ids = [];
|
||||||
|
}
|
||||||
|
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), fn($v) => $v > 0)));
|
||||||
|
|
||||||
|
if ($completedDate === '' || !DateTime::createFromFormat('Y-m-d', $completedDate)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Indicare una data valida.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (empty($ids)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un record.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// Load each record with its topic default frequency
|
||||||
|
$rowStmt = $pdo->prepare("
|
||||||
|
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
|
||||||
|
et.update_frequency_months, tt.default_frequency_months
|
||||||
|
FROM employee_trainings et
|
||||||
|
JOIN training_topics tt ON tt.id = et.training_topic_id
|
||||||
|
WHERE et.id = :id
|
||||||
|
");
|
||||||
|
$upd = $pdo->prepare("
|
||||||
|
UPDATE employee_trainings
|
||||||
|
SET completed_date = :cd, next_due_date = :nd, updated_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
$logStmt = $pdo->prepare("
|
||||||
|
INSERT INTO employee_training_log
|
||||||
|
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, 'updated', :field, :old_v, :new_v, :cb, NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
$updated = 0;
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$rowStmt->execute(['id' => $id]);
|
||||||
|
$row = $rowStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$row) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effective frequency: per-record override, else topic default
|
||||||
|
$effFreq = $row['update_frequency_months'] !== null
|
||||||
|
? (int)$row['update_frequency_months']
|
||||||
|
: ($row['default_frequency_months'] !== null ? (int)$row['default_frequency_months'] : null);
|
||||||
|
|
||||||
|
$nextDue = null;
|
||||||
|
if ($effFreq !== null && $effFreq > 0) {
|
||||||
|
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
|
||||||
|
if ($d) {
|
||||||
|
$d->modify('+' . $effFreq . ' months');
|
||||||
|
$nextDue = $d->format('Y-m-d');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$upd->execute(['cd' => $completedDate, 'nd' => $nextDue, 'id' => $id]);
|
||||||
|
|
||||||
|
if ((string)$row['completed_date'] !== (string)$completedDate) {
|
||||||
|
$logStmt->execute([
|
||||||
|
'eid' => $row['employee_id'], 'tid' => $id, 'field' => 'completed_date',
|
||||||
|
'old_v' => $row['completed_date'], 'new_v' => $completedDate, 'cb' => $currentUserId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ((string)$row['next_due_date'] !== (string)$nextDue) {
|
||||||
|
$logStmt->execute([
|
||||||
|
'eid' => $row['employee_id'], 'tid' => $id, 'field' => 'next_due_date',
|
||||||
|
'old_v' => $row['next_due_date'], 'new_v' => $nextDue, 'cb' => $currentUserId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'updated' => $updated,
|
||||||
|
'message' => $updated . ' record aggiornat' . ($updated === 1 ? 'o' : 'i') . '.',
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Calendar events for the training calendar (training_calendar.php).
|
||||||
|
* Returns FullCalendar event objects for the *current* training record per
|
||||||
|
* (employee, topic) that has a next_due_date, colored by computed status.
|
||||||
|
* HR-only.
|
||||||
|
*/
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// $pdo and $currentUserId provided by hr_auth_check.php
|
||||||
|
|
||||||
|
$start = $_GET['start'] ?? null;
|
||||||
|
$end = $_GET['end'] ?? null;
|
||||||
|
$fStatus = isset($_GET['status']) ? trim($_GET['status']) : '';
|
||||||
|
$fDept = isset($_GET['department_id']) && $_GET['department_id'] !== '' ? (int)$_GET['department_id'] : 0;
|
||||||
|
$fTopic = isset($_GET['topic_id']) && $_GET['topic_id'] !== '' ? (int)$_GET['topic_id'] : 0;
|
||||||
|
$fEmp = isset($_GET['employee_id']) && $_GET['employee_id'] !== '' ? (int)$_GET['employee_id'] : 0;
|
||||||
|
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
// Deadlines only (one-time trainings have no next_due_date)
|
||||||
|
$where[] = "et.next_due_date IS NOT NULL";
|
||||||
|
|
||||||
|
// Only the most recent record per (employee, topic)
|
||||||
|
$where[] = "NOT EXISTS (
|
||||||
|
SELECT 1 FROM employee_trainings et2
|
||||||
|
WHERE et2.employee_id = et.employee_id
|
||||||
|
AND et2.training_topic_id = et.training_topic_id
|
||||||
|
AND (et2.completed_date > et.completed_date
|
||||||
|
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
|
||||||
|
)";
|
||||||
|
|
||||||
|
if ($start && $end) {
|
||||||
|
$where[] = "et.next_due_date >= :start AND et.next_due_date <= :end";
|
||||||
|
$params['start'] = $start;
|
||||||
|
$params['end'] = $end;
|
||||||
|
}
|
||||||
|
if ($fDept > 0) { $where[] = "e.department_id = :did"; $params['did'] = $fDept; }
|
||||||
|
if ($fTopic > 0) { $where[] = "et.training_topic_id = :tid"; $params['tid'] = $fTopic; }
|
||||||
|
if ($fEmp > 0) { $where[] = "et.employee_id = :eid"; $params['eid'] = $fEmp; }
|
||||||
|
|
||||||
|
$whereSql = 'WHERE ' . implode(' AND ', $where);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT et.id, et.employee_id, et.next_due_date, et.reminder_days,
|
||||||
|
tt.name AS topic_name, tt.default_reminder_days AS topic_default_rem,
|
||||||
|
e.first_name, e.last_name
|
||||||
|
FROM employee_trainings et
|
||||||
|
JOIN training_topics tt ON tt.id = et.training_topic_id
|
||||||
|
JOIN employees e ON e.id = et.employee_id
|
||||||
|
$whereSql
|
||||||
|
");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$today = new DateTime('today');
|
||||||
|
$events = [];
|
||||||
|
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$rem = $r['reminder_days'] !== null
|
||||||
|
? (int)$r['reminder_days']
|
||||||
|
: ($r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : 30);
|
||||||
|
|
||||||
|
$due = DateTime::createFromFormat('Y-m-d', $r['next_due_date']);
|
||||||
|
if (!$due) continue;
|
||||||
|
$daysLeft = (int)$today->diff($due)->format('%r%a');
|
||||||
|
|
||||||
|
if ($daysLeft < 0) { $code = 'expired'; $color = '#dc3545'; }
|
||||||
|
elseif ($daysLeft <= $rem){ $code = 'due_soon'; $color = '#e8930c'; }
|
||||||
|
else { $code = 'compliant'; $color = '#198754'; }
|
||||||
|
|
||||||
|
if ($fStatus !== '' && $fStatus !== $code) continue;
|
||||||
|
|
||||||
|
$name = trim($r['first_name'] . ' ' . $r['last_name']);
|
||||||
|
$events[] = [
|
||||||
|
'id' => (int)$r['id'],
|
||||||
|
'title' => $name . ' — ' . $r['topic_name'],
|
||||||
|
'start' => $r['next_due_date'],
|
||||||
|
'allDay' => true,
|
||||||
|
'backgroundColor' => $color,
|
||||||
|
'borderColor' => $color,
|
||||||
|
'url' => 'employee-profile.php?id=' . (int)$r['employee_id'] . '#tab-training',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($events);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode([]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Bulk-create training records: one employee_trainings row per selected employee,
|
||||||
|
* all sharing the same course + parameters (a single training "session").
|
||||||
|
* Mirrors the next_due_date logic of ajax/employee_profile/save_training.php.
|
||||||
|
* HR-only.
|
||||||
|
*/
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $pdo and $currentUserId from hr_auth_check.php
|
||||||
|
|
||||||
|
$topicId = (int)($_POST['training_topic_id'] ?? 0);
|
||||||
|
$completedDate = trim($_POST['completed_date'] ?? '');
|
||||||
|
$deliveredBy = trim($_POST['delivered_by'] ?? '');
|
||||||
|
$description = trim($_POST['description'] ?? '');
|
||||||
|
$trainingType = trim($_POST['training_type'] ?? 'initial');
|
||||||
|
$freqRaw = $_POST['update_frequency_months'] ?? '';
|
||||||
|
$remRaw = $_POST['reminder_days'] ?? '';
|
||||||
|
$employeeIds = $_POST['employee_ids'] ?? [];
|
||||||
|
|
||||||
|
if (!is_array($employeeIds)) {
|
||||||
|
$employeeIds = [];
|
||||||
|
}
|
||||||
|
$employeeIds = array_values(array_unique(array_filter(array_map('intval', $employeeIds), fn($v) => $v > 0)));
|
||||||
|
|
||||||
|
if ($topicId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Selezionare un corso.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($completedDate === '' || !DateTime::createFromFormat('Y-m-d', $completedDate)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'La data di completamento è obbligatoria.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (empty($employeeIds)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un dipendente.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (!in_array($trainingType, ['initial', 'refresher'], true)) {
|
||||||
|
$trainingType = 'initial';
|
||||||
|
}
|
||||||
|
|
||||||
|
$topicStmt = $pdo->prepare("SELECT default_frequency_months, default_reminder_days FROM training_topics WHERE id = :id");
|
||||||
|
$topicStmt->execute(['id' => $topicId]);
|
||||||
|
$topic = $topicStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$topic) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Corso non trovato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
|
||||||
|
$rem = ($remRaw === '' || $remRaw === null) ? null : max(0, (int)$remRaw);
|
||||||
|
|
||||||
|
/* Effective frequency → next_due_date (same for every employee: same date + same frequency) */
|
||||||
|
$effFreq = $freq !== null ? $freq : ($topic['default_frequency_months'] !== null ? (int)$topic['default_frequency_months'] : null);
|
||||||
|
$nextDue = null;
|
||||||
|
if ($effFreq !== null && $effFreq > 0) {
|
||||||
|
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
|
||||||
|
if ($d) {
|
||||||
|
$d->modify('+' . (int)$effFreq . ' months');
|
||||||
|
$nextDue = $d->format('Y-m-d');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
|
||||||
|
$description = $description !== '' ? $description : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// Only insert for employees that actually exist
|
||||||
|
$checkEmp = $pdo->prepare("SELECT id FROM employees WHERE id = :id");
|
||||||
|
|
||||||
|
$ins = $pdo->prepare("
|
||||||
|
INSERT INTO employee_trainings
|
||||||
|
(employee_id, training_topic_id, completed_date,
|
||||||
|
delivered_by, description,
|
||||||
|
training_type, update_frequency_months, reminder_days, next_due_date,
|
||||||
|
created_by, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, :completed_date,
|
||||||
|
:delivered_by, :description,
|
||||||
|
:training_type, :freq, :rem, :next_due,
|
||||||
|
:cb, NOW(), NOW())
|
||||||
|
");
|
||||||
|
$logStmt = $pdo->prepare("
|
||||||
|
INSERT INTO employee_training_log
|
||||||
|
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, 'created', NULL, NULL, NULL, :cb, NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
$created = 0;
|
||||||
|
foreach ($employeeIds as $eid) {
|
||||||
|
$checkEmp->execute(['id' => $eid]);
|
||||||
|
if (!$checkEmp->fetchColumn()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$ins->execute([
|
||||||
|
'eid' => $eid,
|
||||||
|
'tid' => $topicId,
|
||||||
|
'completed_date' => $completedDate,
|
||||||
|
'delivered_by' => $deliveredBy,
|
||||||
|
'description' => $description,
|
||||||
|
'training_type' => $trainingType,
|
||||||
|
'freq' => $freq,
|
||||||
|
'rem' => $rem,
|
||||||
|
'next_due' => $nextDue,
|
||||||
|
'cb' => $currentUserId,
|
||||||
|
]);
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
$logStmt->execute(['eid' => $eid, 'tid' => $newId, 'cb' => $currentUserId]);
|
||||||
|
$created++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'created' => $created,
|
||||||
|
'message' => $created . ' formazion' . ($created === 1 ? 'e registrata' : 'i registrate') . '.',
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/include/headscript.php');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$iduser = $iduserlogin ?? null;
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = (int)($input['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
throw new Exception('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($iduser === null) {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT *
|
||||||
|
FROM cad_area_jobs
|
||||||
|
WHERE id = :id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':id' => $id
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT *
|
||||||
|
FROM cad_area_jobs
|
||||||
|
WHERE id = :id
|
||||||
|
AND iduser = :iduser
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':id' => $id,
|
||||||
|
':iduser' => $iduser
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$job = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$job) {
|
||||||
|
throw new Exception('Record non trovato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($job['file_path']) && file_exists($job['file_path'])) {
|
||||||
|
unlink($job['file_path']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtDelete = $pdo->prepare("
|
||||||
|
DELETE FROM cad_area_jobs
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
$stmtDelete->execute([':id' => $id]);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('CAD area delete error: ' . $e->getMessage());
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/include/headscript.php');
|
||||||
|
|
||||||
|
function jsonResponse(array $data): void
|
||||||
|
{
|
||||||
|
echo json_encode($data);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateJobProcessing(PDO $pdo, int $id): void
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE cad_area_jobs
|
||||||
|
SET
|
||||||
|
status = 'processing',
|
||||||
|
message = 'Elaborazione in corso...',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateJobError(PDO $pdo, int $id, string $message, ?array $pythonResponse = null): void
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE cad_area_jobs
|
||||||
|
SET
|
||||||
|
status = 'error',
|
||||||
|
message = ?,
|
||||||
|
python_response = ?,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$message,
|
||||||
|
$pythonResponse ? json_encode($pythonResponse) : null,
|
||||||
|
$id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateJobCompleted(PDO $pdo, int $id, array $response): void
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE cad_area_jobs
|
||||||
|
SET
|
||||||
|
status = 'completed',
|
||||||
|
message = ?,
|
||||||
|
area_mm2 = ?,
|
||||||
|
area_cm2 = ?,
|
||||||
|
area_m2 = ?,
|
||||||
|
width_mm = ?,
|
||||||
|
height_mm = ?,
|
||||||
|
scale_detected = ?,
|
||||||
|
scale_used = ?,
|
||||||
|
confidence = ?,
|
||||||
|
python_response = ?,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$response['message'] ?? 'Area calcolata correttamente.',
|
||||||
|
$response['area_mm2'] ?? null,
|
||||||
|
$response['area_cm2'] ?? null,
|
||||||
|
$response['area_m2'] ?? null,
|
||||||
|
$response['width_mm'] ?? null,
|
||||||
|
$response['height_mm'] ?? null,
|
||||||
|
$response['scale_detected'] ?? null,
|
||||||
|
$response['scale_used'] ?? null,
|
||||||
|
$response['confidence'] ?? null,
|
||||||
|
json_encode($response),
|
||||||
|
$id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCalculationMode(?string $mode): string
|
||||||
|
{
|
||||||
|
$allowed = [
|
||||||
|
'auto_roi',
|
||||||
|
'stitch_contour',
|
||||||
|
'filled_union',
|
||||||
|
'closed_path'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$mode || !in_array($mode, $allowed, true)) {
|
||||||
|
return 'auto_roi';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasValidRoi(array $job): bool
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
array_key_exists('roi_x', $job) &&
|
||||||
|
array_key_exists('roi_y', $job) &&
|
||||||
|
array_key_exists('roi_width', $job) &&
|
||||||
|
array_key_exists('roi_height', $job) &&
|
||||||
|
$job['roi_x'] !== null &&
|
||||||
|
$job['roi_y'] !== null &&
|
||||||
|
$job['roi_width'] !== null &&
|
||||||
|
$job['roi_height'] !== null &&
|
||||||
|
(float)$job['roi_width'] > 0 &&
|
||||||
|
(float)$job['roi_height'] > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function callPythonAreaService(string $url, array $job): array
|
||||||
|
{
|
||||||
|
$filePath = $job['file_path'] ?? '';
|
||||||
|
$originalFilename = $job['original_filename'] ?? basename($filePath);
|
||||||
|
|
||||||
|
if (!$filePath || !file_exists($filePath)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'File PDF non trovato sul server: ' . $filePath
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = normalizeCalculationMode($job['calculation_mode'] ?? 'auto_roi');
|
||||||
|
|
||||||
|
$scaleRatio = $job['scale_used'] ?? null;
|
||||||
|
|
||||||
|
if ($scaleRatio === null || $scaleRatio === '' || (float)$scaleRatio <= 0) {
|
||||||
|
$scaleRatio = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
$curlFile = new CURLFile(
|
||||||
|
$filePath,
|
||||||
|
'application/pdf',
|
||||||
|
$originalFilename
|
||||||
|
);
|
||||||
|
|
||||||
|
$postFields = [
|
||||||
|
'file' => $curlFile,
|
||||||
|
'mode' => $mode,
|
||||||
|
'scale_ratio' => (string)$scaleRatio,
|
||||||
|
'roi_x' => (string)$job['roi_x'],
|
||||||
|
'roi_y' => (string)$job['roi_y'],
|
||||||
|
'roi_width' => (string)$job['roi_width'],
|
||||||
|
'roi_height' => (string)$job['roi_height'],
|
||||||
|
'roi_page' => (string)($job['roi_page'] ?? 1)
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_URL => $url,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $postFields,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 180,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 10,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Accept: application/json'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$rawResponse = curl_exec($ch);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($rawResponse === false) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Errore cURL verso Python: ' . $curlError
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($rawResponse, true);
|
||||||
|
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Risposta Python non JSON valida.',
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'raw_response' => $rawResponse
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode < 200 || $httpCode >= 300) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $decoded['message'] ?? ('Servizio Python HTTP ' . $httpCode),
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'python_response' => $decoded,
|
||||||
|
'raw_response' => $rawResponse
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$iduser = $iduserlogin ?? null;
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!is_array($input)) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Payload JSON non valido.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = $input['ids'] ?? [];
|
||||||
|
|
||||||
|
if (!is_array($ids) || count($ids) === 0) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Nessun ID ricevuto.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = array_values(array_unique(array_map('intval', $ids)));
|
||||||
|
$ids = array_filter($ids, fn($id) => $id > 0);
|
||||||
|
|
||||||
|
if (count($ids) === 0) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Nessun ID valido ricevuto.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pythonServiceUrl = 'http://127.0.0.1:5055/calculate';
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
if ($iduser === null || $iduser === '') {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT *
|
||||||
|
FROM cad_area_jobs
|
||||||
|
WHERE id = ?
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT *
|
||||||
|
FROM cad_area_jobs
|
||||||
|
WHERE id = ?
|
||||||
|
AND iduser = ?
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([$id, $iduser]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$job = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$job) {
|
||||||
|
$results[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Record non trovato.'
|
||||||
|
];
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasValidRoi($job)) {
|
||||||
|
$message = 'Prima devi definire la sezione da misurare tramite il pulsante Sezione.';
|
||||||
|
|
||||||
|
updateJobError($pdo, $id, $message, [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $message,
|
||||||
|
'job_roi_debug' => [
|
||||||
|
'roi_x' => $job['roi_x'] ?? null,
|
||||||
|
'roi_y' => $job['roi_y'] ?? null,
|
||||||
|
'roi_width' => $job['roi_width'] ?? null,
|
||||||
|
'roi_height' => $job['roi_height'] ?? null,
|
||||||
|
'roi_page' => $job['roi_page'] ?? null,
|
||||||
|
'calculation_mode' => $job['calculation_mode'] ?? null
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'success' => false,
|
||||||
|
'message' => $message
|
||||||
|
];
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateJobProcessing($pdo, $id);
|
||||||
|
|
||||||
|
$pythonResponse = callPythonAreaService($pythonServiceUrl, $job);
|
||||||
|
|
||||||
|
if (!($pythonResponse['success'] ?? false)) {
|
||||||
|
$message = $pythonResponse['message'] ?? 'Errore durante il calcolo Python.';
|
||||||
|
|
||||||
|
updateJobError($pdo, $id, $message, $pythonResponse);
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'success' => false,
|
||||||
|
'message' => $message,
|
||||||
|
'python_response' => $pythonResponse
|
||||||
|
];
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateJobCompleted($pdo, $id, $pythonResponse);
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'success' => true,
|
||||||
|
'message' => $pythonResponse['message'] ?? 'Area calcolata.',
|
||||||
|
'area_mm2' => $pythonResponse['area_mm2'] ?? null,
|
||||||
|
'area_cm2' => $pythonResponse['area_cm2'] ?? null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'results' => $results
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('CAD area process error: ' . $e->getMessage());
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
<?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;
|
||||||
|
$dxfContent = isset($input['dxf_content']) ? (string)$input['dxf_content'] : '';
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
$dxfFilename = null;
|
||||||
|
$dxfUrl = null;
|
||||||
|
|
||||||
|
if ($dxfContent !== '') {
|
||||||
|
|
||||||
|
if (strlen($dxfContent) > 5 * 1024 * 1024) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'DXF troppo grande.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strpos($dxfContent, 'SECTION') === false || strpos($dxfContent, 'EOF') === false) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Contenuto DXF non valido.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dxfDir = __DIR__ . '/uploads/cad_area/dxf/';
|
||||||
|
|
||||||
|
if (!is_dir($dxfDir)) {
|
||||||
|
mkdir($dxfDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dxfFilename = 'profilo_' . $id . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.dxf';
|
||||||
|
|
||||||
|
if (file_put_contents($dxfDir . $dxfFilename, $dxfContent) === false) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Impossibile salvare il file DXF sul server.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dxfUrl = 'uploads/cad_area/dxf/' . $dxfFilename;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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',
|
||||||
|
|
||||||
|
dxf_filename = COALESCE(?, dxf_filename),
|
||||||
|
dxf_url = COALESCE(?, dxf_url),
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
||||||
|
$dxfFilename,
|
||||||
|
$dxfUrl,
|
||||||
|
|
||||||
|
$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',
|
||||||
|
|
||||||
|
dxf_filename = COALESCE(?, dxf_filename),
|
||||||
|
dxf_url = COALESCE(?, dxf_url),
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
||||||
|
$dxfFilename,
|
||||||
|
$dxfUrl,
|
||||||
|
|
||||||
|
$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,
|
||||||
|
'dxf_url' => $dxfUrl
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('CAD manual area save error: ' . $e->getMessage());
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/include/headscript.php');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$iduser = $iduserlogin ?? null;
|
||||||
|
|
||||||
|
$rawInput = file_get_contents('php://input');
|
||||||
|
$input = json_decode($rawInput, true);
|
||||||
|
|
||||||
|
if (!is_array($input)) {
|
||||||
|
throw new Exception('Payload JSON non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (int)($input['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
throw new Exception('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$roiX = isset($input['roi_x']) ? (float)$input['roi_x'] : null;
|
||||||
|
$roiY = isset($input['roi_y']) ? (float)$input['roi_y'] : null;
|
||||||
|
$roiW = isset($input['roi_width']) ? (float)$input['roi_width'] : null;
|
||||||
|
$roiH = isset($input['roi_height']) ? (float)$input['roi_height'] : null;
|
||||||
|
$roiPage = isset($input['roi_page']) ? (int)$input['roi_page'] : 1;
|
||||||
|
$mode = $input['calculation_mode'] ?? 'auto_roi';
|
||||||
|
|
||||||
|
if ($roiX === null || $roiY === null || $roiW === null || $roiH === null) {
|
||||||
|
throw new Exception('ROI non valida.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($roiW <= 0 || $roiH <= 0) {
|
||||||
|
throw new Exception('Dimensioni ROI non valide.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($roiX < 0 || $roiY < 0 || $roiX > 1 || $roiY > 1 || $roiW > 1 || $roiH > 1) {
|
||||||
|
throw new Exception('Coordinate ROI fuori scala.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedModes = [
|
||||||
|
'auto_roi',
|
||||||
|
'stitch_contour',
|
||||||
|
'filled_union',
|
||||||
|
'closed_path'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!in_array($mode, $allowedModes, true)) {
|
||||||
|
$mode = 'auto_roi';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($iduser === null || $iduser === '') {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE cad_area_jobs
|
||||||
|
SET
|
||||||
|
roi_x = ?,
|
||||||
|
roi_y = ?,
|
||||||
|
roi_width = ?,
|
||||||
|
roi_height = ?,
|
||||||
|
roi_page = ?,
|
||||||
|
calculation_mode = ?,
|
||||||
|
status = 'uploaded',
|
||||||
|
message = NULL
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$roiX,
|
||||||
|
$roiY,
|
||||||
|
$roiW,
|
||||||
|
$roiH,
|
||||||
|
$roiPage,
|
||||||
|
$mode,
|
||||||
|
$id
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE cad_area_jobs
|
||||||
|
SET
|
||||||
|
roi_x = ?,
|
||||||
|
roi_y = ?,
|
||||||
|
roi_width = ?,
|
||||||
|
roi_height = ?,
|
||||||
|
roi_page = ?,
|
||||||
|
calculation_mode = ?,
|
||||||
|
status = 'uploaded',
|
||||||
|
message = NULL
|
||||||
|
WHERE id = ?
|
||||||
|
AND iduser = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$roiX,
|
||||||
|
$roiY,
|
||||||
|
$roiW,
|
||||||
|
$roiH,
|
||||||
|
$roiPage,
|
||||||
|
$mode,
|
||||||
|
$id,
|
||||||
|
$iduser
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stmt->rowCount() === 0) {
|
||||||
|
throw new Exception('Nessun record aggiornato. Controlla ID o utente.');
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'ROI salvata correttamente.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('CAD area save ROI error: ' . $e->getMessage());
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/include/headscript.php');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$iduser = $iduserlogin ?? null;
|
||||||
|
|
||||||
|
$uploadDir = __DIR__ . '/uploads/cad_area/originals/';
|
||||||
|
$publicBaseUrl = 'uploads/cad_area/originals/';
|
||||||
|
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
mkdir($uploadDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_FILES['pdf_files'])) {
|
||||||
|
throw new Exception('Nessun file ricevuto.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $_FILES['pdf_files'];
|
||||||
|
$insertedIds = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($files['name']); $i++) {
|
||||||
|
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalName = $files['name'][$i];
|
||||||
|
$tmpName = $files['tmp_name'][$i];
|
||||||
|
$size = (int)$files['size'][$i];
|
||||||
|
|
||||||
|
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
if ($extension !== 'pdf') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($size > 25 * 1024 * 1024) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeBaseName = preg_replace('/[^a-zA-Z0-9_\-]/', '_', pathinfo($originalName, PATHINFO_FILENAME));
|
||||||
|
$storedName = date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '_' . $safeBaseName . '.pdf';
|
||||||
|
|
||||||
|
$targetPath = $uploadDir . $storedName;
|
||||||
|
|
||||||
|
if (!move_uploaded_file($tmpName, $targetPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relativeUrl = $publicBaseUrl . $storedName;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO cad_area_jobs
|
||||||
|
(
|
||||||
|
iduser,
|
||||||
|
original_filename,
|
||||||
|
stored_filename,
|
||||||
|
file_path,
|
||||||
|
file_url,
|
||||||
|
file_size,
|
||||||
|
status
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
:iduser,
|
||||||
|
:original_filename,
|
||||||
|
:stored_filename,
|
||||||
|
:file_path,
|
||||||
|
:file_url,
|
||||||
|
:file_size,
|
||||||
|
'uploaded'
|
||||||
|
)
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':iduser' => $iduser,
|
||||||
|
':original_filename' => $originalName,
|
||||||
|
':stored_filename' => $storedName,
|
||||||
|
':file_path' => $targetPath,
|
||||||
|
':file_url' => $relativeUrl,
|
||||||
|
':file_size' => $size
|
||||||
|
]);
|
||||||
|
|
||||||
|
$insertedIds[] = (int)$pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($insertedIds)) {
|
||||||
|
throw new Exception('Nessun PDF valido caricato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'ids' => $insertedIds
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('CAD area upload error: ' . $e->getMessage());
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,617 @@
|
|||||||
|
<?php
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
function jsonResponse(array $data): void
|
||||||
|
{
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode($data);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeNullableInt($value): ?int
|
||||||
|
{
|
||||||
|
return (isset($value) && $value !== '') ? (int)$value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBoolValue($value): int
|
||||||
|
{
|
||||||
|
return ((string)$value === '0') ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanString(?string $value): string
|
||||||
|
{
|
||||||
|
return trim((string)$value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
AJAX HANDLERS
|
||||||
|
========================================== */
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['ajax'] == '1') {
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($action === 'add') {
|
||||||
|
$functionName = cleanString($_POST['function_name'] ?? '');
|
||||||
|
$personFullName = cleanString($_POST['person_full_name'] ?? '');
|
||||||
|
$phone = cleanString($_POST['phone'] ?? '');
|
||||||
|
$email = cleanString($_POST['email'] ?? '');
|
||||||
|
$notes = cleanString($_POST['notes'] ?? '');
|
||||||
|
$sortOrder = normalizeNullableInt($_POST['sort_order'] ?? '0') ?? 0;
|
||||||
|
$isActive = normalizeBoolValue($_POST['is_active'] ?? '1');
|
||||||
|
|
||||||
|
if ($functionName === '') {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Il nome funzione è obbligatorio.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Email non valida.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("\n INSERT INTO company_functions\n (function_name, person_full_name, phone, email, notes, sort_order, is_active, created_at, updated_at)\n VALUES\n (:function_name, :person_full_name, :phone, :email, :notes, :sort_order, :is_active, NOW(), NOW())\n ");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'function_name' => $functionName,
|
||||||
|
'person_full_name' => $personFullName !== '' ? $personFullName : '',
|
||||||
|
'phone' => $phone !== '' ? $phone : null,
|
||||||
|
'email' => $email !== '' ? $email : null,
|
||||||
|
'notes' => $notes !== '' ? $notes : null,
|
||||||
|
'sort_order' => $sortOrder,
|
||||||
|
'is_active' => $isActive,
|
||||||
|
]);
|
||||||
|
|
||||||
|
jsonResponse(['success' => true, 'message' => 'Funzione salvata correttamente.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'edit') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$functionName = cleanString($_POST['function_name'] ?? '');
|
||||||
|
$personFullName = cleanString($_POST['person_full_name'] ?? '');
|
||||||
|
$phone = cleanString($_POST['phone'] ?? '');
|
||||||
|
$email = cleanString($_POST['email'] ?? '');
|
||||||
|
$notes = cleanString($_POST['notes'] ?? '');
|
||||||
|
$sortOrder = normalizeNullableInt($_POST['sort_order'] ?? '0') ?? 0;
|
||||||
|
$isActive = normalizeBoolValue($_POST['is_active'] ?? '1');
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'ID funzione non valido.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($functionName === '') {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Il nome funzione è obbligatorio.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Email non valida.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("\n UPDATE company_functions\n SET function_name = :function_name,\n person_full_name = :person_full_name,\n phone = :phone,\n email = :email,\n notes = :notes,\n sort_order = :sort_order,\n is_active = :is_active,\n updated_at = NOW()\n WHERE id = :id\n ");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'function_name' => $functionName,
|
||||||
|
'person_full_name' => $personFullName !== '' ? $personFullName : '',
|
||||||
|
'phone' => $phone !== '' ? $phone : null,
|
||||||
|
'email' => $email !== '' ? $email : null,
|
||||||
|
'notes' => $notes !== '' ? $notes : null,
|
||||||
|
'sort_order' => $sortOrder,
|
||||||
|
'is_active' => $isActive,
|
||||||
|
'id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
jsonResponse(['success' => true, 'message' => 'Funzione aggiornata correttamente.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'ID funzione non valido.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM company_functions WHERE id = :id");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
|
||||||
|
jsonResponse(['success' => true, 'message' => 'Funzione cancellata correttamente.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Azione non riconosciuta.']);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
PAGE DATA
|
||||||
|
========================================== */
|
||||||
|
$stmtFunctions = $pdo->query("\n SELECT id, function_name, person_full_name, phone, email, notes, sort_order, is_active, created_at, updated_at\n FROM company_functions\n ORDER BY is_active DESC, sort_order ASC, function_name ASC, person_full_name ASC\n");
|
||||||
|
$functions = $stmtFunctions->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="it">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||||
|
<?php include('cssinclude.php'); ?>
|
||||||
|
<title>Funzioni Aziendali - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-dashboard {
|
||||||
|
background-color: #cfe3ff !important;
|
||||||
|
color: #1f2d3d !important;
|
||||||
|
border: 1px solid #bcd4f4 !important;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 10px 18px;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-dashboard:hover {
|
||||||
|
background-color: #b9d3ff !important;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-main-action,
|
||||||
|
.btn-add {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-main-action:hover,
|
||||||
|
.btn-add:hover {
|
||||||
|
background-color: #0b5ed7;
|
||||||
|
color: #fff;
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead {
|
||||||
|
background-color: #cfe3ff;
|
||||||
|
color: #1f2d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabCompanyFunctions thead th {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-name {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-line {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-small {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
max-width: 420px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-status {
|
||||||
|
padding: 0.25rem 0.65rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-status.active {
|
||||||
|
background-color: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-status.inactive {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-dashboard,
|
||||||
|
.btn-main-action {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="wrapper" id="appWrapper">
|
||||||
|
<?php include('include/navbar.php'); ?>
|
||||||
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<div class="page-content">
|
||||||
|
<div class="card p-3">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
<h5 class="mb-0">Funzioni Aziendali</h5>
|
||||||
|
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||||
|
↩️ Torna alla Dashboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||||
|
<div>
|
||||||
|
<h6 class="fw-semibold mb-1">Elenco Funzioni</h6>
|
||||||
|
<div class="text-muted small">Gestione di RSPP, medico del lavoro, RLS e altre funzioni aziendali.</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#companyFunctionModal" onclick="openCompanyFunctionModal()">
|
||||||
|
➕ Aggiungi Funzione
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="tabCompanyFunctions" class="table table-striped align-middle text-center" style="width:100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Funzione</th>
|
||||||
|
<th>Nominativo</th>
|
||||||
|
<th>Contatti</th>
|
||||||
|
<th>Note</th>
|
||||||
|
<th>Ordine</th>
|
||||||
|
<th>Stato</th>
|
||||||
|
<th>Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($functions as $row): ?>
|
||||||
|
<?php
|
||||||
|
$id = (int)$row['id'];
|
||||||
|
$functionName = (string)($row['function_name'] ?? '');
|
||||||
|
$personFullName = (string)($row['person_full_name'] ?? '');
|
||||||
|
$phone = (string)($row['phone'] ?? '');
|
||||||
|
$email = (string)($row['email'] ?? '');
|
||||||
|
$notes = (string)($row['notes'] ?? '');
|
||||||
|
$sortOrder = (int)($row['sort_order'] ?? 0);
|
||||||
|
$isActive = (int)($row['is_active'] ?? 1);
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-start">
|
||||||
|
<div class="function-name"><?= htmlspecialchars($functionName, ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
</td>
|
||||||
|
<td class="text-start">
|
||||||
|
<?php if ($personFullName !== ''): ?>
|
||||||
|
<div class="person-name"><?= htmlspecialchars($personFullName, ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="empty-text">Da definire</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-start">
|
||||||
|
<?php if ($phone !== ''): ?>
|
||||||
|
<a class="contact-line" href="tel:<?= htmlspecialchars($phone, ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
📞 <?= htmlspecialchars($phone, ENT_QUOTES, 'UTF-8') ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($email !== ''): ?>
|
||||||
|
<a class="contact-line" href="mailto:<?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
✉️ <?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($phone === '' && $email === ''): ?>
|
||||||
|
<span class="empty-text">Nessun contatto</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-start">
|
||||||
|
<?php if ($notes !== ''): ?>
|
||||||
|
<div class="notes-small" title="<?= htmlspecialchars($notes, ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
<?= htmlspecialchars($notes, ENT_QUOTES, 'UTF-8') ?>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="empty-text">—</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><?= $sortOrder ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($isActive === 1): ?>
|
||||||
|
<span class="badge-status active">Attiva</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge-status inactive">Non attiva</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-secondary edit-function mb-1"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-function_name="<?= htmlspecialchars($functionName, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-person_full_name="<?= htmlspecialchars($personFullName, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-phone="<?= htmlspecialchars($phone, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-email="<?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-notes="<?= htmlspecialchars($notes, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-sort_order="<?= $sortOrder ?>"
|
||||||
|
data-is_active="<?= $isActive ?>">
|
||||||
|
✏️ Modifica
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-danger delete-function mb-1"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-name="<?= htmlspecialchars($functionName, ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
🗑️ Cancella
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('include/footer.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODALE ADD / EDIT FUNZIONE -->
|
||||||
|
<div class="modal fade" id="companyFunctionModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||||
|
<h5 class="modal-title" id="companyFunctionModalTitle">Aggiungi Funzione</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="companyFunctionForm">
|
||||||
|
<input type="hidden" id="functionId">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nome funzione <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="functionName" placeholder="Es. RSPP, Medico del lavoro, RLS" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nome e Cognome persona</label>
|
||||||
|
<input type="text" class="form-control" id="personFullName" placeholder="Es. Mario Rossi">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Telefono</label>
|
||||||
|
<input type="text" class="form-control" id="phone" placeholder="Es. +39 333 1234567">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Email</label>
|
||||||
|
<input type="email" class="form-control" id="email" placeholder="nome@azienda.it">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Ordine</label>
|
||||||
|
<input type="number" class="form-control" id="sortOrder" value="0" min="0" step="1">
|
||||||
|
<small class="text-muted">Serve solo per ordinare la visualizzazione.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Stato</label>
|
||||||
|
<select class="form-select" id="isActive">
|
||||||
|
<option value="1">Attiva</option>
|
||||||
|
<option value="0">Non attiva</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Note</label>
|
||||||
|
<textarea class="form-control" id="notes" rows="3" placeholder="Note interne, riferimenti, disponibilità, ecc."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="submit" class="btn btn-add">💾 Salva</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return $('<div>').text(value || '').html();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCompanyFunctionModal() {
|
||||||
|
$('#functionId').val('');
|
||||||
|
$('#functionName').val('');
|
||||||
|
$('#personFullName').val('');
|
||||||
|
$('#phone').val('');
|
||||||
|
$('#email').val('');
|
||||||
|
$('#notes').val('');
|
||||||
|
$('#sortOrder').val('0');
|
||||||
|
$('#isActive').val('1');
|
||||||
|
$('#companyFunctionModalTitle').text('Aggiungi Funzione');
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#tabCompanyFunctions').DataTable({
|
||||||
|
order: [
|
||||||
|
[5, 'asc'],
|
||||||
|
[4, 'asc'],
|
||||||
|
[0, 'asc']
|
||||||
|
],
|
||||||
|
pageLength: 25,
|
||||||
|
language: {
|
||||||
|
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
|
||||||
|
emptyTable: 'Nessuna funzione presente'
|
||||||
|
},
|
||||||
|
columnDefs: [{
|
||||||
|
targets: -1,
|
||||||
|
orderable: false,
|
||||||
|
searchable: false
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.edit-function', function() {
|
||||||
|
const btn = $(this);
|
||||||
|
|
||||||
|
$('#functionId').val(btn.data('id'));
|
||||||
|
$('#functionName').val(btn.data('function_name'));
|
||||||
|
$('#personFullName').val(btn.data('person_full_name'));
|
||||||
|
$('#phone').val(btn.data('phone'));
|
||||||
|
$('#email').val(btn.data('email'));
|
||||||
|
$('#notes').val(btn.data('notes'));
|
||||||
|
$('#sortOrder').val(btn.data('sort_order'));
|
||||||
|
$('#isActive').val(String(btn.data('is_active')));
|
||||||
|
$('#companyFunctionModalTitle').text('Modifica Funzione');
|
||||||
|
|
||||||
|
$('#companyFunctionModal').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#companyFunctionForm').on('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const id = $('#functionId').val();
|
||||||
|
const payload = new URLSearchParams();
|
||||||
|
payload.append('ajax', '1');
|
||||||
|
payload.append('action', id ? 'edit' : 'add');
|
||||||
|
payload.append('id', id);
|
||||||
|
payload.append('function_name', $('#functionName').val().trim());
|
||||||
|
payload.append('person_full_name', $('#personFullName').val().trim());
|
||||||
|
payload.append('phone', $('#phone').val().trim());
|
||||||
|
payload.append('email', $('#email').val().trim());
|
||||||
|
payload.append('notes', $('#notes').val().trim());
|
||||||
|
payload.append('sort_order', $('#sortOrder').val() || '0');
|
||||||
|
payload.append('is_active', $('#isActive').val());
|
||||||
|
|
||||||
|
fetch('', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: payload.toString()
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Salvato!',
|
||||||
|
text: data.message || 'Operazione completata.',
|
||||||
|
confirmButtonColor: '#3085d6'
|
||||||
|
}).then(() => location.reload());
|
||||||
|
} else {
|
||||||
|
Swal.fire('Errore', data.message || 'Impossibile salvare.', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
Swal.fire('Errore', 'Errore di comunicazione.', 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.delete-function', function() {
|
||||||
|
const id = $(this).data('id');
|
||||||
|
const name = $(this).data('name');
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Confermi la cancellazione?',
|
||||||
|
text: name ? ('Funzione: ' + name) : 'La funzione verrà cancellata.',
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#d33',
|
||||||
|
cancelButtonColor: '#6c757d',
|
||||||
|
confirmButtonText: 'Sì, cancella',
|
||||||
|
cancelButtonText: 'Annulla'
|
||||||
|
}).then((result) => {
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
|
const payload = new URLSearchParams();
|
||||||
|
payload.append('ajax', '1');
|
||||||
|
payload.append('action', 'delete');
|
||||||
|
payload.append('id', id);
|
||||||
|
|
||||||
|
fetch('', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: payload.toString()
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Cancellato!',
|
||||||
|
text: data.message || 'Funzione cancellata.',
|
||||||
|
confirmButtonColor: '#3085d6'
|
||||||
|
}).then(() => location.reload());
|
||||||
|
} else {
|
||||||
|
Swal.fire('Errore', data.message || 'Impossibile cancellare.', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
Swal.fire('Errore', 'Errore di comunicazione.', 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorie disponibili per i file associati alle mescole.
|
||||||
|
* Chiave = slug salvato nel DB (colonna mescole_files.categoria)
|
||||||
|
* Valore = etichetta mostrata in UI
|
||||||
|
*
|
||||||
|
* Per aggiungere una nuova categoria in futuro basta aggiungere
|
||||||
|
* una riga qui, senza toccare il database.
|
||||||
|
*/
|
||||||
|
return [
|
||||||
|
'schede_tecniche' => 'Schede Tecniche',
|
||||||
|
'certificati_analisi' => 'Certificati di Analisi',
|
||||||
|
];
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Formazione — Email reminder cron script
|
||||||
|
* Run daily: 0 7 * * * php /var/www/html/public/userarea/cron/send_training_reminders.php
|
||||||
|
*
|
||||||
|
* Sends "due_soon" emails when next_due_date is within the reminder window
|
||||||
|
* (override reminder_days > topic default > 30 days).
|
||||||
|
* Sends "expired" emails when next_due_date is in the past.
|
||||||
|
* Skips rows with next_due_date IS NULL (one-off trainings).
|
||||||
|
* Skips already-sent notifications (same training + addressee + next_due_date).
|
||||||
|
* Recipients: the employee (employees.email or auth_users.email) + every HR user
|
||||||
|
* with role Admin / Superuser / employee-hr / manager.
|
||||||
|
*
|
||||||
|
* Optional CLI flags:
|
||||||
|
* --dry-run — log only, no SMTP, no DB write
|
||||||
|
* --only-email=foo@bar — restrict to a single addressee (for testing)
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../class/db-functions.php';
|
||||||
|
require_once __DIR__ . '/../../../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
|
use PHPMailer\PHPMailer\Exception;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/../../../');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/');
|
||||||
|
|
||||||
|
/* CLI flags */
|
||||||
|
$dryRun = false;
|
||||||
|
$onlyEmail = null;
|
||||||
|
foreach (array_slice($argv ?? [], 1) as $a) {
|
||||||
|
if ($a === '--dry-run' || $a === '-n') {
|
||||||
|
$dryRun = true;
|
||||||
|
} elseif (strpos($a, '--only-email=') === 0) {
|
||||||
|
$onlyEmail = substr($a, strlen('--only-email='));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sent = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$errors = 0;
|
||||||
|
|
||||||
|
/* Candidate trainings (with optional override reminder + topic default).
|
||||||
|
Only the most recent record per (employee, topic) — older history rows skipped. */
|
||||||
|
$stmt = $pdo->query("
|
||||||
|
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
|
||||||
|
et.reminder_days, et.delivered_by,
|
||||||
|
tt.name AS topic_name, tt.default_reminder_days AS topic_default_rem,
|
||||||
|
e.first_name, e.last_name, e.employee_code,
|
||||||
|
e.email AS employee_email_direct,
|
||||||
|
au.email AS employee_email_auth
|
||||||
|
FROM employee_trainings et
|
||||||
|
JOIN training_topics tt ON tt.id = et.training_topic_id
|
||||||
|
JOIN employees e ON e.id = et.employee_id
|
||||||
|
LEFT JOIN auth_users au ON au.id = e.auth_user_id
|
||||||
|
WHERE et.next_due_date IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM employee_trainings et2
|
||||||
|
WHERE et2.employee_id = et.employee_id
|
||||||
|
AND et2.training_topic_id = et.training_topic_id
|
||||||
|
AND (et2.completed_date > et.completed_date
|
||||||
|
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
|
||||||
|
)
|
||||||
|
");
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (empty($rows)) {
|
||||||
|
echo date('Y-m-d H:i:s') . " — Nessuna formazione da notificare.\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HR addressees (one query, reused per training) */
|
||||||
|
$hrUsers = $pdo->query("
|
||||||
|
SELECT u.id, u.email, TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS name
|
||||||
|
FROM auth_users u
|
||||||
|
JOIN auth_roles r ON r.id = u.role_id
|
||||||
|
WHERE r.name IN ('Admin','Superuser','employee-hr','manager')
|
||||||
|
AND u.email IS NOT NULL AND u.email <> ''
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$checkSent = $pdo->prepare("
|
||||||
|
SELECT COUNT(*) FROM training_reminder_log
|
||||||
|
WHERE training_id = ? AND addressee_email = ? AND next_due_date = ?
|
||||||
|
");
|
||||||
|
$insertLog = $pdo->prepare("
|
||||||
|
INSERT INTO training_reminder_log
|
||||||
|
(training_id, addressee_email, next_due_date, status_at_send, sent_at)
|
||||||
|
VALUES (?, ?, ?, ?, NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$rem = $r['reminder_days'] !== null
|
||||||
|
? (int)$r['reminder_days']
|
||||||
|
: ($r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : 30);
|
||||||
|
$isOverdue = $r['next_due_date'] < $today;
|
||||||
|
$daysLeft = (int)((strtotime($r['next_due_date']) - strtotime($today)) / 86400);
|
||||||
|
|
||||||
|
if (!$isOverdue && $daysLeft > $rem) {
|
||||||
|
continue; // not yet in the reminder window
|
||||||
|
}
|
||||||
|
$type = $isOverdue ? 'expired' : 'update_to_be_scheduled';
|
||||||
|
|
||||||
|
$employeeFullName = trim($r['first_name'] . ' ' . $r['last_name']);
|
||||||
|
$employeeEmail = !empty($r['employee_email_direct'])
|
||||||
|
? $r['employee_email_direct']
|
||||||
|
: (!empty($r['employee_email_auth']) ? $r['employee_email_auth'] : null);
|
||||||
|
|
||||||
|
/* Collect addressees (employee + HR), deduplicated by lowercased email */
|
||||||
|
$recipients = [];
|
||||||
|
if ($employeeEmail) {
|
||||||
|
$key = strtolower(trim($employeeEmail));
|
||||||
|
$recipients[$key] = ['email' => $employeeEmail, 'name' => $employeeFullName, 'is_hr' => false];
|
||||||
|
}
|
||||||
|
foreach ($hrUsers as $hr) {
|
||||||
|
$key = strtolower(trim((string)$hr['email']));
|
||||||
|
if ($key === '' || isset($recipients[$key])) continue;
|
||||||
|
$recipients[$key] = ['email' => $hr['email'], 'name' => trim((string)$hr['name']), 'is_hr' => true];
|
||||||
|
}
|
||||||
|
if (empty($recipients)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($recipients as $email => $rec) {
|
||||||
|
if ($onlyEmail !== null && strcasecmp($rec['email'], $onlyEmail) !== 0) continue;
|
||||||
|
|
||||||
|
$checkSent->execute([$r['id'], $rec['email'], $r['next_due_date']]);
|
||||||
|
if ($checkSent->fetchColumn() > 0) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$mail = new PHPMailer(true);
|
||||||
|
|
||||||
|
// SMTP config from .env
|
||||||
|
$mailer = $_ENV['MAIL_MAILER'] ?? 'mail';
|
||||||
|
if ($mailer === 'smtp') {
|
||||||
|
$mail->isSMTP();
|
||||||
|
$mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost';
|
||||||
|
$mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587);
|
||||||
|
if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') {
|
||||||
|
$mail->SMTPAuth = true;
|
||||||
|
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||||
|
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
|
||||||
|
}
|
||||||
|
$enc = $_ENV['MAIL_ENCRYPTION'] ?? '';
|
||||||
|
if ($enc && $enc !== 'null') {
|
||||||
|
$mail->SMTPSecure = $enc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->CharSet = 'UTF-8';
|
||||||
|
$mail->setFrom(
|
||||||
|
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
|
||||||
|
$_ENV['MAIL_FROM_NAME'] ?? 'Formazione ZIBOGOMMA'
|
||||||
|
);
|
||||||
|
$mail->addAddress($rec['email'], $rec['name'] ?: $rec['email']);
|
||||||
|
|
||||||
|
$profileUrl = $appUrl . '/userarea/employee-profile.php?id=' . (int)$r['employee_id'] . '#tab-training';
|
||||||
|
$topicText = $r['topic_name'] . ' — ' . $employeeFullName
|
||||||
|
. (!empty($r['employee_code']) ? ' (' . $r['employee_code'] . ')' : '');
|
||||||
|
|
||||||
|
if ($isOverdue) {
|
||||||
|
$mail->Subject = '⚠️ Formazione scaduta: ' . $r['topic_name'];
|
||||||
|
$mail->Body = buildHtml(
|
||||||
|
'Formazione scaduta',
|
||||||
|
$topicText,
|
||||||
|
'Completata il <strong>' . date('d/m/Y', strtotime($r['completed_date'])) . '</strong>. '
|
||||||
|
. 'Il prossimo aggiornamento era previsto per <strong>' . date('d/m/Y', strtotime($r['next_due_date'])) . '</strong>'
|
||||||
|
. ' (scaduta da <strong>' . abs($daysLeft) . ' giorni</strong>).',
|
||||||
|
'#dc3545',
|
||||||
|
$profileUrl,
|
||||||
|
$rec['is_hr']
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$mail->Subject = '📚 Formazione in scadenza: ' . $r['topic_name'];
|
||||||
|
$daysText = $daysLeft === 0 ? 'oggi' : 'tra <strong>' . $daysLeft . ' giorni</strong>';
|
||||||
|
$mail->Body = buildHtml(
|
||||||
|
'Formazione in scadenza',
|
||||||
|
$topicText,
|
||||||
|
'Completata il <strong>' . date('d/m/Y', strtotime($r['completed_date'])) . '</strong>. '
|
||||||
|
. 'Prossimo aggiornamento previsto per <strong>' . date('d/m/Y', strtotime($r['next_due_date'])) . '</strong>'
|
||||||
|
. ' (' . $daysText . ').',
|
||||||
|
'#e8930c',
|
||||||
|
$profileUrl,
|
||||||
|
$rec['is_hr']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->isHTML(true);
|
||||||
|
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
echo date('H:i:s') . " ◌ DRY {$type} → {$rec['email']} — {$r['topic_name']}\n";
|
||||||
|
$sent++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->send();
|
||||||
|
$insertLog->execute([$r['id'], $rec['email'], $r['next_due_date'], $type]);
|
||||||
|
$sent++;
|
||||||
|
echo date('H:i:s') . " ✓ {$type} → {$rec['email']} — {$r['topic_name']}\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errors++;
|
||||||
|
echo date('H:i:s') . " ✗ Errore {$rec['email']}: {$e->getMessage()}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
NOT-PRESENT reminders — mandatory topics with no record for an employee.
|
||||||
|
Notify HR only.
|
||||||
|
De-dup by (employee_id, training_topic_id, addressee_email).
|
||||||
|
============================================================================ */
|
||||||
|
$missingStmt = $pdo->query("
|
||||||
|
SELECT e.id AS employee_id, e.first_name, e.last_name, e.employee_code,
|
||||||
|
tt.id AS topic_id, tt.name AS topic_name
|
||||||
|
FROM employees e
|
||||||
|
CROSS JOIN training_topics tt
|
||||||
|
WHERE tt.is_active = 1 AND tt.is_mandatory = 1
|
||||||
|
AND (e.status IS NULL OR e.status = 'active')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM employee_trainings et
|
||||||
|
WHERE et.employee_id = e.id AND et.training_topic_id = tt.id
|
||||||
|
)
|
||||||
|
ORDER BY e.last_name, e.first_name, tt.name
|
||||||
|
");
|
||||||
|
$missingRows = $missingStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$checkMissingSent = $pdo->prepare("
|
||||||
|
SELECT COUNT(*) FROM training_reminder_log
|
||||||
|
WHERE employee_id = ? AND training_topic_id = ? AND addressee_email = ?
|
||||||
|
AND status_at_send = 'not_present'
|
||||||
|
");
|
||||||
|
$insertMissingLog = $pdo->prepare("
|
||||||
|
INSERT INTO training_reminder_log
|
||||||
|
(training_id, employee_id, training_topic_id, addressee_email, next_due_date, status_at_send, sent_at)
|
||||||
|
VALUES (NULL, ?, ?, ?, NULL, 'not_present', NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
foreach ($missingRows as $m) {
|
||||||
|
$employeeFullName = trim($m['first_name'] . ' ' . $m['last_name']);
|
||||||
|
|
||||||
|
foreach ($hrUsers as $hr) {
|
||||||
|
$email = trim((string)$hr['email']);
|
||||||
|
if ($email === '') continue;
|
||||||
|
if ($onlyEmail !== null && strcasecmp($email, $onlyEmail) !== 0) continue;
|
||||||
|
|
||||||
|
$checkMissingSent->execute([$m['employee_id'], $m['topic_id'], $email]);
|
||||||
|
if ($checkMissingSent->fetchColumn() > 0) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$mail = new PHPMailer(true);
|
||||||
|
$mailer = $_ENV['MAIL_MAILER'] ?? 'mail';
|
||||||
|
if ($mailer === 'smtp') {
|
||||||
|
$mail->isSMTP();
|
||||||
|
$mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost';
|
||||||
|
$mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587);
|
||||||
|
if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') {
|
||||||
|
$mail->SMTPAuth = true;
|
||||||
|
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||||
|
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
|
||||||
|
}
|
||||||
|
$enc = $_ENV['MAIL_ENCRYPTION'] ?? '';
|
||||||
|
if ($enc && $enc !== 'null') {
|
||||||
|
$mail->SMTPSecure = $enc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->CharSet = 'UTF-8';
|
||||||
|
$mail->setFrom(
|
||||||
|
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
|
||||||
|
$_ENV['MAIL_FROM_NAME'] ?? 'Formazione ZIBOGOMMA'
|
||||||
|
);
|
||||||
|
$mail->addAddress($email, trim((string)$hr['name']) ?: $email);
|
||||||
|
|
||||||
|
$profileUrl = $appUrl . '/userarea/employee-profile.php?id=' . (int)$m['employee_id'] . '#tab-training';
|
||||||
|
$topicText = $m['topic_name'] . ' — ' . $employeeFullName
|
||||||
|
. (!empty($m['employee_code']) ? ' (' . $m['employee_code'] . ')' : '');
|
||||||
|
|
||||||
|
$mail->Subject = '🔔 Formazione obbligatoria non presente: ' . $m['topic_name'];
|
||||||
|
$mail->Body = buildHtml(
|
||||||
|
'Formazione obbligatoria non presente',
|
||||||
|
$topicText,
|
||||||
|
'Il dipendente <strong>' . htmlspecialchars($employeeFullName) . '</strong> non ha nessuna registrazione per il corso obbligatorio <strong>' . htmlspecialchars($m['topic_name']) . '</strong>. Programma la prima erogazione.',
|
||||||
|
'#6b7280',
|
||||||
|
$profileUrl,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
$mail->isHTML(true);
|
||||||
|
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
echo date('H:i:s') . " ◌ DRY not_present → {$email} — {$m['topic_name']} / {$employeeFullName}\n";
|
||||||
|
$sent++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->send();
|
||||||
|
$insertMissingLog->execute([$m['employee_id'], $m['topic_id'], $email]);
|
||||||
|
$sent++;
|
||||||
|
echo date('H:i:s') . " ✓ not_present → {$email} — {$m['topic_name']} / {$employeeFullName}\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errors++;
|
||||||
|
echo date('H:i:s') . " ✗ Errore {$email}: {$e->getMessage()}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n" . date('Y-m-d H:i:s') . " — Completato. Inviate: {$sent}, Saltate: {$skipped}, Errori: {$errors}\n";
|
||||||
|
|
||||||
|
// --- HTML email template ---
|
||||||
|
function buildHtml(string $title, string $topic, string $message, string $accentColor, string $url, bool $isForHr): string
|
||||||
|
{
|
||||||
|
$greeting = $isForHr
|
||||||
|
? 'Una formazione richiede attenzione.'
|
||||||
|
: 'Una delle tue formazioni richiede attenzione.';
|
||||||
|
return '
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><meta charset="UTF-8"></head>
|
||||||
|
<body style="margin:0;padding:0;background:#f4f6f9;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="padding:30px 0">
|
||||||
|
<tr><td align="center">
|
||||||
|
<table width="560" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06)">
|
||||||
|
<tr><td style="background:' . $accentColor . ';padding:20px 30px">
|
||||||
|
<h1 style="margin:0;color:#fff;font-size:18px">' . htmlspecialchars($title) . '</h1>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:30px">
|
||||||
|
<p style="margin:0 0 12px;color:#444;font-size:14px">' . htmlspecialchars($greeting) . '</p>
|
||||||
|
<h2 style="margin:0 0 15px;color:#2c3e6b;font-size:16px">' . htmlspecialchars($topic) . '</h2>
|
||||||
|
<p style="margin:0 0 20px;color:#444;font-size:14px;line-height:1.6">' . $message . '</p>
|
||||||
|
<a href="' . htmlspecialchars($url) . '" style="display:inline-block;background:#5a8fd8;color:#fff;padding:10px 24px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px">Apri profilo</a>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:15px 30px;background:#f8f9fb;border-top:1px solid #eee">
|
||||||
|
<p style="margin:0;color:#999;font-size:11px">ZIBOGOMMA — Formazione</p>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>';
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
include('include/headscript.php');
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID mescola non valido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
// Blocca la cancellazione se ci sono dati collegati: meglio disattivare
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM mescole_supplier_lots WHERE idmescola = ?");
|
||||||
|
$chk->execute([$id]);
|
||||||
|
$nLotti = (int)$chk->fetchColumn();
|
||||||
|
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM mescole_files WHERE idmescola = ?");
|
||||||
|
$chk->execute([$id]);
|
||||||
|
$nFile = (int)$chk->fetchColumn();
|
||||||
|
|
||||||
|
if ($nLotti > 0 || $nFile > 0) {
|
||||||
|
$parti = [];
|
||||||
|
if ($nLotti > 0) $parti[] = $nLotti . ' lotto/i fornitore';
|
||||||
|
if ($nFile > 0) $parti[] = $nFile . ' file';
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Impossibile eliminare: la mescola ha ' . implode(' e ', $parti)
|
||||||
|
. '. Rimuovili prima, oppure disattiva la mescola.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// Le associazioni con le linee non sono dati storici: si possono rimuovere
|
||||||
|
$pdo->prepare("DELETE FROM mescole_lines WHERE idmescola = ?")->execute([$id]);
|
||||||
|
$pdo->prepare("DELETE FROM mescole WHERE id = ?")->execute([$id]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if (isset($pdo) && $pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Errore database: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
include('include/headscript.php');
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID non valido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT idmescola, filename FROM mescole_files WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$row) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'File non trovato']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$del = $pdo->prepare("DELETE FROM mescole_files WHERE id = ?");
|
||||||
|
$del->execute([$id]);
|
||||||
|
|
||||||
|
$path = __DIR__ . '/uploads/mescole/' . $row['idmescola'] . '/' . $row['filename'];
|
||||||
|
if (is_file($path)) {
|
||||||
|
@unlink($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Errore database: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -6,40 +6,37 @@ header('Content-Type: application/json');
|
|||||||
|
|
||||||
require_once 'include/headscript.php';
|
require_once 'include/headscript.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft-deletes a tool: it is marked as decommissioned, not removed, because
|
||||||
|
* production records reference it (productiondata_tools.tool_id).
|
||||||
|
*
|
||||||
|
* production_tools is now the unified registry inv_equipment.
|
||||||
|
*
|
||||||
|
* Note: the page calls this endpoint with GET ?id=..., while the previous
|
||||||
|
* version only read $_POST['id'] and required a name — so deletion always
|
||||||
|
* failed with "Invalid ID.". Both methods are accepted now.
|
||||||
|
*/
|
||||||
try {
|
try {
|
||||||
$db = DBHandlerSelect::getInstance();
|
$db = DBHandlerSelect::getInstance();
|
||||||
$pdo = $db->getConnection();
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
$id = (int)($_POST['id'] ?? 0);
|
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||||
$name = trim($_POST['name'] ?? '');
|
|
||||||
$tool_type = trim($_POST['tool_type'] ?? '');
|
|
||||||
$description = trim($_POST['description'] ?? '');
|
|
||||||
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 1;
|
|
||||||
|
|
||||||
if ($id <= 0) {
|
if ($id <= 0) {
|
||||||
echo json_encode(['success' => false, 'message' => 'Invalid ID.']);
|
echo json_encode(['success' => false, 'message' => 'Invalid ID.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($name === '') {
|
$stmt = $pdo->prepare("SELECT id FROM inv_equipment WHERE id = ?");
|
||||||
echo json_encode(['success' => false, 'message' => 'Name is required.']);
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
|
if (!$stmt->fetchColumn()) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Strumento non trovato.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql = "UPDATE production_tools
|
$stmt = $pdo->prepare("UPDATE inv_equipment SET status = 'decommissioned' WHERE id = ?");
|
||||||
SET name = :name,
|
$stmt->execute([$id]);
|
||||||
tool_type = :tool_type,
|
|
||||||
description = :description,
|
|
||||||
is_active = :is_active
|
|
||||||
WHERE id = :id";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
'name' => $name,
|
|
||||||
'tool_type' => $tool_type ?: null,
|
|
||||||
'description' => $description ?: null,
|
|
||||||
'is_active' => $is_active,
|
|
||||||
'id' => $id
|
|
||||||
]);
|
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
|||||||
@@ -256,7 +256,6 @@ $departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
<!-- jQuery and Bootstrap -->
|
<!-- jQuery and Bootstrap -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
@@ -367,7 +366,7 @@ $departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ try {
|
|||||||
$toolType = trim($_POST['tool_type'] ?? '');
|
$toolType = trim($_POST['tool_type'] ?? '');
|
||||||
$manufacturer = trim($_POST['manufacturer'] ?? '');
|
$manufacturer = trim($_POST['manufacturer'] ?? '');
|
||||||
$description = trim($_POST['description'] ?? '');
|
$description = trim($_POST['description'] ?? '');
|
||||||
$isActive = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 1;
|
$status = (string)($_POST['status'] ?? 'active');
|
||||||
|
|
||||||
if ($id <= 0) {
|
if ($id <= 0) {
|
||||||
echo json_encode(['success' => false, 'message' => 'Invalid ID.']);
|
echo json_encode(['success' => false, 'message' => 'Invalid ID.']);
|
||||||
@@ -29,14 +29,19 @@ try {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql = "UPDATE production_tools
|
if (!in_array($status, ['active', 'out_of_service', 'decommissioned'], true)) {
|
||||||
|
$status = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
// production_tools is now inv_equipment (see manutenzioni/sql)
|
||||||
|
$sql = "UPDATE inv_equipment
|
||||||
SET name = :name,
|
SET name = :name,
|
||||||
registration_number = :registration_number,
|
registration_number = :registration_number,
|
||||||
serial_number = :serial_number,
|
serial_number = :serial_number,
|
||||||
tool_type = :tool_type,
|
tool_type = :tool_type,
|
||||||
manufacturer = :manufacturer,
|
manufacturer = :manufacturer,
|
||||||
description = :description,
|
description = :description,
|
||||||
is_active = :is_active
|
status = :status
|
||||||
WHERE id = :id";
|
WHERE id = :id";
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
@@ -47,7 +52,7 @@ try {
|
|||||||
'tool_type' => $toolType ?: null,
|
'tool_type' => $toolType ?: null,
|
||||||
'manufacturer' => $manufacturer ?: null,
|
'manufacturer' => $manufacturer ?: null,
|
||||||
'description' => $description ?: null,
|
'description' => $description ?: null,
|
||||||
'is_active' => $isActive,
|
'status' => $status,
|
||||||
'id' => $id
|
'id' => $id
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+1528
-294
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
include('include/headscript.php');
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$idmescola = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||||
|
$categoria = $_GET['categoria'] ?? null;
|
||||||
|
|
||||||
|
if ($idmescola <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID mescola non valido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$categorie = require __DIR__ . '/config/mescole_file_categories.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$sql = "SELECT id, idmescola, categoria, titolo, filename, original_filename, mime_type, filesize, uploaded_at
|
||||||
|
FROM mescole_files
|
||||||
|
WHERE idmescola = ?";
|
||||||
|
$params = [$idmescola];
|
||||||
|
|
||||||
|
if (!empty($categoria)) {
|
||||||
|
$sql .= " AND categoria = ?";
|
||||||
|
$params[] = $categoria;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " ORDER BY uploaded_at DESC";
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$r['categoria_label'] = $categorie[$r['categoria']] ?? $r['categoria'];
|
||||||
|
$r['url'] = 'uploads/mescole/' . $r['idmescola'] . '/' . $r['filename'];
|
||||||
|
$rows[] = $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'rows' => $rows, 'categorie' => $categorie]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Errore server: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
include('include/headscript.php');
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID non valido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m.nome,
|
||||||
|
m.nomeuscita,
|
||||||
|
m.is_active,
|
||||||
|
IFNULL(q.qty_totale, 0) AS qty_totale,
|
||||||
|
GROUP_CONCAT(DISTINCT pl.name SEPARATOR ', ') AS linee
|
||||||
|
FROM mescole m
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT idmescola, SUM(qty) AS qty_totale
|
||||||
|
FROM mescole_supplier_lots
|
||||||
|
GROUP BY idmescola
|
||||||
|
) q ON q.idmescola = m.id
|
||||||
|
LEFT JOIN mescole_lines ml ON m.id = ml.idmescola
|
||||||
|
LEFT JOIN production_lines pl ON ml.idlinea = pl.id
|
||||||
|
WHERE m.id = ?
|
||||||
|
GROUP BY m.id
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$row) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Mescola non trovata']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'data' => $row]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Errore database: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -40,8 +40,7 @@ $kindofrole = $user->present()->role_id;
|
|||||||
//$iduserlogin="1";
|
//$iduserlogin="1";
|
||||||
//$nameuser="Claudio";
|
//$nameuser="Claudio";
|
||||||
//$emailuser="info@claudiosironi.com";
|
//$emailuser="info@claudiosironi.com";
|
||||||
?>
|
|
||||||
<?php
|
|
||||||
if (session_status() == PHP_SESSION_NONE) {
|
if (session_status() == PHP_SESSION_NONE) {
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
@@ -54,10 +53,8 @@ $_SESSION["emailuser"] = $emailuser;
|
|||||||
$_SESSION["photouser"] = $avatar;
|
$_SESSION["photouser"] = $avatar;
|
||||||
$photouser = $_SESSION["photouser"];
|
$photouser = $_SESSION["photouser"];
|
||||||
$photousername = basename($avatar);
|
$photousername = basename($avatar);
|
||||||
?>
|
|
||||||
|
|
||||||
|
//include files
|
||||||
<?php //include files
|
|
||||||
|
|
||||||
require_once(__DIR__ . '/../../languages/en/general.php');
|
require_once(__DIR__ . '/../../languages/en/general.php');
|
||||||
|
|
||||||
|
|||||||
@@ -289,6 +289,16 @@
|
|||||||
<i class='bx bx-radio-circle'></i>Dipendenti
|
<i class='bx bx-radio-circle'></i>Dipendenti
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="job-roles.php">
|
||||||
|
<i class='bx bx-radio-circle'></i>Mansioni
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="ppe-items.php">
|
||||||
|
<i class='bx bx-radio-circle'></i>DPI
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (userCan('hr.departments.view')) : ?>
|
<?php if (userCan('hr.departments.view')) : ?>
|
||||||
@@ -299,28 +309,15 @@
|
|||||||
</li>
|
</li>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (userCan('hr.job_roles.view')) : ?>
|
|
||||||
<li>
|
|
||||||
<a href="job_roles.php">
|
|
||||||
<i class='bx bx-radio-circle'></i>Mansioni
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (userCan('hr.training_topics.view')) : ?>
|
|
||||||
<li>
|
|
||||||
<a href="training_topics.php">
|
|
||||||
<i class='bx bx-radio-circle'></i>Corsi di Formazione
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (userCan('hr.trainings.view')) : ?>
|
<?php if (userCan('hr.trainings.view')) : ?>
|
||||||
<li>
|
<li>
|
||||||
<a href="trainings.php">
|
<a href="trainings.php">
|
||||||
<i class='bx bx-radio-circle'></i>Storico Formazione
|
<i class='bx bx-radio-circle'></i>Gestione Formazione
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (userCan('hr.skills.view')) : ?>
|
<?php if (userCan('hr.skills.view')) : ?>
|
||||||
@@ -356,11 +353,91 @@
|
|||||||
<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; ?>
|
||||||
|
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$canSeeMaintenance =
|
||||||
|
userCan('maintenance.equipment.view')
|
||||||
|
|| userCan('maintenance.maintenances.view')
|
||||||
|
|| userCan('maintenance.calendar.view')
|
||||||
|
|| userCan('maintenance.categories.view');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php if ($canSeeMaintenance) : ?>
|
||||||
|
<li>
|
||||||
|
<a href="javascript:;" class="has-arrow">
|
||||||
|
<div class="parent-icon">
|
||||||
|
<i class="bx bx-wrench"></i>
|
||||||
|
</div>
|
||||||
|
<div class="menu-title">Manutenzioni</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<?php if (userCan('maintenance.equipment.view')) : ?>
|
||||||
|
<li>
|
||||||
|
<a href="manutenzioni/index.php">
|
||||||
|
<i class='bx bx-radio-circle'></i>Registro Attrezzature
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (userCan('maintenance.maintenances.view')) : ?>
|
||||||
|
<li>
|
||||||
|
<a href="manutenzioni/maintenances.php">
|
||||||
|
<i class='bx bx-radio-circle'></i>Manutenzioni
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (userCan('maintenance.calendar.view')) : ?>
|
||||||
|
<li>
|
||||||
|
<a href="manutenzioni/calendar.php">
|
||||||
|
<i class='bx bx-radio-circle'></i>Calendario
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="manutenzioni/calendar_global.php">
|
||||||
|
<i class='bx bx-radio-circle'></i>Calendario Cumulativo
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<?php if (userCan('maintenance.categories.view')) : ?>
|
||||||
|
<li>
|
||||||
|
<a href="manutenzioni/categories/index.php">
|
||||||
|
<i class='bx bx-radio-circle'></i>Categorie
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="javascript:;" class="has-arrow">
|
||||||
|
<div class="parent-icon">
|
||||||
|
<i class="bx bx-ruler"></i>
|
||||||
|
</div>
|
||||||
|
<div class="menu-title">Misure</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="cad-area.php">
|
||||||
|
<i class='bx bx-radio-circle'></i>Cad Area
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li class="menu-label">Others</li>
|
<li class="menu-label">Others</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
// Build an absolute URL to employee-profile.php so it works from any depth
|
||||||
|
// (e.g. /userarea/index.php, /userarea/scadenzario/index.php).
|
||||||
|
$__scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
|
||||||
|
$__pos = strpos($__scriptName, '/userarea/');
|
||||||
|
$__base = $__pos !== false ? substr($__scriptName, 0, $__pos) : '';
|
||||||
|
$__myProfileHref = $__base . '/userarea/employee-profile.php';
|
||||||
|
?>
|
||||||
<header>
|
<header>
|
||||||
<div class="topbar d-flex align-items-center">
|
<div class="topbar d-flex align-items-center">
|
||||||
<nav class="navbar navbar-expand gap-3">
|
<nav class="navbar navbar-expand gap-3">
|
||||||
@@ -85,6 +93,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item d-flex align-items-center" href="<?= htmlspecialchars($__myProfileHref) ?>"
|
||||||
|
onclick="event.preventDefault(); window.location.assign(this.href);">
|
||||||
|
<i class="bx bx-id-card fs-5"></i><span>Il Mio Profilo</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item d-flex align-items-center" href="user_settings.php">
|
<a class="dropdown-item d-flex align-items-center" href="user_settings.php">
|
||||||
<i class="bx bx-user fs-5"></i><span>Utente</span>
|
<i class="bx bx-user fs-5"></i><span>Utente</span>
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper per la gestione delle coperture tra corsi di formazione.
|
||||||
|
* Un corso "madre" (topic_id) assorbe uno o più corsi "coperti" (covered_topic_id).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mappa completa delle coperture: [topic_id => [covered_topic_id => inherit_expiry]]
|
||||||
|
*/
|
||||||
|
function tc_getCoverageMap(PDO $pdo): array
|
||||||
|
{
|
||||||
|
static $cache = null;
|
||||||
|
if ($cache !== null) return $cache;
|
||||||
|
|
||||||
|
$rows = $pdo->query("
|
||||||
|
SELECT topic_id, covered_topic_id, inherit_expiry
|
||||||
|
FROM training_topic_coverage
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$map = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$map[(int)$r['topic_id']][(int)$r['covered_topic_id']] = $r['inherit_expiry'];
|
||||||
|
}
|
||||||
|
return $cache = $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mappa inversa: [covered_topic_id => [topic_id, ...]]
|
||||||
|
*/
|
||||||
|
function tc_getReverseCoverageMap(PDO $pdo): array
|
||||||
|
{
|
||||||
|
static $cache = null;
|
||||||
|
if ($cache !== null) return $cache;
|
||||||
|
|
||||||
|
$map = [];
|
||||||
|
foreach (tc_getCoverageMap($pdo) as $topicId => $covered) {
|
||||||
|
foreach (array_keys($covered) as $coveredId) {
|
||||||
|
$map[$coveredId][] = $topicId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $cache = $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elenco dei corsi coperti da un dato corso, con i nomi.
|
||||||
|
* @return array[] [['id'=>int,'name'=>string,'inherit_expiry'=>string], ...]
|
||||||
|
*/
|
||||||
|
function tc_getCoveredTopics(PDO $pdo, int $topicId): array
|
||||||
|
{
|
||||||
|
$st = $pdo->prepare("
|
||||||
|
SELECT tt.id, tt.name, c.inherit_expiry
|
||||||
|
FROM training_topic_coverage c
|
||||||
|
JOIN training_topics tt ON tt.id = c.covered_topic_id
|
||||||
|
WHERE c.topic_id = :tid
|
||||||
|
ORDER BY tt.sort_order, tt.name
|
||||||
|
");
|
||||||
|
$st->execute(['tid' => $topicId]);
|
||||||
|
return $st->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcola la data di scadenza per un corso coperto.
|
||||||
|
*
|
||||||
|
* @param string $completedDate data completamento del corso madre (Y-m-d)
|
||||||
|
* @param string $inherit 'own' | 'source' | 'none'
|
||||||
|
* @param int|null $coveredFreq frequenza (mesi) del corso coperto
|
||||||
|
* @param string|null $sourceNextDue next_due_date del corso madre
|
||||||
|
*/
|
||||||
|
function tc_computeDueDate(
|
||||||
|
string $completedDate,
|
||||||
|
string $inherit,
|
||||||
|
?int $coveredFreq,
|
||||||
|
?string $sourceNextDue
|
||||||
|
): ?string {
|
||||||
|
if ($inherit === 'none') return null;
|
||||||
|
if ($inherit === 'source') return $sourceNextDue ?: null;
|
||||||
|
|
||||||
|
// 'own'
|
||||||
|
if ($coveredFreq === null || $coveredFreq <= 0) return null;
|
||||||
|
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
|
||||||
|
if (!$d) return null;
|
||||||
|
$d->modify('+' . $coveredFreq . ' months');
|
||||||
|
return $d->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dato l'elenco dei training reali di uno o più dipendenti, produce le
|
||||||
|
* righe "virtuali" derivate dalle coperture.
|
||||||
|
*
|
||||||
|
* @param array $realRows righe da employee_trainings (servono almeno:
|
||||||
|
* employee_id, training_topic_id, completed_date,
|
||||||
|
* next_due_date, e i dati anagrafici del dipendente)
|
||||||
|
* @param array $topicsById [id => ['name'=>..,'default_frequency_months'=>..,'default_reminder_days'=>..]]
|
||||||
|
* @return array righe virtuali pronte per il rendering
|
||||||
|
*/
|
||||||
|
function tc_buildVirtualRows(PDO $pdo, array $realRows, array $topicsById): array
|
||||||
|
{
|
||||||
|
$coverage = tc_getCoverageMap($pdo);
|
||||||
|
if (!$coverage) return [];
|
||||||
|
|
||||||
|
// Indice dei record diretti già esistenti: "empId:topicId"
|
||||||
|
$direct = [];
|
||||||
|
foreach ($realRows as $r) {
|
||||||
|
$direct[$r['employee_id'] . ':' . $r['training_topic_id']] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$virtual = [];
|
||||||
|
foreach ($realRows as $r) {
|
||||||
|
$srcTopicId = (int)$r['training_topic_id'];
|
||||||
|
if (empty($coverage[$srcTopicId])) continue;
|
||||||
|
|
||||||
|
foreach ($coverage[$srcTopicId] as $coveredId => $inherit) {
|
||||||
|
$key = $r['employee_id'] . ':' . $coveredId;
|
||||||
|
|
||||||
|
// Precedenza al record diretto
|
||||||
|
if (isset($direct[$key])) continue;
|
||||||
|
// Il corso coperto deve esistere ed essere attivo
|
||||||
|
if (!isset($topicsById[$coveredId])) continue;
|
||||||
|
// Evita doppioni se più corsi madre coprono lo stesso corso:
|
||||||
|
// tiene quello con completed_date più recente
|
||||||
|
if (isset($virtual[$key]) && $virtual[$key]['completed_date'] >= $r['completed_date']) continue;
|
||||||
|
|
||||||
|
$covFreq = $topicsById[$coveredId]['default_frequency_months'];
|
||||||
|
$covFreq = ($covFreq === null || $covFreq === '') ? null : (int)$covFreq;
|
||||||
|
|
||||||
|
$due = tc_computeDueDate(
|
||||||
|
(string)$r['completed_date'],
|
||||||
|
(string)$inherit,
|
||||||
|
$covFreq,
|
||||||
|
$r['next_due_date'] ?: null
|
||||||
|
);
|
||||||
|
|
||||||
|
$virtual[$key] = [
|
||||||
|
'id' => null,
|
||||||
|
'_covered' => true,
|
||||||
|
'_covered_by_id' => $srcTopicId,
|
||||||
|
'_covered_by_name' => $topicsById[$srcTopicId]['name'] ?? ('#' . $srcTopicId),
|
||||||
|
'employee_id' => $r['employee_id'],
|
||||||
|
'first_name' => $r['first_name'] ?? '',
|
||||||
|
'last_name' => $r['last_name'] ?? '',
|
||||||
|
'employee_code' => $r['employee_code'] ?? null,
|
||||||
|
'department_name' => $r['department_name'] ?? null,
|
||||||
|
'department_color' => $r['department_color'] ?? null,
|
||||||
|
'training_topic_id' => $coveredId,
|
||||||
|
'topic_name' => $topicsById[$coveredId]['name'],
|
||||||
|
'topic_default_rem' => $topicsById[$coveredId]['default_reminder_days'] ?? null,
|
||||||
|
'training_type' => $r['training_type'] ?? 'initial',
|
||||||
|
'completed_date' => $r['completed_date'],
|
||||||
|
'next_due_date' => $due,
|
||||||
|
'reminder_days' => null,
|
||||||
|
'attachments_count' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($virtual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insieme di chiavi "empId:topicId" coperte, usato per escludere
|
||||||
|
* i corsi obbligatori dai "Non presenti".
|
||||||
|
*/
|
||||||
|
function tc_coveredKeys(array $virtualRows): array
|
||||||
|
{
|
||||||
|
$keys = [];
|
||||||
|
foreach ($virtualRows as $v) {
|
||||||
|
$keys[$v['employee_id'] . ':' . $v['training_topic_id']] = true;
|
||||||
|
}
|
||||||
|
return $keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica se aggiungere topicId -> coveredId creerebbe un ciclo.
|
||||||
|
*/
|
||||||
|
function tc_wouldCreateCycle(PDO $pdo, int $topicId, int $coveredId): bool
|
||||||
|
{
|
||||||
|
if ($topicId === $coveredId) return true;
|
||||||
|
|
||||||
|
$map = [];
|
||||||
|
$rows = $pdo->query("SELECT topic_id, covered_topic_id FROM training_topic_coverage")
|
||||||
|
->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$map[(int)$r['topic_id']][] = (int)$r['covered_topic_id'];
|
||||||
|
}
|
||||||
|
$map[$topicId][] = $coveredId;
|
||||||
|
|
||||||
|
// DFS da coveredId: se raggiungo topicId c'è ciclo
|
||||||
|
$stack = [$coveredId];
|
||||||
|
$visited = [];
|
||||||
|
while ($stack) {
|
||||||
|
$cur = array_pop($stack);
|
||||||
|
if ($cur === $topicId) return true;
|
||||||
|
if (isset($visited[$cur])) continue;
|
||||||
|
$visited[$cur] = true;
|
||||||
|
foreach ($map[$cur] ?? [] as $next) $stack[] = $next;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Training reminders widget for the production dashboard.
|
||||||
|
* Visible to HR / manager / Admin / User / Superuser.
|
||||||
|
*
|
||||||
|
* Expects $pdo to be set (DBHandlerSelect connection).
|
||||||
|
*/
|
||||||
|
if (!isset($pdo)) {
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
$__trWidgetHr = isset($user)
|
||||||
|
&& ( $user->hasRole('Admin')
|
||||||
|
|| $user->hasRole('Superuser')
|
||||||
|
|| $user->hasRole('employee-hr')
|
||||||
|
|| $user->hasRole('manager'));
|
||||||
|
|
||||||
|
if (!$__trWidgetHr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only the most recent record per (employee, topic) — older history rows ignored. */
|
||||||
|
$__trRows = $pdo->query("
|
||||||
|
SELECT et.id,
|
||||||
|
et.next_due_date,
|
||||||
|
et.reminder_days,
|
||||||
|
tt.default_reminder_days
|
||||||
|
FROM employee_trainings et
|
||||||
|
JOIN training_topics tt ON tt.id = et.training_topic_id
|
||||||
|
WHERE et.next_due_date IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM employee_trainings et2
|
||||||
|
WHERE et2.employee_id = et.employee_id
|
||||||
|
AND et2.training_topic_id = et.training_topic_id
|
||||||
|
AND (et2.completed_date > et.completed_date
|
||||||
|
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
|
||||||
|
)
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$__expiredCount = 0;
|
||||||
|
$__dueSoonCount = 0;
|
||||||
|
$__today = new DateTime('today');
|
||||||
|
foreach ($__trRows as $__r) {
|
||||||
|
$__rem = $__r['reminder_days'] !== null
|
||||||
|
? (int)$__r['reminder_days']
|
||||||
|
: ($__r['default_reminder_days'] !== null ? (int)$__r['default_reminder_days'] : 30);
|
||||||
|
$__due = DateTime::createFromFormat('Y-m-d', $__r['next_due_date']);
|
||||||
|
if (!$__due) continue;
|
||||||
|
$__days = (int)$__today->diff($__due)->format('%r%a');
|
||||||
|
if ($__days < 0) { $__expiredCount++; }
|
||||||
|
elseif ($__days <= $__rem) { $__dueSoonCount++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Missing mandatory trainings (status = not_present) */
|
||||||
|
$__notPresentCount = (int)$pdo->query("
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM employees e
|
||||||
|
CROSS JOIN training_topics tt
|
||||||
|
WHERE tt.is_active = 1 AND tt.is_mandatory = 1
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM employee_trainings et
|
||||||
|
WHERE et.employee_id = e.id AND et.training_topic_id = tt.id
|
||||||
|
)
|
||||||
|
")->fetchColumn();
|
||||||
|
|
||||||
|
if ($__expiredCount === 0 && $__dueSoonCount === 0 && $__notPresentCount === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="my-deadlines-widgets">
|
||||||
|
<?php if ($__expiredCount > 0): ?>
|
||||||
|
<a class="mdw mdw-red" href="trainings.php?status=expired">
|
||||||
|
<span class="mdw-icon"><i class="fa-solid fa-graduation-cap"></i></span>
|
||||||
|
<span class="mdw-body">
|
||||||
|
<span class="mdw-count"><?= (int)$__expiredCount ?></span>
|
||||||
|
<span class="mdw-label d-block">Formazion<?= $__expiredCount === 1 ? 'e scaduta' : 'i scadute' ?></span>
|
||||||
|
</span>
|
||||||
|
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($__dueSoonCount > 0): ?>
|
||||||
|
<a class="mdw mdw-orange" href="trainings.php?status=due_soon">
|
||||||
|
<span class="mdw-icon"><i class="fa-solid fa-hourglass-half"></i></span>
|
||||||
|
<span class="mdw-body">
|
||||||
|
<span class="mdw-count"><?= (int)$__dueSoonCount ?></span>
|
||||||
|
<span class="mdw-label d-block">Formazion<?= $__dueSoonCount === 1 ? 'e da aggiornare' : 'i da aggiornare' ?></span>
|
||||||
|
</span>
|
||||||
|
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($__notPresentCount > 0): ?>
|
||||||
|
<a class="mdw mdw-gray" href="trainings.php?status=not_present">
|
||||||
|
<span class="mdw-icon"><i class="fa-solid fa-circle-question"></i></span>
|
||||||
|
<span class="mdw-body">
|
||||||
|
<span class="mdw-count"><?= (int)$__notPresentCount ?></span>
|
||||||
|
<span class="mdw-label d-block">Obbligator<?= $__notPresentCount === 1 ? 'ia non presente' : 'ie non presenti' ?></span>
|
||||||
|
</span>
|
||||||
|
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,428 @@
|
|||||||
|
<?php
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
PAGE DATA
|
||||||
|
========================================== */
|
||||||
|
$sql = "
|
||||||
|
SELECT jr.*,
|
||||||
|
(SELECT COUNT(*) FROM employees e WHERE e.job_role_id = jr.id) AS employees_count
|
||||||
|
FROM job_roles jr
|
||||||
|
ORDER BY jr.sort_order ASC, jr.name ASC
|
||||||
|
";
|
||||||
|
$jobRoles = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="it">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||||
|
<?php include('cssinclude.php'); ?>
|
||||||
|
<title>Gestione Mansioni - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body { font-size: 1.05rem; background: #f8fafc; }
|
||||||
|
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
|
||||||
|
.back-dashboard {
|
||||||
|
background-color: #cfe3ff !important; color: #1f2d3d !important;
|
||||||
|
border: 1px solid #bcd4f4 !important; border-radius: 10px;
|
||||||
|
font-weight: 600; padding: 10px 18px;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.back-dashboard:hover { background-color: #b9d3ff !important; transform: translateY(-2px); }
|
||||||
|
.btn-add { background-color: #0d6efd; color: #fff; border-radius: 8px; padding: 10px 20px; font-weight: 500; }
|
||||||
|
.btn-add:hover { background-color: #0b5ed7; transform: scale(1.02); }
|
||||||
|
.table thead { background-color: #cfe3ff; color: #1f2d3d; }
|
||||||
|
.modal-content { border-radius: 16px; }
|
||||||
|
#tabellaJobRoles thead th { text-align: center; vertical-align: middle; }
|
||||||
|
.badge-status { padding: 0.25rem 0.6rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }
|
||||||
|
.badge-status.active { background-color: #d1fae5; color: #065f46; }
|
||||||
|
.badge-status.inactive { background-color: #e5e7eb; color: #374151; }
|
||||||
|
.description-cell {
|
||||||
|
max-width: 320px; white-space: nowrap; overflow: hidden;
|
||||||
|
text-overflow: ellipsis; text-align: left;
|
||||||
|
}
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
|
||||||
|
.back-dashboard { width: 100%; }
|
||||||
|
.btn-add { width: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-card {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
.jr-card-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.jr-card-desc {
|
||||||
|
color: #475569;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.jr-card-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 14px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.jr-card-meta b { color: #1f2937; font-weight: 600; }
|
||||||
|
.jr-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.jr-card-actions .btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.jr-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="wrapper" id="appWrapper">
|
||||||
|
<?php include('include/navbar.php'); ?>
|
||||||
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<div class="page-content">
|
||||||
|
<div class="card p-3">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
<h5 class="mb-0">Gestione Mansioni</h5>
|
||||||
|
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||||
|
↩️ Torna alla Dashboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||||
|
<h6 class="fw-semibold mb-0">Elenco Mansioni / Job Roles</h6>
|
||||||
|
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addJobRoleModal">
|
||||||
|
➕ Aggiungi Mansione
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DESKTOP / TABLET ≥768px: TABLE -->
|
||||||
|
<div class="table-responsive d-none d-md-block"><!-- hide on <md -->
|
||||||
|
<table id="tabellaJobRoles" class="table table-striped align-middle text-center" style="width:100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Nome</th>
|
||||||
|
<th>Descrizione</th>
|
||||||
|
<th>Ordine</th>
|
||||||
|
<th>Stato</th>
|
||||||
|
<th>Dipendenti</th>
|
||||||
|
<th>Creato</th>
|
||||||
|
<th>Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($jobRoles as $row): ?>
|
||||||
|
<?php
|
||||||
|
$id = (int)$row['id'];
|
||||||
|
$name = $row['name'] ?? '';
|
||||||
|
$description = $row['description'] ?? '';
|
||||||
|
$sortOrder = (int)($row['sort_order'] ?? 999);
|
||||||
|
$isActive = (int)($row['is_active'] ?? 1);
|
||||||
|
$cnt = (int)($row['employees_count'] ?? 0);
|
||||||
|
$statusClass = $isActive === 1 ? 'active' : 'inactive';
|
||||||
|
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
|
||||||
|
$createdAt = !empty($row['created_at'])
|
||||||
|
? date('d/m/Y H:i', strtotime($row['created_at']))
|
||||||
|
: '-';
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $id ?></td>
|
||||||
|
<td class="fw-semibold text-start"><?= htmlspecialchars($name) ?></td>
|
||||||
|
<td class="description-cell" title="<?= htmlspecialchars($description, ENT_QUOTES) ?>">
|
||||||
|
<?= $description !== '' ? htmlspecialchars($description) : '-' ?>
|
||||||
|
</td>
|
||||||
|
<td><?= $sortOrder ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span>
|
||||||
|
</td>
|
||||||
|
<td><?= $cnt ?></td>
|
||||||
|
<td><?= $createdAt ?></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary edit-job-role"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||||
|
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
|
||||||
|
data-sort_order="<?= $sortOrder ?>"
|
||||||
|
data-is_active="<?= $isActive ?>">
|
||||||
|
✏️ Modifica
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger delete-job-role"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||||
|
data-count="<?= $cnt ?>">
|
||||||
|
🗑️ Cancella
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MOBILE <768px: CARDS -->
|
||||||
|
<div class="d-block d-md-none">
|
||||||
|
<?php if (empty($jobRoles)): ?>
|
||||||
|
<div class="jr-empty">Nessuna mansione presente</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php foreach ($jobRoles as $row): ?>
|
||||||
|
<?php
|
||||||
|
$id = (int)$row['id'];
|
||||||
|
$name = $row['name'] ?? '';
|
||||||
|
$description = $row['description'] ?? '';
|
||||||
|
$sortOrder = (int)($row['sort_order'] ?? 999);
|
||||||
|
$isActive = (int)($row['is_active'] ?? 1);
|
||||||
|
$cnt = (int)($row['employees_count'] ?? 0);
|
||||||
|
$statusClass = $isActive === 1 ? 'active' : 'inactive';
|
||||||
|
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
|
||||||
|
?>
|
||||||
|
<div class="jr-card">
|
||||||
|
<h6 class="jr-card-title"><?= htmlspecialchars($name) ?></h6>
|
||||||
|
<?php if ($description !== ''): ?>
|
||||||
|
<p class="jr-card-desc"><?= htmlspecialchars($description) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="jr-card-meta">
|
||||||
|
<span><span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span></span>
|
||||||
|
<span><b>Dipendenti:</b> <?= $cnt ?></span>
|
||||||
|
<span><b>Ordine:</b> <?= $sortOrder ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="jr-card-actions">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary edit-job-role"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||||
|
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
|
||||||
|
data-sort_order="<?= $sortOrder ?>"
|
||||||
|
data-is_active="<?= $isActive ?>">
|
||||||
|
✏️ Modifica
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger delete-job-role"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||||
|
data-count="<?= $cnt ?>">
|
||||||
|
🗑️ Cancella
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('include/footer.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ADD MODAL -->
|
||||||
|
<div class="modal fade" id="addJobRoleModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||||
|
<h5 class="modal-title">Aggiungi Mansione</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="addJobRoleForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="addName" name="name" placeholder="es. Saldatore" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Descrizione</label>
|
||||||
|
<textarea class="form-control" id="addDescription" name="description" rows="3" placeholder="Opzionale"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Ordine</label>
|
||||||
|
<input type="number" class="form-control" id="addSortOrder" name="sort_order" value="999" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Stato</label>
|
||||||
|
<select class="form-select" id="addIsActive" name="is_active">
|
||||||
|
<option value="1" selected>Attivo</option>
|
||||||
|
<option value="0">Inattivo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="submit" class="btn btn-add">💾 Salva</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- EDIT MODAL -->
|
||||||
|
<div class="modal fade" id="editJobRoleModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||||
|
<h5 class="modal-title">Modifica Mansione</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="editJobRoleForm">
|
||||||
|
<input type="hidden" id="editJobRoleId">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="editName" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Descrizione</label>
|
||||||
|
<textarea class="form-control" id="editDescription" name="description" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Ordine</label>
|
||||||
|
<input type="number" class="form-control" id="editSortOrder" name="sort_order" value="999" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Stato</label>
|
||||||
|
<select class="form-select" id="editIsActive" name="is_active">
|
||||||
|
<option value="1">Attivo</option>
|
||||||
|
<option value="0">Inattivo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="submit" class="btn btn-add">💾 Salva Modifiche</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#tabellaJobRoles').DataTable({
|
||||||
|
order: [[3, 'asc'], [1, 'asc']],
|
||||||
|
pageLength: 25,
|
||||||
|
language: {
|
||||||
|
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
|
||||||
|
emptyTable: 'Nessuna mansione presente'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function ajaxPost(url, payload, successTitle, errorFallback) {
|
||||||
|
return fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body: payload.toString()
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
Swal.fire({ icon: "success", title: successTitle, confirmButtonColor: "#3085d6" })
|
||||||
|
.then(() => location.reload());
|
||||||
|
} else {
|
||||||
|
Swal.fire({ icon: "error", title: "Errore", text: data.message || errorFallback });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
Swal.fire({ icon: "error", title: "Errore", text: "Errore di comunicazione." });
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#addJobRoleForm").on("submit", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const p = new URLSearchParams();
|
||||||
|
p.append('name', $("#addName").val().trim());
|
||||||
|
p.append('description', $("#addDescription").val().trim());
|
||||||
|
p.append('sort_order', $("#addSortOrder").val());
|
||||||
|
p.append('is_active', $("#addIsActive").val());
|
||||||
|
ajaxPost("ajax/job_roles/save.php", p, "Salvato!", "Impossibile salvare la mansione.");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on("click", ".edit-job-role", function() {
|
||||||
|
const b = $(this);
|
||||||
|
$("#editJobRoleId").val(b.data("id"));
|
||||||
|
$("#editName").val(b.data("name"));
|
||||||
|
$("#editDescription").val(b.data("description"));
|
||||||
|
$("#editSortOrder").val(b.data("sort_order"));
|
||||||
|
$("#editIsActive").val(String(b.data("is_active")));
|
||||||
|
$("#editJobRoleModal").modal("show");
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#editJobRoleForm").on("submit", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const p = new URLSearchParams();
|
||||||
|
p.append('id', $("#editJobRoleId").val());
|
||||||
|
p.append('name', $("#editName").val().trim());
|
||||||
|
p.append('description', $("#editDescription").val().trim());
|
||||||
|
p.append('sort_order', $("#editSortOrder").val());
|
||||||
|
p.append('is_active', $("#editIsActive").val());
|
||||||
|
ajaxPost("ajax/job_roles/save.php", p, "Aggiornato!", "Impossibile aggiornare la mansione.");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on("click", ".delete-job-role", function() {
|
||||||
|
const id = $(this).data("id");
|
||||||
|
const name = $(this).data("name");
|
||||||
|
const cnt = parseInt($(this).data("count")) || 0;
|
||||||
|
|
||||||
|
if (cnt > 0) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: "warning",
|
||||||
|
title: "Impossibile cancellare",
|
||||||
|
text: "La mansione \"" + name + "\" è assegnata a " + cnt + " dipendente/i. Rimuovi prima l'associazione."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "Confermi la cancellazione?",
|
||||||
|
text: name ? ("Mansione: " + name) : "La mansione verrà cancellata.",
|
||||||
|
icon: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: "#d33",
|
||||||
|
cancelButtonColor: "#6c757d",
|
||||||
|
confirmButtonText: "Sì, cancella",
|
||||||
|
cancelButtonText: "Annulla"
|
||||||
|
}).then((result) => {
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
const p = new URLSearchParams();
|
||||||
|
p.append('id', id);
|
||||||
|
ajaxPost("ajax/job_roles/delete.php", p, "Cancellato!", "Impossibile cancellare la mansione.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -42,7 +42,6 @@ $params = $stmtParams->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
<!-- jQuery / Bootstrap / SweetAlert -->
|
<!-- jQuery / Bootstrap / SweetAlert -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
<!-- jQuery e Bootstrap -->
|
<!-- jQuery e Bootstrap -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
@@ -118,7 +117,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ function h($v)
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
@@ -496,14 +496,41 @@ function h($v)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Select2 for category filter + modal selects
|
// Select2 solo per il filtro categoria (fuori dai modali)
|
||||||
$('#categoryFilter, #add_category, #edit_category').select2({
|
$('#categoryFilter').select2({
|
||||||
theme: 'bootstrap4',
|
theme: 'bootstrap4',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
placeholder: '-- Seleziona --',
|
placeholder: '-- Seleziona --',
|
||||||
allowClear: true
|
allowClear: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Select2 per il modale ADD: inizializza quando il modale è visibile
|
||||||
|
// e aggancia il dropdown dentro il modale (altrimenti le opzioni non si vedono)
|
||||||
|
$('#addModal').on('shown.bs.modal', function() {
|
||||||
|
if (!$('#add_category').data('select2')) {
|
||||||
|
$('#add_category').select2({
|
||||||
|
theme: 'bootstrap4',
|
||||||
|
width: '100%',
|
||||||
|
placeholder: '-- Seleziona --',
|
||||||
|
allowClear: true,
|
||||||
|
dropdownParent: $('#addModal')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select2 per il modale EDIT: stessa logica
|
||||||
|
$('#editModal').on('shown.bs.modal', function() {
|
||||||
|
if (!$('#edit_category').data('select2')) {
|
||||||
|
$('#edit_category').select2({
|
||||||
|
theme: 'bootstrap4',
|
||||||
|
width: '100%',
|
||||||
|
placeholder: '-- Seleziona --',
|
||||||
|
allowClear: true,
|
||||||
|
dropdownParent: $('#editModal')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Filter redirect
|
// Filter redirect
|
||||||
$('#categoryFilter').on('change', function() {
|
$('#categoryFilter').on('change', function() {
|
||||||
const v = $(this).val() || '';
|
const v = $(this).val() || '';
|
||||||
|
|||||||
@@ -365,18 +365,13 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load packaging items from dedicated table
|
// Load ALL packaging items (tutte le categorie, distinct via GROUP nel dropdown)
|
||||||
$packagingLookup = [
|
$packagingByCategory = []; // categoria => [ {value, label}, ... ]
|
||||||
'PACKAGING_TYPE' => [],
|
|
||||||
'BOX' => [],
|
|
||||||
'PALLET' => []
|
|
||||||
];
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT category, item_name, item_code
|
SELECT id, category, item_name, item_code
|
||||||
FROM packaging_items
|
FROM packaging_items
|
||||||
WHERE is_active = 1
|
WHERE is_active = 1
|
||||||
AND category IN ('PACKAGING_TYPE', 'BOX', 'PALLET')
|
|
||||||
ORDER BY category ASC, item_name ASC
|
ORDER BY category ASC, item_name ASC
|
||||||
");
|
");
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
@@ -387,12 +382,20 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
|||||||
$label .= ' — ' . $r['item_code'];
|
$label .= ' — ' . $r['item_code'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$packagingLookup[$r['category']][] = [
|
$packagingByCategory[$r['category']][] = [
|
||||||
'value' => $r['item_code'],
|
'value' => (int)$r['id'], // salviamo l'ID
|
||||||
'label' => $label
|
'label' => $label
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Elenco categorie distinte (per il primo dropdown)
|
||||||
|
$packagingCategories = array_keys($packagingByCategory);
|
||||||
|
sort($packagingCategories);
|
||||||
|
|
||||||
|
// Questi verranno calcolati DOPO il caricamento del worksheet
|
||||||
|
$savedPackageCode = '';
|
||||||
|
$savedPackageCategory = '';
|
||||||
|
|
||||||
// Load mescole dropdown for modal
|
// Load mescole dropdown for modal
|
||||||
$mescole = $pdo->query("
|
$mescole = $pdo->query("
|
||||||
SELECT id, nome, nomeuscita
|
SELECT id, nome, nomeuscita
|
||||||
@@ -432,6 +435,19 @@ if ($worksheet_id > 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ora che $worksheet è caricato, ricava categoria + codice salvati per l'imballo
|
||||||
|
$savedPackageCode = $worksheet['requested_package_code'] ?? '';
|
||||||
|
if ($savedPackageCode !== '' && $savedPackageCode !== null) {
|
||||||
|
foreach ($packagingByCategory as $cat => $items) {
|
||||||
|
foreach ($items as $it) {
|
||||||
|
if ((string)$it['value'] === (string)$savedPackageCode) {
|
||||||
|
$savedPackageCategory = $cat;
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
function h($v)
|
function h($v)
|
||||||
{
|
{
|
||||||
@@ -874,7 +890,7 @@ $isEdit = ($worksheet_id > 0);
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
@@ -1045,14 +1061,28 @@ $isEdit = ($worksheet_id > 0);
|
|||||||
<div class="soft-badge">Packaging</div>
|
<div class="soft-badge">Packaging</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-body">
|
<div class="section-body">
|
||||||
<div class="section-grid-note">Compila prima la confezione, poi eventuali dettagli di scatola e bancale</div>
|
<div class="section-grid-note">Seleziona prima la categoria, poi lo specifico elemento di imballo</div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<label class="form-label">Codice confezione</label>
|
<label class="form-label">Categoria imballo</label>
|
||||||
<select class="form-select" name="requested_package_code" id="requested_package_code">
|
<select class="form-select" id="packaging_category">
|
||||||
<?= packaging_options($packagingLookup['PACKAGING_TYPE'] ?? [], $worksheet['requested_package_code'] ?? '') ?>
|
<option value="">-- Seleziona --</option>
|
||||||
|
<?php foreach ($packagingCategories as $cat): ?>
|
||||||
|
<option value="<?= h($cat) ?>" <?= $savedPackageCategory === $cat ? 'selected' : '' ?>>
|
||||||
|
<?= h($cat) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
|
<div class="input-tip">Es. PACKAGING_TYPE, BOX, PALLET…</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="form-label">Elemento imballo</label>
|
||||||
|
<select class="form-select" name="requested_package_code" id="requested_package_code">
|
||||||
|
<option value="">-- Prima scegli una categoria --</option>
|
||||||
|
</select>
|
||||||
|
<div class="input-tip">Il valore salvato è il codice dell'elemento</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-4 col-md-6">
|
<div class="col-lg-4 col-md-6">
|
||||||
@@ -1074,13 +1104,6 @@ $isEdit = ($worksheet_id > 0);
|
|||||||
value="<?= $worksheet ? h($worksheet['meters_per_package_notes']) : '' ?>">
|
value="<?= $worksheet ? h($worksheet['meters_per_package_notes']) : '' ?>">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-3 col-md-6">
|
|
||||||
<label class="form-label">Scatola (tipo)</label>
|
|
||||||
<select class="form-select" name="box_type" id="box_type">
|
|
||||||
<?= packaging_options($packagingLookup['BOX'] ?? [], $worksheet['box_type'] ?? '') ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-3 col-md-6">
|
<div class="col-lg-3 col-md-6">
|
||||||
<label class="form-label">N° conf./pezzi per scatola</label>
|
<label class="form-label">N° conf./pezzi per scatola</label>
|
||||||
<input type="number" class="form-control" name="packages_or_pieces_per_box"
|
<input type="number" class="form-control" name="packages_or_pieces_per_box"
|
||||||
@@ -1093,13 +1116,6 @@ $isEdit = ($worksheet_id > 0);
|
|||||||
value="<?= $worksheet ? h($worksheet['meters_per_box']) : '' ?>">
|
value="<?= $worksheet ? h($worksheet['meters_per_box']) : '' ?>">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-3 col-md-6">
|
|
||||||
<label class="form-label">Bancale (tipo)</label>
|
|
||||||
<select class="form-select" name="pallet_type" id="pallet_type">
|
|
||||||
<?= packaging_options($packagingLookup['PALLET'] ?? [], $worksheet['pallet_type'] ?? '') ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-3 col-md-6">
|
<div class="col-lg-3 col-md-6">
|
||||||
<label class="form-label">Scatole/conf. per bancale</label>
|
<label class="form-label">Scatole/conf. per bancale</label>
|
||||||
<input type="number" class="form-control" name="boxes_or_packages_per_pallet"
|
<input type="number" class="form-control" name="boxes_or_packages_per_pallet"
|
||||||
@@ -1338,6 +1354,39 @@ $isEdit = ($worksheet_id > 0);
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Imballo: categoria -> elemento filtrato ----
|
||||||
|
const packagingData = <?= json_encode($packagingByCategory, JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
const savedPackageCode = <?= json_encode($savedPackageCode, JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
|
||||||
|
function fillPackagingItems(category, selectedValue) {
|
||||||
|
const $items = $('#requested_package_code');
|
||||||
|
$items.empty();
|
||||||
|
|
||||||
|
if (!category || !packagingData[category]) {
|
||||||
|
$items.append('<option value="">-- Prima scegli una categoria --</option>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items.append('<option value="">-- Seleziona --</option>');
|
||||||
|
packagingData[category].forEach(function(it) {
|
||||||
|
const sel = (String(it.value) === String(selectedValue)) ? ' selected' : '';
|
||||||
|
$items.append('<option value="' + it.value + '"' + sel + '>' + it.label + '</option>');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Al cambio categoria, ricarica gli elementi (svuota selezione)
|
||||||
|
$('#packaging_category').on('change', function() {
|
||||||
|
fillPackagingItems($(this).val(), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Popola all'avvio se in modifica c'è già una categoria selezionata
|
||||||
|
// Usiamo il valore direttamente dal PHP, non dal .val() del select
|
||||||
|
const initialCat = <?= json_encode($savedPackageCategory, JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
if (initialCat) {
|
||||||
|
$('#packaging_category').val(initialCat);
|
||||||
|
fillPackagingItems(initialCat, savedPackageCode);
|
||||||
|
}
|
||||||
|
|
||||||
$('#mixModal').on('shown.bs.modal', function() {
|
$('#mixModal').on('shown.bs.modal', function() {
|
||||||
if (!$('#idmescola').data('select2')) {
|
if (!$('#idmescola').data('select2')) {
|
||||||
$('#idmescola').select2({
|
$('#idmescola').select2({
|
||||||
@@ -1457,6 +1506,11 @@ $isEdit = ($worksheet_id > 0);
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Se stiamo modificando una riga esistente, non resettare il form
|
||||||
|
if (parseInt($('#mixRowId').val() || '0', 10) > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$('#mixModalTitle').text('Aggiungi Mescola');
|
$('#mixModalTitle').text('Aggiungi Mescola');
|
||||||
$('#mixForm')[0].reset();
|
$('#mixForm')[0].reset();
|
||||||
$('#mixRowId').val('0');
|
$('#mixRowId').val('0');
|
||||||
@@ -1470,8 +1524,8 @@ $isEdit = ($worksheet_id > 0);
|
|||||||
let row = $(this).attr('data-row');
|
let row = $(this).attr('data-row');
|
||||||
row = JSON.parse(row);
|
row = JSON.parse(row);
|
||||||
|
|
||||||
$('#mixModalTitle').text('Modifica Mescola');
|
|
||||||
$('#mixForm')[0].reset();
|
$('#mixForm')[0].reset();
|
||||||
|
$('#mixModalTitle').text('Modifica Mescola');
|
||||||
|
|
||||||
$('#mixRowId').val(row.id);
|
$('#mixRowId').val(row.id);
|
||||||
$('#mixWorksheetId').val(row.worksheet_id);
|
$('#mixWorksheetId').val(row.worksheet_id);
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ $rows_special = array_filter($rows, function ($r) {
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
```bash
|
||||||
|
# 1. database — nell'ordine, tutti idempotenti
|
||||||
|
cd public/userarea/manutenzioni/sql
|
||||||
|
for f in 1_rename_production_tools 2_create_tables 3_seed_and_link 4_permissions; do
|
||||||
|
mysql -u <user> -p <database> < $f.sql
|
||||||
|
done
|
||||||
|
|
||||||
|
# 2. dati reali del cliente dagli XLS (facoltativo)
|
||||||
|
mysql -u <user> -p <database> < 5_import_from_xls.sql
|
||||||
|
|
||||||
|
# 3. cartella allegati scrivibile
|
||||||
|
chmod 755 ../attachments && chown www-data:www-data ../attachments
|
||||||
|
|
||||||
|
# 4. cron giornaliero delle notifiche
|
||||||
|
echo '0 7 * * * php /var/www/html/public/userarea/manutenzioni/cron/send_notifications.php' | crontab -
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth + permission guard for the Manutenzioni ajax endpoints.
|
||||||
|
* Include at the top of every handler; it defines $currentUserId.
|
||||||
|
*
|
||||||
|
* Authorisation is enforced here, on the backend — the UI only hides
|
||||||
|
* buttons, it never decides what a request is allowed to do.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
if (empty($_SESSION['iduserlogin'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Non autorizzato. Effettua il login.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentUserId = (int)$_SESSION['iduserlogin'];
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../include/functions.php';
|
||||||
|
|
||||||
|
if (!function_exists('mnt_user_can')) {
|
||||||
|
/**
|
||||||
|
* Permission check straight from the Vanguard RBAC tables
|
||||||
|
* (the Auth facade is not bootstrapped in these endpoints).
|
||||||
|
*/
|
||||||
|
function mnt_user_can(string $permission): bool
|
||||||
|
{
|
||||||
|
global $currentUserId;
|
||||||
|
|
||||||
|
static $permissions = null;
|
||||||
|
|
||||||
|
if ($permissions === null) {
|
||||||
|
$stmt = mnt_pdo()->prepare("
|
||||||
|
SELECT p.name
|
||||||
|
FROM auth_users u
|
||||||
|
INNER JOIN auth_permission_role pr ON pr.role_id = u.role_id
|
||||||
|
INNER JOIN auth_permissions p ON p.id = pr.permission_id
|
||||||
|
WHERE u.id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$currentUserId]);
|
||||||
|
$permissions = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($permission, $permissions, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('mnt_require_permission')) {
|
||||||
|
function mnt_require_permission(string $permission): void
|
||||||
|
{
|
||||||
|
if (!mnt_user_can($permission)) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Permesso negato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('mnt_json_fail')) {
|
||||||
|
function mnt_json_fail(string $message, int $code = 200): void
|
||||||
|
{
|
||||||
|
if ($code !== 200) {
|
||||||
|
http_response_code($code);
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => false, 'message' => $message]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('mnt_json_ok')) {
|
||||||
|
function mnt_json_ok(array $payload = []): void
|
||||||
|
{
|
||||||
|
echo json_encode(['success' => true] + $payload);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.manage');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
mnt_json_fail('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The FK is ON DELETE SET NULL, so a delete would silently orphan items:
|
||||||
|
// refuse while the category is still in use.
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM inv_equipment WHERE category_id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$inUse = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
if ($inUse > 0) {
|
||||||
|
mnt_json_fail('Impossibile eliminare: la categoria è usata da ' . $inUse . ' attrezzature.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->prepare("DELETE FROM inv_categories WHERE id = ?")->execute([$id]);
|
||||||
|
|
||||||
|
mnt_json_ok();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.manage');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
mnt_json_fail('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT id, name FROM inv_equipment WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$equipment = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$equipment) {
|
||||||
|
mnt_json_fail('Attrezzatura non trovata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy production records reference tools without ON DELETE CASCADE:
|
||||||
|
// refuse with a clear message instead of letting the FK throw.
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM productiondata_tools WHERE tool_id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$usedInProduction = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
if ($usedInProduction > 0) {
|
||||||
|
mnt_json_fail(
|
||||||
|
'Impossibile eliminare: l\'attrezzatura è usata in ' . $usedInProduction .
|
||||||
|
' registrazioni di produzione. Impostala su "Dismesso" per escluderla dalle pianificazioni.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect stored files before the cascade removes their rows
|
||||||
|
$storedNames = [];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT stored_name FROM inv_equipment_files WHERE equipment_id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT f.stored_name
|
||||||
|
FROM maint_maintenance_files f
|
||||||
|
INNER JOIN maint_maintenances m ON m.id = f.maintenance_id
|
||||||
|
WHERE m.equipment_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT f.stored_name
|
||||||
|
FROM maint_intervention_files f
|
||||||
|
INNER JOIN maint_interventions i ON i.id = f.intervention_id
|
||||||
|
WHERE i.equipment_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT signature_path FROM maint_interventions WHERE equipment_id = ? AND signature_path IS NOT NULL");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
|
||||||
|
|
||||||
|
// cover_file_id points at inv_equipment_files, which cascades from the
|
||||||
|
// equipment row: clear it first so the FK does not block the delete.
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
$pdo->prepare("UPDATE inv_equipment SET cover_file_id = NULL WHERE id = ?")->execute([$id]);
|
||||||
|
$pdo->prepare("DELETE FROM inv_equipment WHERE id = ?")->execute([$id]);
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
foreach (array_filter($storedNames) as $storedName) {
|
||||||
|
mnt_delete_stored_file($storedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
mnt_log($pdo, 'equipment_deleted', null, null, null, $currentUserId, null, $equipment['name']);
|
||||||
|
|
||||||
|
mnt_json_ok();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if (isset($pdo) && $pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.manage');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$scope = (string)($_POST['scope'] ?? 'equipment');
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
$tables = [
|
||||||
|
'equipment' => 'inv_equipment_files',
|
||||||
|
'maintenance' => 'maint_maintenance_files',
|
||||||
|
'intervention' => 'maint_intervention_files',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isset($tables[$scope])) {
|
||||||
|
mnt_json_fail('Scope non valido.');
|
||||||
|
}
|
||||||
|
if ($id <= 0) {
|
||||||
|
mnt_json_fail('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM {$tables[$scope]} WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$file = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$file) {
|
||||||
|
mnt_json_fail('File non trovato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the cover reference first — the FK would block the delete
|
||||||
|
if ($scope === 'equipment') {
|
||||||
|
$pdo->prepare("UPDATE inv_equipment SET cover_file_id = NULL WHERE cover_file_id = ?")->execute([$id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->prepare("DELETE FROM {$tables[$scope]} WHERE id = ?")->execute([$id]);
|
||||||
|
mnt_delete_stored_file((string)$file['stored_name']);
|
||||||
|
|
||||||
|
mnt_log(
|
||||||
|
$pdo,
|
||||||
|
'file_deleted',
|
||||||
|
$scope === 'equipment' ? (int)$file['equipment_id'] : null,
|
||||||
|
$scope === 'maintenance' ? (int)$file['maintenance_id'] : null,
|
||||||
|
$scope === 'intervention' ? (int)$file['intervention_id'] : null,
|
||||||
|
$currentUserId,
|
||||||
|
null,
|
||||||
|
(string)$file['original_name']
|
||||||
|
);
|
||||||
|
|
||||||
|
mnt_json_ok();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.manage');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
mnt_json_fail('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM maint_interventions WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$intervention = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$intervention) {
|
||||||
|
mnt_json_fail('Intervento non trovato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT stored_name FROM maint_intervention_files WHERE intervention_id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$storedNames = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
if (!empty($intervention['signature_path'])) {
|
||||||
|
$storedNames[] = $intervention['signature_path'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->prepare("DELETE FROM maint_interventions WHERE id = ?")->execute([$id]);
|
||||||
|
|
||||||
|
foreach (array_filter($storedNames) as $storedName) {
|
||||||
|
mnt_delete_stored_file($storedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removing the last completed intervention must roll the due date back
|
||||||
|
mnt_recalc_maintenance($pdo, (int)$intervention['maintenance_id']);
|
||||||
|
|
||||||
|
mnt_log(
|
||||||
|
$pdo,
|
||||||
|
'intervention_deleted',
|
||||||
|
(int)$intervention['equipment_id'],
|
||||||
|
(int)$intervention['maintenance_id'],
|
||||||
|
null,
|
||||||
|
$currentUserId,
|
||||||
|
null,
|
||||||
|
'Intervento del ' . $intervention['performed_at']
|
||||||
|
);
|
||||||
|
|
||||||
|
mnt_json_ok();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.manage');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
mnt_json_fail('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT id, equipment_id, title FROM maint_maintenances WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$maintenance = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$maintenance) {
|
||||||
|
mnt_json_fail('Manutenzione non trovata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files of the maintenance and of its interventions (both cascade in DB)
|
||||||
|
$stmt = $pdo->prepare("SELECT stored_name FROM maint_maintenance_files WHERE maintenance_id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$storedNames = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT f.stored_name
|
||||||
|
FROM maint_intervention_files f
|
||||||
|
INNER JOIN maint_interventions i ON i.id = f.intervention_id
|
||||||
|
WHERE i.maintenance_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT signature_path FROM maint_interventions WHERE maintenance_id = ? AND signature_path IS NOT NULL");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
|
||||||
|
|
||||||
|
$pdo->prepare("DELETE FROM maint_maintenances WHERE id = ?")->execute([$id]);
|
||||||
|
|
||||||
|
foreach (array_filter($storedNames) as $storedName) {
|
||||||
|
mnt_delete_stored_file($storedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
mnt_log($pdo, 'maintenance_deleted', (int)$maintenance['equipment_id'], null, null, $currentUserId, null, $maintenance['title']);
|
||||||
|
|
||||||
|
mnt_json_ok();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serves an attachment. The attachments/ folder itself is closed by
|
||||||
|
* .htaccess — this is the only way to read a file, and it requires a
|
||||||
|
* logged-in session.
|
||||||
|
*
|
||||||
|
* scope = equipment | maintenance | intervention | signature
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_SESSION['iduserlogin'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
exit('Non autorizzato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../include/functions.php';
|
||||||
|
|
||||||
|
$scope = (string)($_GET['scope'] ?? 'equipment');
|
||||||
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tables = [
|
||||||
|
'equipment' => 'inv_equipment_files',
|
||||||
|
'maintenance' => 'maint_maintenance_files',
|
||||||
|
'intervention' => 'maint_intervention_files',
|
||||||
|
];
|
||||||
|
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
if ($scope === 'signature') {
|
||||||
|
$stmt = $pdo->prepare("SELECT signature_path AS stored_name, 'firma.png' AS original_name, 'image/png' AS mime_type FROM maint_interventions WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
} elseif (isset($tables[$scope])) {
|
||||||
|
$stmt = $pdo->prepare("SELECT stored_name, original_name, mime_type FROM {$tables[$scope]} WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
} else {
|
||||||
|
http_response_code(400);
|
||||||
|
exit('Scope non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$file || empty($file['stored_name'])) {
|
||||||
|
http_response_code(404);
|
||||||
|
exit('File non trovato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// basename() keeps a crafted stored_name from escaping the folder
|
||||||
|
$path = __DIR__ . '/../attachments/' . basename((string)$file['stored_name']);
|
||||||
|
|
||||||
|
if (!is_file($path)) {
|
||||||
|
http_response_code(404);
|
||||||
|
exit('File non presente sul server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$mimeType = $file['mime_type'] ?: 'application/octet-stream';
|
||||||
|
$disposition = isset($_GET['download']) ? 'attachment' : 'inline';
|
||||||
|
|
||||||
|
header('Content-Type: ' . $mimeType);
|
||||||
|
header('Content-Length: ' . filesize($path));
|
||||||
|
header('Content-Disposition: ' . $disposition . '; filename="' . rawurlencode((string)$file['original_name']) . '"');
|
||||||
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
|
||||||
|
readfile($path);
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.calendar.view');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FullCalendar feed for maintenance due dates.
|
||||||
|
* Optional filters: type=scheduled|extraordinary, category=<id>
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$start = (string)($_GET['start'] ?? date('Y-m-01', strtotime('-6 months')));
|
||||||
|
$end = (string)($_GET['end'] ?? date('Y-m-t', strtotime('+12 months')));
|
||||||
|
$type = (string)($_GET['type'] ?? '');
|
||||||
|
$categoryId = (int)($_GET['category'] ?? 0);
|
||||||
|
|
||||||
|
// Le attrezzature dismesse o fuori servizio restano fuori dalle viste di
|
||||||
|
// pianificazione: è a questo che serve lo stato (capitolato §1).
|
||||||
|
$where = [
|
||||||
|
"m.is_active = 1",
|
||||||
|
"e.status = 'active'",
|
||||||
|
"m.next_due_date IS NOT NULL",
|
||||||
|
"m.next_due_date BETWEEN ? AND ?",
|
||||||
|
];
|
||||||
|
$params = [substr($start, 0, 10), substr($end, 0, 10)];
|
||||||
|
|
||||||
|
if (array_key_exists($type, mnt_intervention_types())) {
|
||||||
|
$where[] = "m.intervention_type = ?";
|
||||||
|
$params[] = $type;
|
||||||
|
}
|
||||||
|
if ($categoryId > 0) {
|
||||||
|
$where[] = "e.category_id = ?";
|
||||||
|
$params[] = $categoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT m.id, m.title, m.code, m.next_due_date, m.alert_days, m.is_critical, m.intervention_type,
|
||||||
|
e.id AS equipment_id, e.name AS equipment_name,
|
||||||
|
c.color AS category_color
|
||||||
|
FROM maint_maintenances m
|
||||||
|
INNER JOIN inv_equipment e ON e.id = m.equipment_id
|
||||||
|
LEFT JOIN inv_categories c ON c.id = e.category_id
|
||||||
|
WHERE " . implode(' AND ', $where) . "
|
||||||
|
ORDER BY m.next_due_date ASC
|
||||||
|
");
|
||||||
|
$stmt->execute($params);
|
||||||
|
|
||||||
|
$events = [];
|
||||||
|
|
||||||
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||||
|
$state = mnt_due_state($row['next_due_date'], (int)$row['alert_days']);
|
||||||
|
|
||||||
|
// Overdue/soon override the category colour so urgency wins visually
|
||||||
|
$color = $state === 'overdue' ? '#dc3545' : ($state === 'due_soon' ? '#f0a202' : ($row['category_color'] ?: '#2f7d8f'));
|
||||||
|
|
||||||
|
$events[] = [
|
||||||
|
'id' => (int)$row['id'],
|
||||||
|
'title' => ($row['code'] ? $row['code'] . ' — ' : '') . $row['equipment_name'] . ': ' . $row['title'],
|
||||||
|
'start' => $row['next_due_date'],
|
||||||
|
'allDay' => true,
|
||||||
|
'backgroundColor' => $color,
|
||||||
|
'borderColor' => $color,
|
||||||
|
'extendedProps' => [
|
||||||
|
'equipmentId' => (int)$row['equipment_id'],
|
||||||
|
'equipmentName' => $row['equipment_name'],
|
||||||
|
'maintenanceTitle' => $row['title'],
|
||||||
|
'isCritical' => (int)$row['is_critical'] === 1,
|
||||||
|
'state' => $state,
|
||||||
|
'type' => $row['intervention_type'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($events);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.equipment.view');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
mnt_json_fail('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = mnt_pdo()->prepare("SELECT * FROM inv_equipment WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$equipment = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$equipment) {
|
||||||
|
mnt_json_fail('Attrezzatura non trovata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
mnt_json_ok(['equipment' => $equipment]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cumulative calendar feed: deadlines + trainings + maintenances.
|
||||||
|
* Each layer is returned only if the user may see that module, and only
|
||||||
|
* if the layer is requested via ?layers=deadlines,trainings,maintenances
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$start = substr((string)($_GET['start'] ?? date('Y-m-01', strtotime('-6 months'))), 0, 10);
|
||||||
|
$end = substr((string)($_GET['end'] ?? date('Y-m-t', strtotime('+12 months'))), 0, 10);
|
||||||
|
|
||||||
|
$requested = array_filter(explode(',', (string)($_GET['layers'] ?? 'deadlines,trainings,maintenances')));
|
||||||
|
$wants = fn(string $layer): bool => in_array($layer, $requested, true);
|
||||||
|
|
||||||
|
$events = [];
|
||||||
|
|
||||||
|
// ---- Maintenances --------------------------------------------------
|
||||||
|
if ($wants('maintenances') && mnt_user_can('maintenance.calendar.view')) {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT m.id, m.title, m.code, m.next_due_date, m.alert_days, m.is_critical,
|
||||||
|
e.id AS equipment_id, e.name AS equipment_name
|
||||||
|
FROM maint_maintenances m
|
||||||
|
INNER JOIN inv_equipment e ON e.id = m.equipment_id
|
||||||
|
WHERE m.is_active = 1
|
||||||
|
AND e.status = 'active'
|
||||||
|
AND m.next_due_date BETWEEN ? AND ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$start, $end]);
|
||||||
|
|
||||||
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||||
|
$state = mnt_due_state($row['next_due_date'], (int)$row['alert_days']);
|
||||||
|
$color = $state === 'overdue' ? '#dc3545' : '#2f7d8f';
|
||||||
|
|
||||||
|
$events[] = [
|
||||||
|
'id' => 'maint-' . $row['id'],
|
||||||
|
'title' => '🔧 ' . $row['equipment_name'] . ': ' . $row['title'],
|
||||||
|
'start' => $row['next_due_date'],
|
||||||
|
'allDay' => true,
|
||||||
|
'backgroundColor' => $color,
|
||||||
|
'borderColor' => $color,
|
||||||
|
'extendedProps' => [
|
||||||
|
'layer' => 'maintenances',
|
||||||
|
'url' => 'manutenzioni/equipment.php?id=' . (int)$row['equipment_id'],
|
||||||
|
'state' => $state,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Deadlines (scadenzario) --------------------------------------
|
||||||
|
if ($wants('deadlines') && mnt_user_can('deadlines.view')) {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT d.id, d.topic, d.due_date, d.status, d.notification_days, s.name AS subject_name
|
||||||
|
FROM scad_deadlines d
|
||||||
|
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
||||||
|
WHERE d.due_date BETWEEN ? AND ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$start, $end]);
|
||||||
|
|
||||||
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||||
|
$isCompleted = $row['status'] === 'completed';
|
||||||
|
$state = $isCompleted ? 'ok' : mnt_due_state($row['due_date'], (int)$row['notification_days']);
|
||||||
|
$color = $isCompleted ? '#6c757d' : ($state === 'overdue' ? '#b02a37' : '#6f42c1');
|
||||||
|
|
||||||
|
$events[] = [
|
||||||
|
'id' => 'scad-' . $row['id'],
|
||||||
|
'title' => '📅 ' . $row['topic'],
|
||||||
|
'start' => $row['due_date'],
|
||||||
|
'allDay' => true,
|
||||||
|
'backgroundColor' => $color,
|
||||||
|
'borderColor' => $color,
|
||||||
|
'extendedProps' => [
|
||||||
|
'layer' => 'deadlines',
|
||||||
|
'url' => 'scadenzario/detail.php?id=' . (int)$row['id'],
|
||||||
|
'state' => $state,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Trainings (formazione) ---------------------------------------
|
||||||
|
if ($wants('trainings') && mnt_user_can('hr.trainings.view')) {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT t.id, t.next_due_date, t.reminder_days,
|
||||||
|
tt.name AS topic_name,
|
||||||
|
CONCAT(e.first_name, ' ', e.last_name) AS employee_name
|
||||||
|
FROM employee_trainings t
|
||||||
|
INNER JOIN training_topics tt ON tt.id = t.training_topic_id
|
||||||
|
INNER JOIN employees e ON e.id = t.employee_id
|
||||||
|
WHERE t.next_due_date IS NOT NULL AND t.next_due_date BETWEEN ? AND ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$start, $end]);
|
||||||
|
|
||||||
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||||
|
$state = mnt_due_state($row['next_due_date'], (int)($row['reminder_days'] ?? 0));
|
||||||
|
$color = $state === 'overdue' ? '#a94442' : '#198754';
|
||||||
|
|
||||||
|
$events[] = [
|
||||||
|
'id' => 'train-' . $row['id'],
|
||||||
|
'title' => '🎓 ' . $row['topic_name'] . ' — ' . $row['employee_name'],
|
||||||
|
'start' => $row['next_due_date'],
|
||||||
|
'allDay' => true,
|
||||||
|
'backgroundColor' => $color,
|
||||||
|
'borderColor' => $color,
|
||||||
|
'extendedProps' => [
|
||||||
|
'layer' => 'trainings',
|
||||||
|
'url' => 'trainings.php',
|
||||||
|
'state' => $state,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($events);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.maintenances.view');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$maintenanceId = (int)($_GET['maintenance_id'] ?? 0);
|
||||||
|
$interventionId = (int)($_GET['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($interventionId > 0) {
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM maint_interventions WHERE id = ?");
|
||||||
|
$stmt->execute([$interventionId]);
|
||||||
|
$intervention = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$intervention) {
|
||||||
|
mnt_json_fail('Intervento non trovato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT id, original_name FROM maint_intervention_files WHERE intervention_id = ?");
|
||||||
|
$stmt->execute([$interventionId]);
|
||||||
|
|
||||||
|
mnt_json_ok([
|
||||||
|
'intervention' => $intervention,
|
||||||
|
'files' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($maintenanceId <= 0) {
|
||||||
|
mnt_json_fail('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT i.*,
|
||||||
|
CONCAT(e.first_name, ' ', e.last_name) AS operator_full_name,
|
||||||
|
s.supplier_name,
|
||||||
|
(SELECT COUNT(*) FROM maint_intervention_files f WHERE f.intervention_id = i.id) AS file_count
|
||||||
|
FROM maint_interventions i
|
||||||
|
LEFT JOIN employees e ON e.id = i.operator_employee_id
|
||||||
|
LEFT JOIN suppliers s ON s.idsupplier = i.supplier_id
|
||||||
|
WHERE i.maintenance_id = ?
|
||||||
|
ORDER BY i.performed_at DESC, i.id DESC
|
||||||
|
");
|
||||||
|
$stmt->execute([$maintenanceId]);
|
||||||
|
|
||||||
|
mnt_json_ok(['interventions' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.maintenances.view');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
mnt_json_fail('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT m.*, e.name AS equipment_name
|
||||||
|
FROM maint_maintenances m
|
||||||
|
INNER JOIN inv_equipment e ON e.id = m.equipment_id
|
||||||
|
WHERE m.id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$maintenance = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$maintenance) {
|
||||||
|
mnt_json_fail('Manutenzione non trovata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT id, original_name, size
|
||||||
|
FROM maint_maintenance_files
|
||||||
|
WHERE maintenance_id = ?
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
|
mnt_json_ok([
|
||||||
|
'maintenance' => $maintenance,
|
||||||
|
'files' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.manage');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$name = trim((string)($_POST['name'] ?? ''));
|
||||||
|
$code = trim((string)($_POST['code'] ?? ''));
|
||||||
|
$color = trim((string)($_POST['color'] ?? '#6c757d'));
|
||||||
|
$requiresLine = !empty($_POST['requires_line']) ? 1 : 0;
|
||||||
|
$sortOrder = (int)($_POST['sort_order'] ?? 999);
|
||||||
|
$isActive = !empty($_POST['is_active']) ? 1 : 0;
|
||||||
|
$description = trim((string)($_POST['description'] ?? ''));
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
mnt_json_fail('Il nome è obbligatorio.');
|
||||||
|
}
|
||||||
|
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
|
||||||
|
$color = '#6c757d';
|
||||||
|
}
|
||||||
|
|
||||||
|
// name is UNIQUE — check first so the user gets a readable message
|
||||||
|
$stmt = $pdo->prepare("SELECT id FROM inv_categories WHERE name = ? AND id <> ?");
|
||||||
|
$stmt->execute([$name, $id]);
|
||||||
|
if ($stmt->fetchColumn()) {
|
||||||
|
mnt_json_fail('Esiste già una categoria con questo nome.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($id > 0) {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE inv_categories
|
||||||
|
SET name = ?, code = ?, color = ?, requires_line = ?, sort_order = ?, is_active = ?, description = ?
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$name, $code ?: null, $color, $requiresLine, $sortOrder, $isActive, $description ?: null, $id]);
|
||||||
|
|
||||||
|
mnt_json_ok(['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO inv_categories (name, code, color, requires_line, sort_order, is_active, description)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
$stmt->execute([$name, $code ?: null, $color, $requiresLine, $sortOrder, $isActive, $description ?: null]);
|
||||||
|
|
||||||
|
mnt_json_ok(['id' => (int)$pdo->lastInsertId()]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.manage');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$name = trim((string)($_POST['name'] ?? ''));
|
||||||
|
$categoryId = (int)($_POST['category_id'] ?? 0);
|
||||||
|
$lineId = (int)($_POST['line_id'] ?? 0);
|
||||||
|
$registration = trim((string)($_POST['registration_number'] ?? ''));
|
||||||
|
$serial = trim((string)($_POST['serial_number'] ?? ''));
|
||||||
|
$batchLot = trim((string)($_POST['batch_lot'] ?? ''));
|
||||||
|
$manufacturer = trim((string)($_POST['manufacturer'] ?? ''));
|
||||||
|
$purchaseDate = trim((string)($_POST['purchase_date'] ?? ''));
|
||||||
|
$commissioningDate = trim((string)($_POST['commissioning_date'] ?? ''));
|
||||||
|
$toolType = trim((string)($_POST['tool_type'] ?? ''));
|
||||||
|
$status = (string)($_POST['status'] ?? 'active');
|
||||||
|
$departmentId = (int)($_POST['department_id'] ?? 0);
|
||||||
|
$location = trim((string)($_POST['location'] ?? ''));
|
||||||
|
$description = trim((string)($_POST['description'] ?? ''));
|
||||||
|
$notes = trim((string)($_POST['notes'] ?? ''));
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
mnt_json_fail('Il nome è obbligatorio.');
|
||||||
|
}
|
||||||
|
if ($categoryId <= 0) {
|
||||||
|
mnt_json_fail('La categoria è obbligatoria.');
|
||||||
|
}
|
||||||
|
if (!array_key_exists($status, mnt_equipment_statuses())) {
|
||||||
|
mnt_json_fail('Stato non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// A category flagged requires_line must carry a production line
|
||||||
|
$stmt = $pdo->prepare("SELECT requires_line FROM inv_categories WHERE id = ?");
|
||||||
|
$stmt->execute([$categoryId]);
|
||||||
|
$requiresLine = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
if ($requiresLine === false) {
|
||||||
|
mnt_json_fail('Categoria inesistente.');
|
||||||
|
}
|
||||||
|
if ((int)$requiresLine === 1 && $lineId <= 0) {
|
||||||
|
mnt_json_fail('Per questa categoria la linea di produzione è obbligatoria.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = [
|
||||||
|
'name' => $name,
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'line_id' => $lineId ?: null,
|
||||||
|
'registration_number' => $registration ?: null,
|
||||||
|
'serial_number' => $serial ?: null,
|
||||||
|
'batch_lot' => $batchLot ?: null,
|
||||||
|
'manufacturer' => $manufacturer ?: null,
|
||||||
|
'purchase_date' => $purchaseDate ?: null,
|
||||||
|
'commissioning_date' => $commissioningDate ?: null,
|
||||||
|
'tool_type' => $toolType ?: null,
|
||||||
|
'status' => $status,
|
||||||
|
'department_id' => $departmentId ?: null,
|
||||||
|
'location' => $location ?: null,
|
||||||
|
'description' => $description ?: null,
|
||||||
|
'notes' => $notes ?: null,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($id > 0) {
|
||||||
|
$assignments = [];
|
||||||
|
foreach (array_keys($values) as $column) {
|
||||||
|
$assignments[] = "$column = :$column";
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE inv_equipment SET " . implode(', ', $assignments) . " WHERE id = :id");
|
||||||
|
$stmt->execute($values + ['id' => $id]);
|
||||||
|
|
||||||
|
mnt_log($pdo, 'equipment_updated', $id, null, null, $currentUserId, $values);
|
||||||
|
mnt_json_ok(['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$columns = array_keys($values);
|
||||||
|
$placeholders = array_map(fn($column) => ':' . $column, $columns);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO inv_equipment (" . implode(', ', $columns) . ", created_by)
|
||||||
|
VALUES (" . implode(', ', $placeholders) . ", :created_by)"
|
||||||
|
);
|
||||||
|
$stmt->execute($values + ['created_by' => $currentUserId]);
|
||||||
|
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
mnt_log($pdo, 'equipment_created', $newId, null, null, $currentUserId, $values);
|
||||||
|
|
||||||
|
mnt_json_ok(['id' => $newId]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.manage');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers (or updates) one intervention against a maintenance and
|
||||||
|
* recomputes the maintenance schedule.
|
||||||
|
*
|
||||||
|
* The operator signature arrives as a data URL from the tablet canvas and
|
||||||
|
* is stored as a PNG in attachments/.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$maintenanceId = (int)($_POST['maintenance_id'] ?? 0);
|
||||||
|
$performedAt = trim((string)($_POST['performed_at'] ?? ''));
|
||||||
|
$status = (string)($_POST['status'] ?? 'completed');
|
||||||
|
$result = trim((string)($_POST['result'] ?? ''));
|
||||||
|
$notes = trim((string)($_POST['notes'] ?? ''));
|
||||||
|
$materials = trim((string)($_POST['materials'] ?? ''));
|
||||||
|
$operatorId = (int)($_POST['operator_employee_id'] ?? 0);
|
||||||
|
$operatorName = trim((string)($_POST['operator_name'] ?? ''));
|
||||||
|
$supplierId = (int)($_POST['supplier_id'] ?? 0);
|
||||||
|
$signatureData = (string)($_POST['signature'] ?? '');
|
||||||
|
$clearSignature = !empty($_POST['clear_signature']);
|
||||||
|
|
||||||
|
if ($performedAt === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $performedAt)) {
|
||||||
|
mnt_json_fail('La data dell\'intervento è obbligatoria.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validStatuses = mnt_lookup($pdo, 'maint_status', ['planned' => '', 'in_progress' => '', 'completed' => '']);
|
||||||
|
if (!array_key_exists($status, $validStatuses)) {
|
||||||
|
mnt_json_fail('Stato intervento non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validResults = mnt_lookup($pdo, 'maint_result', []);
|
||||||
|
if ($result !== '' && $validResults && !array_key_exists($result, $validResults)) {
|
||||||
|
mnt_json_fail('Esito intervento non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Existing row: maintenance is taken from the record, not from the client
|
||||||
|
if ($id > 0) {
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM maint_interventions WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$existing) {
|
||||||
|
mnt_json_fail('Intervento non trovato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$maintenanceId = (int)$existing['maintenance_id'];
|
||||||
|
$equipmentId = (int)$existing['equipment_id'];
|
||||||
|
$signaturePath = $existing['signature_path'];
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("SELECT equipment_id FROM maint_maintenances WHERE id = ?");
|
||||||
|
$stmt->execute([$maintenanceId]);
|
||||||
|
$equipmentId = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
if (!$equipmentId) {
|
||||||
|
mnt_json_fail('Manutenzione non trovata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$signaturePath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signature handling
|
||||||
|
if ($clearSignature && $signaturePath) {
|
||||||
|
mnt_delete_stored_file((string)$signaturePath);
|
||||||
|
$signaturePath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($signatureData !== '' && preg_match('#^data:image/png;base64,#', $signatureData)) {
|
||||||
|
$binary = base64_decode(substr($signatureData, strlen('data:image/png;base64,')), true);
|
||||||
|
|
||||||
|
if ($binary === false || strlen($binary) > 2 * 1024 * 1024) {
|
||||||
|
mnt_json_fail('Firma non valida.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploadDir = __DIR__ . '/../attachments/';
|
||||||
|
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true) && !is_dir($uploadDir)) {
|
||||||
|
mnt_json_fail('Cartella allegati non scrivibile.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$newSignature = uniqid('sig_', true) . '.png';
|
||||||
|
$newSignature = preg_replace('/[^A-Za-z0-9._-]/', '_', $newSignature);
|
||||||
|
|
||||||
|
if (file_put_contents($uploadDir . $newSignature, $binary) === false) {
|
||||||
|
mnt_json_fail('Impossibile salvare la firma.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($signaturePath) {
|
||||||
|
mnt_delete_stored_file((string)$signaturePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
$signaturePath = $newSignature;
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = [
|
||||||
|
'performed_at' => $performedAt,
|
||||||
|
'status' => $status,
|
||||||
|
'result' => $result ?: null,
|
||||||
|
'notes' => $notes ?: null,
|
||||||
|
'materials' => $materials ?: null,
|
||||||
|
'operator_employee_id' => $operatorId ?: null,
|
||||||
|
'operator_name' => $operatorName ?: null,
|
||||||
|
'supplier_id' => $supplierId ?: null,
|
||||||
|
'signature_path' => $signaturePath,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($id > 0) {
|
||||||
|
$assignments = [];
|
||||||
|
foreach (array_keys($values) as $column) {
|
||||||
|
$assignments[] = "$column = :$column";
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE maint_interventions SET " . implode(', ', $assignments) . " WHERE id = :id");
|
||||||
|
$stmt->execute($values + ['id' => $id]);
|
||||||
|
|
||||||
|
$interventionId = $id;
|
||||||
|
$action = 'intervention_updated';
|
||||||
|
} else {
|
||||||
|
$values['maintenance_id'] = $maintenanceId;
|
||||||
|
$values['equipment_id'] = $equipmentId;
|
||||||
|
$values['created_by'] = $currentUserId;
|
||||||
|
|
||||||
|
$columns = array_keys($values);
|
||||||
|
$placeholders = array_map(fn($column) => ':' . $column, $columns);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO maint_interventions (" . implode(', ', $columns) . ")
|
||||||
|
VALUES (" . implode(', ', $placeholders) . ")"
|
||||||
|
);
|
||||||
|
$stmt->execute($values);
|
||||||
|
|
||||||
|
$interventionId = (int)$pdo->lastInsertId();
|
||||||
|
$action = 'intervention_created';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only completed interventions move the next due date; this call handles it
|
||||||
|
mnt_recalc_maintenance($pdo, $maintenanceId);
|
||||||
|
|
||||||
|
mnt_log($pdo, $action, $equipmentId, $maintenanceId, $interventionId, $currentUserId, $values);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT last_done_date, next_due_date FROM maint_maintenances WHERE id = ?");
|
||||||
|
$stmt->execute([$maintenanceId]);
|
||||||
|
$schedule = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
|
||||||
|
mnt_json_ok(['id' => $interventionId, 'schedule' => $schedule]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.manage');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$equipmentId = (int)($_POST['equipment_id'] ?? 0);
|
||||||
|
$code = trim((string)($_POST['code'] ?? ''));
|
||||||
|
$title = trim((string)($_POST['title'] ?? ''));
|
||||||
|
$description = trim((string)($_POST['description'] ?? ''));
|
||||||
|
$interventionType = (string)($_POST['intervention_type'] ?? 'scheduled');
|
||||||
|
$executionType = (string)($_POST['execution_type'] ?? 'internal');
|
||||||
|
$isCritical = !empty($_POST['is_critical']) ? 1 : 0;
|
||||||
|
$frequencyUnit = (string)($_POST['frequency_unit'] ?? 'month');
|
||||||
|
$frequencyValue = (int)($_POST['frequency_value'] ?? 0);
|
||||||
|
$frequencyNote = trim((string)($_POST['frequency_note'] ?? ''));
|
||||||
|
$alertDays = (int)($_POST['alert_days'] ?? 7);
|
||||||
|
$assigneeId = (int)($_POST['assignee_employee_id'] ?? 0);
|
||||||
|
$supervisorId = (int)($_POST['supervisor_employee_id'] ?? 0);
|
||||||
|
$supplierId = (int)($_POST['supplier_id'] ?? 0);
|
||||||
|
$isActive = isset($_POST['is_active']) ? (int)!empty($_POST['is_active']) : 1;
|
||||||
|
$notes = trim((string)($_POST['notes'] ?? ''));
|
||||||
|
|
||||||
|
if ($title === '') {
|
||||||
|
mnt_json_fail('Il titolo è obbligatorio.');
|
||||||
|
}
|
||||||
|
if (!array_key_exists($interventionType, mnt_intervention_types())) {
|
||||||
|
mnt_json_fail('Tipo di intervento non valido.');
|
||||||
|
}
|
||||||
|
if (!array_key_exists($executionType, mnt_execution_types())) {
|
||||||
|
mnt_json_fail('Tipo di esecuzione non valido.');
|
||||||
|
}
|
||||||
|
if (!array_key_exists($frequencyUnit, mnt_frequency_units())) {
|
||||||
|
mnt_json_fail('Unità di frequenza non valida.');
|
||||||
|
}
|
||||||
|
if ($frequencyUnit !== 'on_demand' && $frequencyValue <= 0) {
|
||||||
|
mnt_json_fail('Indica ogni quanto va eseguita la manutenzione, oppure scegli "Al bisogno".');
|
||||||
|
}
|
||||||
|
if ($executionType === 'external' && $supplierId <= 0) {
|
||||||
|
mnt_json_fail('Per una manutenzione esterna indica il fornitore.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($frequencyUnit === 'on_demand') {
|
||||||
|
$frequencyValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = [
|
||||||
|
'code' => $code ?: null,
|
||||||
|
'title' => $title,
|
||||||
|
'description' => $description ?: null,
|
||||||
|
'intervention_type' => $interventionType,
|
||||||
|
'execution_type' => $executionType,
|
||||||
|
'is_critical' => $isCritical,
|
||||||
|
'frequency_value' => $frequencyValue,
|
||||||
|
'frequency_unit' => $frequencyUnit,
|
||||||
|
'frequency_note' => $frequencyNote ?: null,
|
||||||
|
'alert_days' => max(0, $alertDays),
|
||||||
|
'assignee_employee_id' => $assigneeId ?: null,
|
||||||
|
'supervisor_employee_id' => $supervisorId ?: null,
|
||||||
|
'supplier_id' => $supplierId ?: null,
|
||||||
|
'is_active' => $isActive,
|
||||||
|
'notes' => $notes ?: null,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($id > 0) {
|
||||||
|
$stmt = $pdo->prepare("SELECT equipment_id FROM maint_maintenances WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$equipmentId = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
if (!$equipmentId) {
|
||||||
|
mnt_json_fail('Manutenzione non trovata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$assignments = [];
|
||||||
|
foreach (array_keys($values) as $column) {
|
||||||
|
$assignments[] = "$column = :$column";
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE maint_maintenances SET " . implode(', ', $assignments) . " WHERE id = :id");
|
||||||
|
$stmt->execute($values + ['id' => $id]);
|
||||||
|
|
||||||
|
// frequency may have changed — the due date must follow
|
||||||
|
mnt_recalc_maintenance($pdo, $id);
|
||||||
|
mnt_log($pdo, 'maintenance_updated', $equipmentId, $id, null, $currentUserId, $values);
|
||||||
|
|
||||||
|
mnt_json_ok(['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($equipmentId <= 0) {
|
||||||
|
mnt_json_fail('Attrezzatura non valida.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT id FROM inv_equipment WHERE id = ?");
|
||||||
|
$stmt->execute([$equipmentId]);
|
||||||
|
if (!$stmt->fetchColumn()) {
|
||||||
|
mnt_json_fail('Attrezzatura non trovata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$values['equipment_id'] = $equipmentId;
|
||||||
|
$values['created_by'] = $currentUserId;
|
||||||
|
|
||||||
|
$columns = array_keys($values);
|
||||||
|
$placeholders = array_map(fn($column) => ':' . $column, $columns);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO maint_maintenances (" . implode(', ', $columns) . ")
|
||||||
|
VALUES (" . implode(', ', $placeholders) . ")"
|
||||||
|
);
|
||||||
|
$stmt->execute($values);
|
||||||
|
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
mnt_log($pdo, 'maintenance_created', $equipmentId, $newId, null, $currentUserId, $values);
|
||||||
|
|
||||||
|
mnt_json_ok(['id' => $newId]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.manage');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$fileId = (int)($_POST['file_id'] ?? 0);
|
||||||
|
|
||||||
|
if ($fileId <= 0) {
|
||||||
|
mnt_json_fail('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT equipment_id, kind FROM inv_equipment_files WHERE id = ?");
|
||||||
|
$stmt->execute([$fileId]);
|
||||||
|
$file = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$file) {
|
||||||
|
mnt_json_fail('Immagine non trovata.');
|
||||||
|
}
|
||||||
|
if ($file['kind'] !== 'photo') {
|
||||||
|
mnt_json_fail('Solo una foto può essere usata come copertina.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->prepare("UPDATE inv_equipment SET cover_file_id = ? WHERE id = ?")
|
||||||
|
->execute([$fileId, (int)$file['equipment_id']]);
|
||||||
|
|
||||||
|
mnt_log($pdo, 'cover_changed', (int)$file['equipment_id'], null, null, $currentUserId);
|
||||||
|
|
||||||
|
mnt_json_ok();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
|
||||||
|
mnt_require_permission('maintenance.manage');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-file upload for the three attachment tables.
|
||||||
|
* scope = equipment | maintenance | intervention
|
||||||
|
* kind = file | photo (equipment only; photos feed the gallery)
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
$pdo = mnt_pdo();
|
||||||
|
|
||||||
|
$scope = (string)($_POST['scope'] ?? 'equipment');
|
||||||
|
$ownerId = (int)($_POST['owner_id'] ?? 0);
|
||||||
|
$kind = ($_POST['kind'] ?? 'file') === 'photo' ? 'photo' : 'file';
|
||||||
|
|
||||||
|
$config = [
|
||||||
|
'equipment' => ['table' => 'inv_equipment_files', 'owner' => 'equipment_id', 'parent' => 'inv_equipment'],
|
||||||
|
'maintenance' => ['table' => 'maint_maintenance_files', 'owner' => 'maintenance_id', 'parent' => 'maint_maintenances'],
|
||||||
|
'intervention' => ['table' => 'maint_intervention_files', 'owner' => 'intervention_id', 'parent' => 'maint_interventions'],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isset($config[$scope])) {
|
||||||
|
mnt_json_fail('Scope non valido.');
|
||||||
|
}
|
||||||
|
if ($ownerId <= 0) {
|
||||||
|
mnt_json_fail('ID non valido.');
|
||||||
|
}
|
||||||
|
if (empty($_FILES['files']['name'][0])) {
|
||||||
|
mnt_json_fail('Nessun file selezionato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$target = $config[$scope];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT id FROM {$target['parent']} WHERE id = ?");
|
||||||
|
$stmt->execute([$ownerId]);
|
||||||
|
if (!$stmt->fetchColumn()) {
|
||||||
|
mnt_json_fail('Record collegato non trovato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$columns = $scope === 'equipment'
|
||||||
|
? "({$target['owner']}, kind, original_name, stored_name, mime_type, size, uploaded_by)"
|
||||||
|
: "({$target['owner']}, original_name, stored_name, mime_type, size, uploaded_by)";
|
||||||
|
$placeholders = $scope === 'equipment' ? '(?, ?, ?, ?, ?, ?, ?)' : '(?, ?, ?, ?, ?, ?)';
|
||||||
|
|
||||||
|
$insert = $pdo->prepare("INSERT INTO {$target['table']} $columns VALUES $placeholders");
|
||||||
|
|
||||||
|
$uploaded = [];
|
||||||
|
$errors = [];
|
||||||
|
$fileCount = count($_FILES['files']['name']);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $fileCount; $i++) {
|
||||||
|
$file = [
|
||||||
|
'name' => $_FILES['files']['name'][$i],
|
||||||
|
'type' => $_FILES['files']['type'][$i],
|
||||||
|
'tmp_name' => $_FILES['files']['tmp_name'][$i],
|
||||||
|
'error' => $_FILES['files']['error'][$i],
|
||||||
|
'size' => $_FILES['files']['size'][$i],
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stored = mnt_store_upload($file, $scope);
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
$errors[] = $file['name'] . ': ' . $e->getMessage();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A file uploaded as a photo but not actually an image is kept as a file
|
||||||
|
$rowKind = ($kind === 'photo' && $stored['is_image']) ? 'photo' : 'file';
|
||||||
|
|
||||||
|
$values = $scope === 'equipment'
|
||||||
|
? [$ownerId, $rowKind, $stored['original_name'], $stored['stored_name'], $stored['mime_type'], $stored['size'], $currentUserId]
|
||||||
|
: [$ownerId, $stored['original_name'], $stored['stored_name'], $stored['mime_type'], $stored['size'], $currentUserId];
|
||||||
|
|
||||||
|
$insert->execute($values);
|
||||||
|
$uploaded[] = ['id' => (int)$pdo->lastInsertId(), 'name' => $stored['original_name']];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$uploaded) {
|
||||||
|
mnt_json_fail($errors ? implode('; ', $errors) : 'Nessun file caricato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first photo of an item becomes its cover automatically
|
||||||
|
if ($scope === 'equipment' && $kind === 'photo') {
|
||||||
|
$stmt = $pdo->prepare("SELECT cover_file_id FROM inv_equipment WHERE id = ?");
|
||||||
|
$stmt->execute([$ownerId]);
|
||||||
|
if (!$stmt->fetchColumn()) {
|
||||||
|
$pdo->prepare("UPDATE inv_equipment SET cover_file_id = ? WHERE id = ?")
|
||||||
|
->execute([$uploaded[0]['id'], $ownerId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$equipmentId = $scope === 'equipment' ? $ownerId : null;
|
||||||
|
$maintenanceId = $scope === 'maintenance' ? $ownerId : null;
|
||||||
|
$interventionId = $scope === 'intervention' ? $ownerId : null;
|
||||||
|
|
||||||
|
mnt_log($pdo, 'file_added', $equipmentId, $maintenanceId, $interventionId, $currentUserId, null,
|
||||||
|
implode(', ', array_column($uploaded, 'name')));
|
||||||
|
|
||||||
|
mnt_json_ok(['uploaded' => $uploaded, 'errors' => $errors]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
FullCalendar Core v6.1.9
|
||||||
|
Docs & License: https://fullcalendar.io
|
||||||
|
(c) 2023 Adam Shaw
|
||||||
|
*/
|
||||||
|
!function(e){"use strict";var t={code:"it",week:{dow:1,doy:4},buttonText:{prev:"Prec",next:"Succ",today:"Oggi",year:"Anno",month:"Mese",week:"Settimana",day:"Giorno",list:"Agenda"},weekText:"Sm",allDayText:"Tutto il giorno",moreLinkText:e=>"+altri "+e,noEventsText:"Non ci sono eventi da visualizzare"};FullCalendar.globalLocales.push(t)}();
|
||||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user