Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e13e0c9b2a | ||
|
|
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,275 @@
|
|||||||
|
# Manutenzioni — тест-план
|
||||||
|
|
||||||
|
Полный список проверок. **Бо́льшая часть автоматизирована** — руками остаётся только то, что действительно требует глаз (список ниже).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Автопрогон — одна команда
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test # всё: 20 юнитов + 68 API-проверок + 69 браузерных тестов
|
||||||
|
```
|
||||||
|
|
||||||
|
По отдельности (`make` без аргументов покажет весь список):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-unit # логика расписания, без базы и сервера (0.02 с)
|
||||||
|
make test-api # логин, ajax, БД, файлы, права — 68 проверок
|
||||||
|
make test-e2e # браузер на 1440 px — 69 тестов
|
||||||
|
make test-sizes # полный прогон на каждом размере: 1440 / 820 / 390 px
|
||||||
|
make test-e2e-ui # интерактивный режим, видно каждый шаг
|
||||||
|
```
|
||||||
|
|
||||||
|
### Прогон по размерам экрана
|
||||||
|
|
||||||
|
`make test-sizes` гоняет **всю** сюиту трижды — на PC (1440), планшете (820) и телефоне (390):
|
||||||
|
|
||||||
|
| Размер | Тестов | Что проверяется отдельно |
|
||||||
|
|---|---|---|
|
||||||
|
| desktop 1440 | 69 | + вёрстка на трёх ширинах и сквозной обход |
|
||||||
|
| tablet 820 | 56 | целевое устройство по ТЗ |
|
||||||
|
| mobile 390 | 56 | карточки вместо таблиц, полноэкранные модалки |
|
||||||
|
|
||||||
|
Итого 181 браузерная проверка. Тесты написаны без привязки к вёрстке: хелпер `listItem()` берёт строку таблицы или карточку — смотря что видно на этой ширине.
|
||||||
|
|
||||||
|
## Видео вместо кликанья
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make demo # ~50 с: сквозной обход модуля → tests/e2e/video/demo.mp4
|
||||||
|
make video # весь прогон, склеенный по устройствам → tests/e2e/video/
|
||||||
|
make video-sizes # то же для каждого размера → tests/e2e/video/<формат>/
|
||||||
|
```
|
||||||
|
|
||||||
|
`make demo` прогоняет `tour.spec.js` — один непрерывный сценарий: реестр и фильтры → импортированная карточка `mod. 6.3-1` → создание оборудования → добавление ТО → регистрация работы с подписью мышью → пересчёт срока → история → печатная форма → оба календаря → категории → телефон и планшет. Это полноценный тест: если что-то сломано, он упадёт.
|
||||||
|
|
||||||
|
`make video` записывает все 69 тестов и склеивает их в пять файлов (`ffmpeg`, склейка по разрешениям — иначе кадры разного размера не соединить):
|
||||||
|
|
||||||
|
| Файл | Что внутри | Длительность |
|
||||||
|
|---|---|---|
|
||||||
|
| `demo.mp4` | сквозной обход | ~50 с |
|
||||||
|
| `suite-funzionali.mp4` | реестр, ТО, интервенции, календари, печать, права | ~70 с |
|
||||||
|
| `suite-desktop.mp4` | вёрстка 1440 px | ~15 с |
|
||||||
|
| `suite-tablet.mp4` | вёрстка 820 px + подпись пальцем | ~35 с |
|
||||||
|
| `suite-mobile.mp4` | вёрстка 390 px | ~15 с |
|
||||||
|
|
||||||
|
Прогоны API и браузерный делают настоящий логин, работают с живой страницей и сверяют результат в базе. Заводят только данные с префиксом `ZZTEST_` / `ZZE2E_` и **удаляют их за собой**, в том числе при падении. Оба возвращают ненулевой код при ошибке — годятся для CI.
|
||||||
|
|
||||||
|
Полезное для e2e:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:e2e:ui # интерактивный режим: видно каждый шаг
|
||||||
|
npm run test:e2e:headed # с открытым браузером
|
||||||
|
npx playwright show-trace tests/e2e/.artifacts/<папка>/trace.zip # разбор падения
|
||||||
|
```
|
||||||
|
|
||||||
|
**Скриншоты.** Каждый прогон e2e складывает в `tests/e2e/screenshots/` по 5 картинок на устройство (390 / 820 / 1440 px): реестр, список ТО, карточка, календарь, модалка интервенции, плюс `tablet-signature.png` с реальным росчерком пальцем. Проще пролистать их, чем кликать вручную.
|
||||||
|
|
||||||
|
**Что закрыто автоматикой:** разделы 1, 2, 3, 5, 6, 7, 8, 9 (включая генерацию PDF), 10, 11, 12 (запуск без ошибок).
|
||||||
|
|
||||||
|
## Что смотреть глазами (короткий список)
|
||||||
|
|
||||||
|
Осталось немного — то, что машина проверить не может в принципе:
|
||||||
|
|
||||||
|
| Что | Пункты |
|
||||||
|
|---|---|
|
||||||
|
| Субъективное качество вёрстки — по скриншотам из `tests/e2e/screenshots/` | 2.1, 11.1–11.4 |
|
||||||
|
| Как выглядит PDF после реальной печати из браузера | 9.4, 9.5 |
|
||||||
|
| **Реальная доставка писем** на живой ящик | 12.2, 12.4 |
|
||||||
|
| Импортированные из XLS данные — выборочная сверка с их файлами | 13.2–13.7 |
|
||||||
|
| Подпись пальцем на **настоящем** планшете (в браузере эмулируется, но железо стоит потрогать) | 6.6 |
|
||||||
|
|
||||||
|
Ниже — полный список, если хочется пройти всё подряд руками.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Подготовка
|
||||||
|
|
||||||
|
- База: `http://localhost:8001/zibo-dashboard/public` (контейнеры `zibo-app` + `zibo-db`, база на порту 3308).
|
||||||
|
Если не подняты: `docker compose up -d` (порт 3307 может быть занят — тогда нужен override на 3308).
|
||||||
|
- Логин под админом (роль Admin, id 1). В форме логина поле называется **username**, туда e-mail.
|
||||||
|
- Дев-база уже наполнена импортом: **101 единица оборудования, 126 ТО, 20 подрядчиков, 5 категорий**.
|
||||||
|
- Всё меню модуля: сайдбар → **Manutenzioni**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ничего старого не сломалось (сначала это!)
|
||||||
|
|
||||||
|
Переименование `production_tools` → `inv_equipment` затронуло модуль производства.
|
||||||
|
|
||||||
|
- [ ] **1.1** `userarea/production_tools.php` — открывается по прежнему адресу, в списке видны `Taglierina` (×2), `Marcatura Laser`, `Coestrusore diametro 70` **и** оборудование из импорта.
|
||||||
|
- [ ] **1.2** Там же: кнопка «Aggiungi Strumento» — создать тестовый инструмент. Сохраняется, появляется в списке.
|
||||||
|
- [ ] **1.3** Он же появился в новом реестре (`Manutenzioni → Registro Attrezzature`) в категории **Attrezzature**.
|
||||||
|
- [ ] **1.4** Редактирование инструмента на старой странице: в поле «Stato» теперь три варианта (Attivo / Fuori servizio / Dismesso). Меняем на «Fuori servizio» → сохранилось.
|
||||||
|
- [ ] **1.5** Кнопка удаления на старой странице — раньше **всегда падала** с «Invalid ID.», теперь помечает как «Dismesso». Проверить на тестовом инструменте.
|
||||||
|
- [ ] **1.6** `userarea/skills.php` — открывается, выпадающий список «Attrezzature» заполнен.
|
||||||
|
- [ ] **1.7** `userarea/production_line_view2.php` — открывается, колонка с инструментами у записей производства не пустая и не сломана.
|
||||||
|
- [ ] **1.8** `userarea/production_dashboard.php` — открывается, кнопка «Attrezzature» ведёт куда вела.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Реестр оборудования
|
||||||
|
|
||||||
|
`Manutenzioni → Registro Attrezzature`
|
||||||
|
|
||||||
|
- [ ] **2.1** Список открывается, у части позиций видна обложка-заглушка (шестерёнка), у категорий свои цветные точки.
|
||||||
|
- [ ] **2.2** Поиск «CARRELLO» → находит погрузчики.
|
||||||
|
- [ ] **2.3** Фильтр по категории **Edifici e impianti** → ~24 позиции (здания, противопожарные двери, чиллеры, котёл).
|
||||||
|
- [ ] **2.4** Фильтр по линии **Linea 1 — UHF** → 10 позиций, все с «LINEA 1» в названии.
|
||||||
|
- [ ] **2.5** Фильтр по статусу «Dismesso» → пусто (или только то, что сам пометил в п.1.4).
|
||||||
|
- [ ] **2.6** Кнопка «Azzera» сбрасывает все фильтры.
|
||||||
|
- [ ] **2.7** «Nuova attrezzatura» → заполнить имя + категорию **Attrezzature** → сохраняется, появляется в списке.
|
||||||
|
- [ ] **2.8** В той же форме выбрать категорию **Linea di produzione** → появляется обязательное поле «Linea di produzione». Попытка сохранить без линии → предупреждение.
|
||||||
|
- [ ] **2.9** Редактирование (карандаш) — форма открывается **с заполненными** полями, изменения сохраняются.
|
||||||
|
- [ ] **2.10** Удаление (корзина) своей тестовой записи → удаляется после подтверждения.
|
||||||
|
- [ ] **2.11** Удаление `Taglierina` (id 1) → **отказ**: «используется в 2 записях производства, пометьте как Dismesso».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Категории
|
||||||
|
|
||||||
|
`Manutenzioni → Categorie`
|
||||||
|
|
||||||
|
- [ ] **3.1** Пять категорий: Linea di produzione, Edifici e impianti, Attrezzature, Accessori, Qualità. У «Linea di produzione» стоит галка «Richiede linea».
|
||||||
|
- [ ] **3.2** Создать категорию «Test QA» с цветом и порядком → появилась.
|
||||||
|
- [ ] **3.3** Создать вторую с тем же именем → отказ «Esiste già una categoria con questo nome».
|
||||||
|
- [ ] **3.4** Отредактировать «Test QA», поставить «Richiede una linea di produzione» → сохранилось; в форме оборудования при выборе этой категории появляется поле линии.
|
||||||
|
- [ ] **3.5** Удалить «Test QA» (пустую) → удаляется.
|
||||||
|
- [ ] **3.6** Удалить «Attrezzature» → **отказ**: используется N оборудованием.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Карточка оборудования, фото и документы
|
||||||
|
|
||||||
|
Открыть `CARRELLO ELEVATORE CAR.MI. MOD. PL416` (из реестра).
|
||||||
|
|
||||||
|
- [ ] **4.1** Слева блок «Dati»: матрикола `55551002N5069`, производитель `CAR.MI.`, дата ввода в эксплуатацию 01/01/2002.
|
||||||
|
- [ ] **4.2** Справа 8 работ с кодами **A–H**, частоты: A — «Ogni 2 settimane», B–E — «Ogni 3 mesi», F,G — «Ogni 6 mesi», H — «Ogni 1 anno».
|
||||||
|
- [ ] **4.3** Загрузить фото (кнопка «+» в блоке «Foto») → появилось в галерее и **сразу стало обложкой** (рамка), обложка видна в списке реестра.
|
||||||
|
- [ ] **4.4** Загрузить второе фото → обложка не поменялась. Нажать на нём звёздочку → стало обложкой.
|
||||||
|
- [ ] **4.5** Загрузить документ PDF в блок «Documenti» → появился, открывается по клику в новой вкладке.
|
||||||
|
- [ ] **4.6** Попробовать загрузить `.php` файл → отказ «Tipo di file non consentito».
|
||||||
|
- [ ] **4.7** Скопировать ссылку на фото, открыть в **приватном окне** (без сессии) → 401, файл не отдаётся.
|
||||||
|
- [ ] **4.8** Удалить фото и документ → исчезают.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Манутенции (ТО)
|
||||||
|
|
||||||
|
- [ ] **5.1** «Nuova manutenzione» на карточке: код `Z`, название, тип **Programmata**, исполнение **Interna**, частота «Ogni 2 mesi», преавизо 15, исполнитель и ответственный → сохранилось, видно в списке.
|
||||||
|
- [ ] **5.2** Исполнение **Esterna** → появляется обязательный «Fornitore esterno». Сохранить без него → предупреждение.
|
||||||
|
- [ ] **5.3** Частота **«Al bisogno»** → поле «Ogni» прячется; сохранённая запись показывает «Al bisogno», даты «Prossimo» нет.
|
||||||
|
- [ ] **5.4** Поле «Nota sulla frequenza»: вписать «prima dell'accensione» → текст виден в строке ТО после частоты.
|
||||||
|
- [ ] **5.5** Галка «Manutenzione critica» → красная плашка «Critica» и красная полоса слева.
|
||||||
|
- [ ] **5.6** Галка «Attiva» снята → строка тускнеет, плашка «Disattivata»; такое ТО пропадает из общего списка `Manutenzioni`.
|
||||||
|
- [ ] **5.7** Приложить файл-инструкцию в форме ТО → после сохранения счётчик «N istruzioni» в строке; при повторном открытии формы файл в списке, скачивается, удаляется.
|
||||||
|
- [ ] **5.8** Удалить своё тестовое ТО → предупреждение о числе интервентов, после подтверждения исчезает.
|
||||||
|
- [ ] **5.9** Открыть `CHILLER UFFICI E POMPA DI CALORE ELCO` → 4 работы A–D **без даты** (сезонные), в примечании частоты полный текст «prima dell'accensione / durante il periodo estivo… / dopo lo spegnimento». Плюс одна внешняя от `Piacenza Impianti`.
|
||||||
|
- [ ] **5.10** Открыть `EVAPORATORE SOTTOVUOTO…` → **три** ТО: внутреннее + два внешних, `Ecotechno` и `Ecogenerazione` (не должны схлопнуться в одно).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Регистрация интервенций и пересчёт срока
|
||||||
|
|
||||||
|
Это ядро модуля — проверить внимательно. Взять любое ТО с частотой (например CARMI, код B — «Ogni 3 mesi»).
|
||||||
|
|
||||||
|
- [ ] **6.1** Кнопка «✓» → открывается форма, дата подставлена сегодняшняя, вверху написано, к какому оборудованию и ТО относится.
|
||||||
|
- [ ] **6.2** Поставить дату **три месяца назад**, статус **Completato**, эсито **Conforme**, оператора, заметку, материалы → сохранить.
|
||||||
|
- [ ] **6.3** В строке ТО: «Ultimo» = введённая дата, «Prossimo» = она же + 3 месяца, плашка состояния пересчиталась.
|
||||||
|
- [ ] **6.4** Кнопка «Storico interventi (N)» разворачивает таймлайн, там видна запись с оператором, эсито и заметками.
|
||||||
|
- [ ] **6.5** **Подпись:** в форме интервенции расписаться мышью в поле подписи → после сохранения миниатюра подписи видна в таймлайне и в печатной форме.
|
||||||
|
- [ ] **6.6** На планшете/телефоне: подпись рисуется пальцем, страница при этом не скроллится.
|
||||||
|
- [ ] **6.7** «Cancella firma» очищает поле; при редактировании существующей интервенции стирает и сохранённую подпись.
|
||||||
|
- [ ] **6.8** Добавить интервенцию в статусе **Pianificato** на будущую дату → «Prossimo» **не изменился** (двигают только завершённые).
|
||||||
|
- [ ] **6.9** Приложить файл к интервенции (модуль подрядчика) → счётчик скрепки в таймлайне, файл открывается.
|
||||||
|
- [ ] **6.10** Удалить последнюю завершённую интервенцию → «Prossimo» откатился на предыдущую дату (или опустел, если она была единственной).
|
||||||
|
- [ ] **6.11** Редактирование интервенции (карандаш в таймлайне) → поля подставлены, изменение даты пересчитывает срок.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Общий список ТО
|
||||||
|
|
||||||
|
`Manutenzioni → Manutenzioni`
|
||||||
|
|
||||||
|
- [ ] **7.1** Четыре плитки сверху: Scadute / In scadenza / In regola / Al bisogno, сумма ≈ числу активных ТО активного оборудования.
|
||||||
|
- [ ] **7.2** Клик по плитке «Scadute» фильтрует список; повторный клик снимает фильтр.
|
||||||
|
- [ ] **7.3** Фильтр «Solo programmata» / «Solo straordinaria» работает.
|
||||||
|
- [ ] **7.4** Галка «Solo critiche» → остаются только с красной плашкой.
|
||||||
|
- [ ] **7.5** Галка «Assegnate a me» → остаются ТО, где ты исполнитель или ответственный (нужно, чтобы твой `auth_users` был связан с `employees.auth_user_id`; если связи нет — список пустой, это ожидаемо).
|
||||||
|
- [ ] **7.6** Сортировка: просроченные и ближайшие сверху, «Al bisogno» — в конце.
|
||||||
|
- [ ] **7.7** Кнопка «✓» прямо из списка открывает ту же форму интервенции.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Календари
|
||||||
|
|
||||||
|
- [ ] **8.1** `Manutenzioni → Calendario`: события на датах, цвет по состоянию (красный — просрочено, жёлтый — скоро, иначе цвет категории).
|
||||||
|
- [ ] **8.2** Клик по событию → открывается карточка нужного оборудования.
|
||||||
|
- [ ] **8.3** Фильтры «тип» и «категория» перерисовывают календарь.
|
||||||
|
- [ ] **8.4** На телефоне календарь открывается сразу в виде списка (listMonth), не ломается по ширине.
|
||||||
|
- [ ] **8.5** `Manutenzioni → Calendario Cumulativo`: три переключателя — Scadenze 📅, Formazione 🎓, Manutenzioni 🔧.
|
||||||
|
- [ ] **8.6** Выключение слоя убирает его события, включение возвращает.
|
||||||
|
- [ ] **8.7** Клик по событию scadenza ведёт в scadenzario, по formazione — в trainings, по manutenzione — в карточку оборудования.
|
||||||
|
- [ ] **8.8** Выключить все три слоя → календарь пуст, ошибок нет.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Печать / PDF
|
||||||
|
|
||||||
|
- [ ] **9.1** На карточке кнопка «Scheda PDF» → открывается вёрстка их бланка: шапка «SCHEDA MANUTENZIONE», справа «ZIBOGOMMA / mod. 6.3-1», строка «MACCHINA:».
|
||||||
|
- [ ] **9.2** Таблица «Interventi previsti» — коды, описания, частоты, исполнение (внутр./фирма).
|
||||||
|
- [ ] **9.3** Таблица «Registro interventi eseguiti» — дата, код, описание, оператор, картинка подписи.
|
||||||
|
- [ ] **9.4** Ctrl/Cmd+P → в предпросмотре печати кнопки скрыты, таблицы не рвутся по строкам, шапка таблицы повторяется на второй странице.
|
||||||
|
- [ ] **9.5** Сохранить как PDF — файл читаемый.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Права (важно — проверяется на бэке, не только в меню)
|
||||||
|
|
||||||
|
Понадобится второй пользователь с ролью без прав `maintenance.*` (например `employee`).
|
||||||
|
|
||||||
|
- [ ] **10.1** Под таким пользователем пункт «Manutenzioni» в меню не виден.
|
||||||
|
- [ ] **10.2** Прямой заход на `userarea/manutenzioni/index.php` → «Permesso negato», а не страница.
|
||||||
|
- [ ] **10.3** Выдать в «Autorizzazioni» роли только `maintenance.equipment.view` → реестр открывается, но **кнопок создания/редактирования нет**, блок «Manutenzioni» на карточке пишет, что прав нет.
|
||||||
|
- [ ] **10.4** Добавить `maintenance.manage` → кнопки появились, создание работает.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Мобильный и планшет
|
||||||
|
|
||||||
|
Проверять на реальном планшете или в devtools (375px и 768px).
|
||||||
|
|
||||||
|
- [ ] **11.1** 375px: реестр показан **карточками**, не таблицей; ничего не уезжает за край, горизонтального скролла нет.
|
||||||
|
- [ ] **11.2** 375px: список ТО — карточками; плитки-счётчики по две в ряд.
|
||||||
|
- [ ] **11.3** Модалки открываются на весь экран, кнопки внизу доступны.
|
||||||
|
- [ ] **11.4** 768px (планшет — целевое устройство по ТЗ): таблицы читаемы, сайдбар по умолчанию скрыт, гамбургер работает.
|
||||||
|
- [ ] **11.5** Форма интервенции на планшете: подпись пальцем, все поля доступны без зума.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Уведомления (cron)
|
||||||
|
|
||||||
|
- [ ] **12.1** Поставить у ТО исполнителя, у которого `employees.auth_user_id` связан с реальным почтовым ящиком, и дату «Prossimo» в прошлом.
|
||||||
|
- [ ] **12.2** Запустить: `docker exec zibo-app php /var/www/zibo-dashboard/public/userarea/manutenzioni/cron/send_notifications.php`
|
||||||
|
→ в консоли «Inviate: N», письмо приходит исполнителю **и** ответственному.
|
||||||
|
- [ ] **12.3** Запустить второй раз в тот же день → «saltate», повторных писем нет.
|
||||||
|
- [ ] **12.4** В письме: тема с названием оборудования, дата, для критичного ТО — пометка, кнопка ведёт на карточку.
|
||||||
|
- [ ] **12.5** ТО с частотой «Al bisogno» писем не порождает.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Импортированные данные (выборочно, глазами)
|
||||||
|
|
||||||
|
- [ ] **13.1** Всего в реестре ~101 позиция; фильтр по категориям даёт ~74 Attrezzature, ~24 Edifici e impianti, 3 Linee.
|
||||||
|
- [ ] **13.2** У `FABBRICATI` — 8 работ уборки от `PULINET di Angelini Bassano` (giornaliero / settimanale / mensile / trimestrale / al bisogno).
|
||||||
|
- [ ] **13.3** У позиций с «LINEA 1/2/3» в названии проставлена соответствующая линия (в карточке строка «Linea»).
|
||||||
|
- [ ] **13.4** Исполнители расставлены: Alfonso, Gabriele, Junior, Marco, Ester, Marina.
|
||||||
|
- [ ] **13.5** У `DERATTIZZAZIONE CON ESCHE DOMESTICHE` исполнитель пуст, а в заметках ТО написано «Incaricato da XLS: M.GRANATA» — это фирма, записанная в колонке внутренних работ; надо решить, переводить ли её в подрядчики.
|
||||||
|
- [ ] **13.6** В заметках оборудования сохранены имена их бумажных карточек («Schede originali: …»).
|
||||||
|
- [ ] **13.7** **Ожидаемо и требует решения клиента:** у 110 из 125 ТО частота «Al bisogno» — в сводном XLS частот попросту нет, они лежат в отдельных карточках `mod. 6.3-1`, а прислали три из 57.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Чего в модуле нет намеренно
|
||||||
|
|
||||||
|
- Калибровка/поверка приборов — вынесена за скоуп самим клиентом. Структура данных её примет отдельной таблицей без переделок.
|
||||||
|
- Импорт истории выполненных работ из старых карточек — журналы в присланных файлах пустые.
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# Manutenzioni — трассируемость: ТЗ → тесты
|
||||||
|
|
||||||
|
Каждый пункт исходного документа клиента (`Manutenzioni - ACS.pdf`) и то, чем он подтверждён.
|
||||||
|
Условные обозначения: **UI** — браузерный тест (Playwright, гоняется на 1440/820/390), **API** — `tests/manutenzioni/api_test.php`, **U** — юнит-тест PHPUnit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Реестр оборудования
|
||||||
|
|
||||||
|
| Пункт ТЗ | Чем подтверждён | Тест |
|
||||||
|
|---|---|---|
|
||||||
|
| Единая база оборудования, у каждой позиции категория | UI | `§1.2 le quattro categorie del capitolato esistono e sono gestibili` |
|
||||||
|
| Категории: Linea di produzione, Attrezzature, Accessori, Qualità | UI | там же |
|
||||||
|
| Категории **не хардкод**, свой CRUD | UI + API | `Categories › CRUD from the modal, and categories in use cannot be deleted`; API: создание, дубль имени, запрет удаления занятой |
|
||||||
|
| Для категории «линия» обязательно указать линию | UI + API | `§1.5 il campo «linea» compare solo per le categorie che lo richiedono`; API: отказ без линии |
|
||||||
|
| Фильтрация по категории / линии / отделу | UI | `§1.5 the registry filters by production line and by department`; `Equipment registry › the filters really narrow the list` |
|
||||||
|
| Перенос существующих `production_lines` и `production_tools` | UI + API | `§1.6 le linee e gli attrezzi già esistenti sono nel registro unico`; API: старые страницы производства работают после переименования |
|
||||||
|
| Поля: название, серийник, партия, дата покупки, дата ввода, производитель, тип, ubicazione, отдел, заметки | UI | `§1.7 tutti i campi dell'anagrafica si salvano e si rivedono` — сохранение, возврат в форму и отображение в карточке |
|
||||||
|
| Статус (attivo / fuori servizio / dismesso) исключает неактивное из планировочных вью | UI | `§1.7 un'attrezzatura non attiva sparisce dalle viste di pianificazione` — проверяет и список ТО, и календарь |
|
||||||
|
| Вложения: файлы-инструкции и мануалы | UI + API | `Photos, cover image and documents › documents upload and stay downloadable`; отказ на недопустимый тип |
|
||||||
|
| Обложка + дополнительные фото | UI | `la prima foto caricata diventa copertina, anche nel registro`; `la stellina sposta la copertina sulla seconda foto`; `la foto si elimina previa conferma` |
|
||||||
|
| Калибровка — вне скоупа | — | намеренно не реализована; отдельной таблицы нет |
|
||||||
|
|
||||||
|
## 2. Модуль обслуживания
|
||||||
|
|
||||||
|
| Пункт ТЗ | Чем подтверждён | Тест |
|
||||||
|
|---|---|---|
|
||||||
|
| 0..N обслуживаний на единицу; 0 = вне планирования | UI | `§2.1 un'attrezzatura senza manutenzioni resta fuori dalla pianificazione` |
|
||||||
|
| Несколько видов ТО одновременно | UI | `§2.2 la stessa attrezzatura porta più manutenzioni insieme` (три работы на одной машине) |
|
||||||
|
| Типы: программное и внеплановое | UI + API | `«solo critiche» e «programmate/straordinarie» restringono l'elenco`; `riaprendo una manutenzione i campi sono quelli salvati` |
|
||||||
|
| Флаг критичности | UI | `una manutenzione critica è evidenziata nella scheda` |
|
||||||
|
| Внешние исполнители: указать подрядчика | UI + API | `«esterna» chiede il fornitore e blocca il salvataggio senza` |
|
||||||
|
| Приложить форму подрядчика | UI | `un allegato dell'intervento compare nello storico` |
|
||||||
|
| Название/описание | UI | `riaprendo una manutenzione i campi sono quelli salvati` |
|
||||||
|
| Частота через выпадающий список интервалов | UI + U | `«al bisogno» nasconde il campo della frequenza`; `la nota di frequenza resta visibile`; юниты на все единицы интервала |
|
||||||
|
| Файлы-инструкции к ТО | UI | `le istruzioni allegate a una manutenzione si vedono, si riaprono e si eliminano` |
|
||||||
|
| Срок предупреждения | UI | `riaprendo una manutenzione i campi sono quelli salvati` (alert_days) |
|
||||||
|
| Один исполнитель + один ответственный | UI | `§2.6 incaricato e responsabile si salvano e si leggono nella scheda` |
|
||||||
|
| Следующий срок = от последней выполненной работы | UI + U | `registrare un intervento completato aggiorna le date a schermo`; `un intervento pianificato non sposta la scadenza`; `eliminando l'ultimo intervento completato la scadenza torna indietro`; 20 юнитов на арифметику дат |
|
||||||
|
|
||||||
|
## 3. Журнал интервенций
|
||||||
|
|
||||||
|
| Пункт ТЗ | Чем подтверждён | Тест |
|
||||||
|
|---|---|---|
|
||||||
|
| Привязка к конкретному ТО | UI | `la registrazione rapida dall'elenco aggiorna la scadenza` (контекст в модалке) |
|
||||||
|
| Дата | UI | `registrare un intervento completato aggiorna le date a schermo` |
|
||||||
|
| Заметки | UI | `riaprendo un intervento i campi sono quelli salvati` |
|
||||||
|
| Оператор + подпись | UI | `la firma disegnata finisce nello storico`; `la firma si cancella con il pulsante dedicato`; `si firma col dito e la pagina non scorre sotto il tratto` (планшет/телефон) |
|
||||||
|
| Статус (pianificato / in corso / completato) | UI | `un intervento pianificato non sposta la scadenza` |
|
||||||
|
| Результат (conforme / bloccato …) | UI | `riaprendo un intervento i campi sono quelli salvati` |
|
||||||
|
| Материалы / замененные части | UI | там же |
|
||||||
|
| Для внешних: подрядчик и его форма | UI | `un allegato dell'intervento compare nello storico` |
|
||||||
|
|
||||||
|
## 4. Фильтры и вью
|
||||||
|
|
||||||
|
| Пункт ТЗ | Чем подтверждён | Тест |
|
||||||
|
|---|---|---|
|
||||||
|
| Фильтр между программными и внеплановыми | UI | `«solo critiche» e «programmate/straordinarie» restringono l'elenco` |
|
||||||
|
| Календарь ТО по срокам | UI | `il calendario disegna gli eventi e il click porta alla scheda`; `i filtri del calendario rifanno la richiesta` |
|
||||||
|
|
||||||
|
## 5. Выгрузка PDF
|
||||||
|
|
||||||
|
| Пункт ТЗ | Чем подтверждён | Тест |
|
||||||
|
|---|---|---|
|
||||||
|
| Карточка с историей обслуживания по единице | UI | `la scheda riproduce il modulo cartaceo` (шапка mod. 6.3-1, работы, журнал) |
|
||||||
|
| Собственно PDF | UI | `in stampa la barra dei pulsanti sparisce e il PDF si genera` |
|
||||||
|
|
||||||
|
## 6. Уведомления
|
||||||
|
|
||||||
|
| Пункт ТЗ | Чем подтверждён | Тест |
|
||||||
|
|---|---|---|
|
||||||
|
| Письма о просрочке и заранее | API | cron запускается без ошибок; выборка находит просроченное ТО |
|
||||||
|
| Получатели: ответственный **и** исполнитель | — | **браузером не проверяется**: письма уходят из cron, а не из UI. Логика получателей сверена запросом; фактическая доставка — ручной пункт 12 тест-плана |
|
||||||
|
|
||||||
|
## 7. Сводный календарь
|
||||||
|
|
||||||
|
| Пункт ТЗ | Чем подтверждён | Тест |
|
||||||
|
|---|---|---|
|
||||||
|
| Scadenze + formazione + manutenzioni в одном календаре | UI | `ogni strato porta i propri eventi`; `il calendario cumulativo accende e spegne gli strati` |
|
||||||
|
| Разные цветные бейджи по типу срока | UI | `§7 i tre tipi di scadenza hanno colori distinti` (свой цвет и свой значок у каждого слоя) |
|
||||||
|
| Переход из календаря в нужный модуль | UI | `dal calendario cumulativo si arriva alla scheda dell'attrezzatura` |
|
||||||
|
|
||||||
|
## 8. Техтребования
|
||||||
|
|
||||||
|
| Пункт ТЗ | Чем подтверждён | Тест |
|
||||||
|
|---|---|---|
|
||||||
|
| Работа на PC и планшете → адаптивность | UI × 3 | весь набор гоняется на 1440 / 820 / 390; `Layout › …` (нет горизонтальной прокрутки, карточки против таблицы, модалка достижима, сайдбар не прыгает) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что осталось за пределами автотестов
|
||||||
|
|
||||||
|
| Пункт | Почему | Где проверять |
|
||||||
|
|---|---|---|
|
||||||
|
| Фактическая доставка писем | нужен живой SMTP и ящик | ручной чеклист, раздел 12 |
|
||||||
|
| Внешний вид PDF после печати из браузера | субъективно | ручной чеклист, разделы 9.4–9.5 |
|
||||||
|
| Подпись на физическом планшете | эмуляция ≠ железо | ручной чеклист, 6.6 |
|
||||||
|
| Соответствие импортированных данных их XLS | сверка с бумагой | ручной чеклист, раздел 13 |
|
||||||
|
|
||||||
|
## Как перепроверить
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-sizes # весь набор на трёх размерах
|
||||||
|
npx playwright test tests/e2e/capitolato.spec.js # только пункты ТЗ
|
||||||
|
```
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// @ts-check
|
||||||
|
const { defineConfig, devices } = require('@playwright/test')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E2E suite for the Manutenzioni (maintenance) module.
|
||||||
|
*
|
||||||
|
* Covers what the PHP tests cannot see: modals, canvas signature, calendars,
|
||||||
|
* printing and the layout on tablet/phone.
|
||||||
|
*
|
||||||
|
* Prerequisite: the project containers are up
|
||||||
|
* docker compose up -d
|
||||||
|
* Base URL can be overridden with MNT_BASE_URL.
|
||||||
|
*/
|
||||||
|
// The trailing slash is mandatory: without it the relative paths used by the
|
||||||
|
// tests would resolve against the domain root instead of the app.
|
||||||
|
const baseURL = (process.env.MNT_BASE_URL || 'http://localhost:8001/zibo-dashboard/public').replace(/\/?$/, '/')
|
||||||
|
|
||||||
|
/** Viewports under test: phone, tablet (the reference device), desktop. */
|
||||||
|
const SIZES = {
|
||||||
|
desktop: { width: 1440, height: 900 },
|
||||||
|
tablet: { width: 820, height: 1180 },
|
||||||
|
mobile: { width: 390, height: 844 },
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIZE = SIZES[process.env.MNT_SIZE] ? process.env.MNT_SIZE : 'desktop'
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
testDir: './tests/e2e',
|
||||||
|
outputDir: './tests/e2e/.artifacts',
|
||||||
|
globalSetup: require.resolve('./tests/e2e/global-setup'),
|
||||||
|
globalTeardown: require.resolve('./tests/e2e/global-teardown'),
|
||||||
|
|
||||||
|
// the tests write to the same fixture data: no parallelism
|
||||||
|
workers: 1,
|
||||||
|
fullyParallel: false,
|
||||||
|
|
||||||
|
// Video recording slows every page down noticeably: with the same limits the
|
||||||
|
// longer tests would time out without anything actually being broken.
|
||||||
|
timeout: process.env.MNT_VIDEO ? 90_000 : 45_000,
|
||||||
|
expect: { timeout: process.env.MNT_VIDEO ? 20_000 : 10_000 },
|
||||||
|
|
||||||
|
// The HTML report moves attachments into hash-named folders: handy to browse,
|
||||||
|
// but it makes grouping the videos per device impossible. With MNT_VIDEO we
|
||||||
|
// therefore keep the plain reporter and leave the files in .artifacts.
|
||||||
|
reporter: process.env.MNT_VIDEO
|
||||||
|
// The JSON feeds the stitching step: it maps every video to the exact test
|
||||||
|
// title, which then gets burned into the caption bar.
|
||||||
|
? [['list'], ['json', { outputFile: 'tests/e2e/.report/results.json' }]]
|
||||||
|
: process.env.CI
|
||||||
|
? [['list'], ['html', { open: 'never', outputFolder: 'tests/e2e/.report' }]]
|
||||||
|
: [['list']],
|
||||||
|
|
||||||
|
use: {
|
||||||
|
baseURL,
|
||||||
|
storageState: 'tests/e2e/.auth/state.json',
|
||||||
|
locale: 'it-IT',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
trace: process.env.MNT_VIDEO ? 'on' : 'retain-on-failure',
|
||||||
|
|
||||||
|
// By default the video is kept only for failing tests: with MNT_VIDEO=1
|
||||||
|
// (npm run test:e2e:video) everything is recorded, one .webm per test,
|
||||||
|
// handy to show the module running without clicking through it.
|
||||||
|
video: process.env.MNT_VIDEO
|
||||||
|
? { mode: 'on', size: { width: 1280, height: 720 } }
|
||||||
|
: 'retain-on-failure',
|
||||||
|
},
|
||||||
|
|
||||||
|
// One viewport per invocation: the tests create data under fixed names, and
|
||||||
|
// running them in parallel across viewports would make them collide on the
|
||||||
|
// same database. The loop over the three viewports lives in the Makefile
|
||||||
|
// (make test-sizes).
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: SIZE,
|
||||||
|
use: {
|
||||||
|
...devices['Desktop Chrome'],
|
||||||
|
viewport: SIZES[SIZE],
|
||||||
|
hasTouch: SIZE !== 'desktop',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
@@ -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()]);
|
||||||
|
}
|
||||||
@@ -256,7 +256,6 @@ $departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
<!-- jQuery and Bootstrap -->
|
<!-- jQuery and Bootstrap -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
@@ -367,7 +366,7 @@ $departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+1658
-424
File diff suppressed because it is too large
Load Diff
@@ -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,31 @@
|
|||||||
<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; ?>
|
||||||
|
|
||||||
|
<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'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -551,7 +551,7 @@ function revisionLabel($rev)
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
+857
-63
File diff suppressed because it is too large
Load Diff
@@ -231,7 +231,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
<!-- jQuery + Bootstrap -->
|
<!-- jQuery + Bootstrap -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
@@ -133,7 +132,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
@@ -273,6 +272,13 @@
|
|||||||
<td><span class='qty-badge'>{$qtyTot}</span></td>
|
<td><span class='qty-badge'>{$qtyTot}</span></td>
|
||||||
<td>{$badge}</td>
|
<td>{$badge}</td>
|
||||||
<td>
|
<td>
|
||||||
|
<button class='btn btn-sm btn-outline-primary edit-item'
|
||||||
|
data-id='{$r['id']}'
|
||||||
|
data-category='" . htmlspecialchars($r['category'], ENT_QUOTES) . "'
|
||||||
|
data-name='" . htmlspecialchars($r['item_name'], ENT_QUOTES) . "'
|
||||||
|
data-code='" . htmlspecialchars($r['item_code'], ENT_QUOTES) . "'>
|
||||||
|
✏️ Modifica
|
||||||
|
</button>
|
||||||
<button class='btn btn-sm btn-outline-dark manage-stock'
|
<button class='btn btn-sm btn-outline-dark manage-stock'
|
||||||
data-id='{$r['id']}'
|
data-id='{$r['id']}'
|
||||||
data-name='" . htmlspecialchars($r['item_name'], ENT_QUOTES) . "'>
|
data-name='" . htmlspecialchars($r['item_name'], ENT_QUOTES) . "'>
|
||||||
@@ -343,7 +349,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- MODALE: MODIFICA IMBALLO -->
|
||||||
|
<div class="modal fade" id="editItemModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||||
|
<h5 class="modal-title">Modifica Imballo</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="editItemForm">
|
||||||
|
<input type="hidden" id="editItemId">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Categoria</label>
|
||||||
|
<select class="form-select" id="editCategory" required>
|
||||||
|
<option value="PACKAGING_TYPE">Tipo Confezione</option>
|
||||||
|
<option value="BOX">Scatola</option>
|
||||||
|
<option value="PALLET">Pallet</option>
|
||||||
|
<option value="OTHER">Altro</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="editItemName" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Codice</label>
|
||||||
|
<input type="text" class="form-control" id="editItemCode" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="submit" class="btn btn-add">💾 Salva modifiche</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- MODALE: STOCK FORNITORI/LOTTI -->
|
<!-- MODALE: STOCK FORNITORI/LOTTI -->
|
||||||
<div class="modal fade" id="stockModal" tabindex="-1">
|
<div class="modal fade" id="stockModal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||||
@@ -506,7 +552,48 @@
|
|||||||
|
|
||||||
$('#filterActive, #filterCategory').on('change', refreshUrlParams);
|
$('#filterActive, #filterCategory').on('change', refreshUrlParams);
|
||||||
});
|
});
|
||||||
|
// Open edit item modal
|
||||||
|
$(document).on("click", ".edit-item", function() {
|
||||||
|
$("#editItemId").val($(this).data("id"));
|
||||||
|
$("#editCategory").val($(this).data("category"));
|
||||||
|
$("#editItemName").val($(this).data("name"));
|
||||||
|
$("#editItemCode").val($(this).data("code"));
|
||||||
|
|
||||||
|
$("#editItemModal").modal("show");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save edited item
|
||||||
|
$("#editItemForm").on("submit", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const id = $("#editItemId").val();
|
||||||
|
const category = $("#editCategory").val();
|
||||||
|
const item_name = $("#editItemName").val().trim();
|
||||||
|
const item_code = $("#editItemCode").val().trim();
|
||||||
|
|
||||||
|
fetch("update_packaging_item.php", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
body: `id=${encodeURIComponent(id)}&category=${encodeURIComponent(category)}&item_name=${encodeURIComponent(item_name)}&item_code=${encodeURIComponent(item_code)}`
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: "success",
|
||||||
|
title: "Aggiornato!"
|
||||||
|
}).then(() => location.reload());
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Errore",
|
||||||
|
text: data.message || "Non aggiornato"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
// Create new item
|
// Create new item
|
||||||
$("#addItemForm").on("submit", function(e) {
|
$("#addItemForm").on("submit", function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -559,11 +646,16 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Flag: true se lo stock è stato modificato mentre il modale era aperto
|
||||||
|
let stockChanged = false;
|
||||||
|
|
||||||
// Open stock modal
|
// Open stock modal
|
||||||
$(document).on("click", ".manage-stock", function() {
|
$(document).on("click", ".manage-stock", function() {
|
||||||
const id = $(this).data("id");
|
const id = $(this).data("id");
|
||||||
const name = $(this).data("name");
|
const name = $(this).data("name");
|
||||||
|
|
||||||
|
stockChanged = false; // reset ad ogni apertura
|
||||||
|
|
||||||
$("#stockItemId").val(id);
|
$("#stockItemId").val(id);
|
||||||
$("#stockItemName").text(name);
|
$("#stockItemName").text(name);
|
||||||
|
|
||||||
@@ -572,6 +664,13 @@
|
|||||||
loadSuppliersSelect($("#stockSupplier"), $("#stockModal")).then(() => stockLoadRows(id));
|
loadSuppliersSelect($("#stockSupplier"), $("#stockModal")).then(() => stockLoadRows(id));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Alla chiusura del modale, se qualcosa è cambiato ricarica la pagina
|
||||||
|
$("#stockModal").on("hidden.bs.modal", function() {
|
||||||
|
if (stockChanged) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Click edit stock row
|
// Click edit stock row
|
||||||
$(document).on("click", ".stock-edit", function() {
|
$(document).on("click", ".stock-edit", function() {
|
||||||
$("#stockEditId").val($(this).data("id"));
|
$("#stockEditId").val($(this).data("id"));
|
||||||
@@ -630,11 +729,9 @@
|
|||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
stockChanged = true;
|
||||||
stockResetForm();
|
stockResetForm();
|
||||||
stockLoadRows(itemId);
|
stockLoadRows(itemId);
|
||||||
// Refresh totals by reloading page (simple and safe)
|
|
||||||
// If you want live update without reload, we can do it.
|
|
||||||
// location.reload();
|
|
||||||
} else {
|
} else {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: "error",
|
icon: "error",
|
||||||
@@ -670,8 +767,8 @@
|
|||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
stockChanged = true;
|
||||||
stockLoadRows(itemId);
|
stockLoadRows(itemId);
|
||||||
// location.reload();
|
|
||||||
} else {
|
} else {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: "error",
|
icon: "error",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,6 @@
|
|||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
|
||||||
<!-- Bootstrap (se già incluso puoi rimuoverlo) -->
|
<!-- Bootstrap (se già incluso puoi rimuoverlo) -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
|
|
||||||
<!-- SweetAlert2 -->
|
<!-- SweetAlert2 -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
|
||||||
<!-- Bootstrap -->
|
<!-- Bootstrap -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||||
|
|||||||
@@ -150,17 +150,24 @@ $dashboardSections = [
|
|||||||
[
|
[
|
||||||
'id' => 'secPersonale',
|
'id' => 'secPersonale',
|
||||||
'title' => 'Personale',
|
'title' => 'Personale',
|
||||||
'subtitle' => 'Dipendenti, skill',
|
'subtitle' => 'Dipendenti, formazione, skill',
|
||||||
'icon' => '👥',
|
'icon' => '👥',
|
||||||
'open' => false,
|
'open' => false,
|
||||||
'buttons' => [
|
'buttons' => [
|
||||||
[
|
[
|
||||||
'label' => 'Employees',
|
'label' => 'Dipendenti',
|
||||||
'icon' => '👥',
|
'icon' => '👥',
|
||||||
'class' => 'btn-employees',
|
'class' => 'btn-employees',
|
||||||
'url' => 'employees.php',
|
'url' => 'employees.php',
|
||||||
'permission' => 'hr.employees.view',
|
'permission' => 'hr.employees.view',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Mansioni',
|
||||||
|
'icon' => '🧩',
|
||||||
|
'class' => 'btn-setup',
|
||||||
|
'url' => 'job-roles.php',
|
||||||
|
'permission' => 'hr.employees.view',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'label' => 'Departments',
|
'label' => 'Departments',
|
||||||
'icon' => '🏢',
|
'icon' => '🏢',
|
||||||
@@ -168,6 +175,20 @@ $dashboardSections = [
|
|||||||
'url' => 'departments.php',
|
'url' => 'departments.php',
|
||||||
'permission' => 'hr.departments.view',
|
'permission' => 'hr.departments.view',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'label' => 'DPI',
|
||||||
|
'icon' => '🦺',
|
||||||
|
'class' => 'btn-setup',
|
||||||
|
'url' => 'ppe-items.php',
|
||||||
|
'permission' => 'hr.employees.view',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Gestione Formazione',
|
||||||
|
'icon' => '🎓',
|
||||||
|
'class' => 'btn-setup',
|
||||||
|
'url' => 'trainings.php',
|
||||||
|
'permission' => 'hr.trainings.view',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'label' => 'Skills',
|
'label' => 'Skills',
|
||||||
'icon' => '🧠',
|
'icon' => '🧠',
|
||||||
@@ -478,17 +499,118 @@ $dashboardSections = [
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
|
|
||||||
<?php
|
<?php $pdo = DBHandlerSelect::getInstance()->getConnection(); ?>
|
||||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
<style>
|
||||||
include(__DIR__ . '/scadenzario/include/my_deadlines_widget.php');
|
.my-deadlines-widgets {
|
||||||
?>
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Each widget wraps itself in .my-deadlines-widgets; collapse the nested
|
||||||
|
wrapper so all cards flow into the outer flex (single row). */
|
||||||
|
.my-deadlines-widgets .my-deadlines-widgets {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.8rem 0.9rem;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: transform 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.my-deadlines-widgets .mdw {
|
||||||
|
flex: 1 1 calc(50% - 0.375rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 575.98px) {
|
||||||
|
.my-deadlines-widgets .mdw {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-red {
|
||||||
|
background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-orange {
|
||||||
|
background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-gray {
|
||||||
|
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-icon {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.22);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-body {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 1.2;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-count {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-label {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
opacity: 0.95;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-arrow {
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="my-deadlines-widgets">
|
||||||
|
<?php include(__DIR__ . '/scadenzario/include/my_deadlines_widget.php'); ?>
|
||||||
|
<?php include(__DIR__ . '/include/training_widget.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 class="dashboard-title">Dashboard</h3>
|
<h3 class="dashboard-title">Dashboard</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -1114,7 +1114,7 @@ if (!empty($_GET['ajax'])) {
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['action'])) {
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php');
|
<?php include('include/navbar.php');
|
||||||
include('include/topbar.php'); ?>
|
include('include/topbar.php'); ?>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
include('include/headscript.php');
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
|
||||||
|
$titolo = trim($_POST['titolo'] ?? '');
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID non valido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($titolo === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Il titolo non può essere vuoto']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$titolo = mb_substr($titolo, 0, 255);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE mescole_files SET titolo = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$titolo, $id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'titolo' => $titolo]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Errore database: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
include('include/headscript.php');
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$categorie = require __DIR__ . '/config/mescole_file_categories.php';
|
||||||
|
|
||||||
|
$idmescola = isset($_POST['idmescola']) ? (int)$_POST['idmescola'] : 0;
|
||||||
|
$categoria = $_POST['categoria'] ?? '';
|
||||||
|
$titolo = trim($_POST['titolo'] ?? '');
|
||||||
|
|
||||||
|
if ($idmescola <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID mescola non valido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (!array_key_exists($categoria, $categorie)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Categoria non valida']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'File mancante o upload non riuscito']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Validazioni di sicurezza sul file ---
|
||||||
|
$allowedExt = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'jpg', 'jpeg', 'png'];
|
||||||
|
$maxSize = 15 * 1024 * 1024; // 15 MB
|
||||||
|
|
||||||
|
$originalName = $_FILES['file']['name'];
|
||||||
|
$tmpPath = $_FILES['file']['tmp_name'];
|
||||||
|
$size = (int)$_FILES['file']['size'];
|
||||||
|
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
// Titolo automatico dal nome file se non fornito
|
||||||
|
if ($titolo === '') {
|
||||||
|
$titolo = pathinfo($originalName, PATHINFO_FILENAME);
|
||||||
|
$titolo = trim(preg_replace('/[_\-]+/', ' ', $titolo));
|
||||||
|
if ($titolo === '') {
|
||||||
|
$titolo = 'Documento del ' . date('d/m/Y');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$titolo = mb_substr($titolo, 0, 255);
|
||||||
|
|
||||||
|
if (!in_array($ext, $allowedExt, true)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Estensione file non consentita (' . $ext . ')']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($size <= 0 || $size > $maxSize) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Il file supera la dimensione massima di 15MB']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
$mimeType = finfo_file($finfo, $tmpPath);
|
||||||
|
finfo_close($finfo);
|
||||||
|
|
||||||
|
// Cartella dedicata per mescola: uploads/mescole/{idmescola}/
|
||||||
|
$destDir = __DIR__ . '/uploads/mescole/' . $idmescola . '/';
|
||||||
|
if (!is_dir($destDir)) {
|
||||||
|
mkdir($destDir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nome fisico casuale: evita collisioni e path traversal
|
||||||
|
$storedName = bin2hex(random_bytes(16)) . '.' . $ext;
|
||||||
|
$destPath = $destDir . $storedName;
|
||||||
|
|
||||||
|
if (!move_uploaded_file($tmpPath, $destPath)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file sul server']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO mescole_files (idmescola, categoria, titolo, filename, original_filename, mime_type, filesize, uploaded_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
|
||||||
|
");
|
||||||
|
$stmt->execute([$idmescola, $categoria, $titolo, $storedName, $originalName, $mimeType, $size]);
|
||||||
|
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'id' => $newId,
|
||||||
|
'row' => [
|
||||||
|
'id' => $newId,
|
||||||
|
'idmescola' => $idmescola,
|
||||||
|
'categoria' => $categoria,
|
||||||
|
'categoria_label' => $categorie[$categoria],
|
||||||
|
'titolo' => $titolo,
|
||||||
|
'filename' => $storedName,
|
||||||
|
'original_filename' => $originalName,
|
||||||
|
'mime_type' => $mimeType,
|
||||||
|
'filesize' => $size,
|
||||||
|
'uploaded_at' => date('Y-m-d H:i:s'),
|
||||||
|
'url' => 'uploads/mescole/' . $idmescola . '/' . $storedName,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// rollback file se il DB fallisce
|
||||||
|
@unlink($destPath);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Errore database: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
include('../../include/headscript.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
function jsonResponse(array $data): void
|
||||||
|
{
|
||||||
|
echo json_encode($data);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeNullableInt($value): ?int
|
||||||
|
{
|
||||||
|
return (isset($value) && $value !== '') ? (int)$value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$isHrManager = Auth::user()->hasRole('Admin')
|
||||||
|
|| Auth::user()->hasRole('Superuser')
|
||||||
|
|| Auth::user()->hasRole('employee-hr')
|
||||||
|
|| Auth::user()->hasRole('manager');
|
||||||
|
|
||||||
|
if (!$isHrManager) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Non autorizzato.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$employeeId = (int)($_POST['employee_id'] ?? 0);
|
||||||
|
$firstName = trim($_POST['first_name'] ?? '');
|
||||||
|
$lastName = trim($_POST['last_name'] ?? '');
|
||||||
|
$employeeCode = trim($_POST['employee_code'] ?? '');
|
||||||
|
$hireDate = trim($_POST['hire_date'] ?? '');
|
||||||
|
$address = trim($_POST['address'] ?? '');
|
||||||
|
$phone = trim($_POST['phone'] ?? '');
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
$departmentId = normalizeNullableInt($_POST['department_id'] ?? '');
|
||||||
|
$status = trim($_POST['status'] ?? 'active');
|
||||||
|
$authUserId = normalizeNullableInt($_POST['auth_user_id'] ?? '');
|
||||||
|
$roleId = normalizeNullableInt($_POST['role_id'] ?? '');
|
||||||
|
|
||||||
|
$jobSubRoleIds = $_POST['job_sub_role_ids'] ?? [];
|
||||||
|
if (!is_array($jobSubRoleIds)) {
|
||||||
|
$jobSubRoleIds = [$jobSubRoleIds];
|
||||||
|
}
|
||||||
|
|
||||||
|
$jobSubRoleIds = array_values(array_unique(array_filter(array_map('intval', $jobSubRoleIds))));
|
||||||
|
|
||||||
|
if ($employeeId <= 0) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'ID dipendente non valido.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($firstName === '' || $lastName === '') {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Nome e cognome sono obbligatori.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Email non valida.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($status, ['active', 'inactive', 'suspended'], true)) {
|
||||||
|
$status = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtEmployee = $pdo->prepare('SELECT id FROM employees WHERE id = ? LIMIT 1');
|
||||||
|
$stmtEmployee->execute([$employeeId]);
|
||||||
|
if (!$stmtEmployee->fetchColumn()) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Dipendente non trovato.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$primaryJobRoleId = null;
|
||||||
|
$primaryJobSubRoleId = null;
|
||||||
|
|
||||||
|
if ($jobSubRoleIds) {
|
||||||
|
$placeholders = implode(',', array_fill(0, count($jobSubRoleIds), '?'));
|
||||||
|
$stmtSubRoles = $pdo->prepare("\n SELECT id, job_role_id\n FROM job_sub_roles\n WHERE id IN ($placeholders)\n AND is_active = 1\n ");
|
||||||
|
$stmtSubRoles->execute($jobSubRoleIds);
|
||||||
|
$validRows = $stmtSubRoles->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$validMap = [];
|
||||||
|
foreach ($validRows as $row) {
|
||||||
|
$validMap[(int)$row['id']] = (int)$row['job_role_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$jobSubRoleIds = array_values(array_filter($jobSubRoleIds, static function ($id) use ($validMap) {
|
||||||
|
return isset($validMap[(int)$id]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
if ($jobSubRoleIds) {
|
||||||
|
$primaryJobSubRoleId = (int)$jobSubRoleIds[0];
|
||||||
|
$primaryJobRoleId = $validMap[$primaryJobSubRoleId] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("\n UPDATE employees\n SET first_name = :first_name,\n last_name = :last_name,\n employee_code = :employee_code,\n hire_date = :hire_date,\n address = :address,\n phone = :phone,\n email = :email,\n department_id = :department_id,\n job_role_id = :job_role_id,\n job_sub_role_id = :job_sub_role_id,\n status = :status,\n auth_user_id = :auth_user_id,\n updated_at = NOW()\n WHERE id = :employee_id\n ");
|
||||||
|
$stmt->execute([
|
||||||
|
'first_name' => $firstName,
|
||||||
|
'last_name' => $lastName,
|
||||||
|
'employee_code' => $employeeCode !== '' ? $employeeCode : null,
|
||||||
|
'hire_date' => $hireDate !== '' ? $hireDate : null,
|
||||||
|
'address' => $address !== '' ? $address : null,
|
||||||
|
'phone' => $phone !== '' ? $phone : null,
|
||||||
|
'email' => $email !== '' ? $email : null,
|
||||||
|
'department_id' => $departmentId,
|
||||||
|
'job_role_id' => $primaryJobRoleId,
|
||||||
|
'job_sub_role_id' => $primaryJobSubRoleId,
|
||||||
|
'status' => $status,
|
||||||
|
'auth_user_id' => $authUserId,
|
||||||
|
'employee_id' => $employeeId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$stmtDelete = $pdo->prepare('DELETE FROM employee_job_sub_roles WHERE employee_id = ?');
|
||||||
|
$stmtDelete->execute([$employeeId]);
|
||||||
|
|
||||||
|
if ($jobSubRoleIds) {
|
||||||
|
$stmtInsert = $pdo->prepare("\n INSERT INTO employee_job_sub_roles\n (employee_id, job_sub_role_id, is_primary, created_at)\n VALUES\n (:employee_id, :job_sub_role_id, :is_primary, NOW())\n ");
|
||||||
|
|
||||||
|
foreach ($jobSubRoleIds as $index => $jobSubRoleId) {
|
||||||
|
$stmtInsert->execute([
|
||||||
|
'employee_id' => $employeeId,
|
||||||
|
'job_sub_role_id' => (int)$jobSubRoleId,
|
||||||
|
'is_primary' => $index === 0 ? 1 : 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authUserId !== null && $roleId !== null) {
|
||||||
|
$checkRole = $pdo->prepare('SELECT COUNT(*) FROM auth_roles WHERE id = ?');
|
||||||
|
$checkRole->execute([$roleId]);
|
||||||
|
|
||||||
|
if ((int)$checkRole->fetchColumn() > 0) {
|
||||||
|
$stmtRole = $pdo->prepare('UPDATE auth_users SET role_id = :role_id, updated_at = NOW() WHERE id = :auth_user_id');
|
||||||
|
$stmtRole->execute([
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'auth_user_id' => $authUserId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
jsonResponse(['success' => true]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if (isset($pdo) && $pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -50,17 +50,38 @@ try {
|
|||||||
$documentDate = $deadline['document_date'] ? new DateTime($deadline['document_date']) : null;
|
$documentDate = $deadline['document_date'] ? new DateTime($deadline['document_date']) : null;
|
||||||
|
|
||||||
switch ($deadline['recurrence_type']) {
|
switch ($deadline['recurrence_type']) {
|
||||||
case 'monthly': $interval = new DateInterval('P1M'); break;
|
case 'monthly':
|
||||||
case 'quarterly': $interval = new DateInterval('P3M'); break;
|
$interval = new DateInterval('P1M');
|
||||||
case 'semiannual': $interval = new DateInterval('P6M'); break;
|
break;
|
||||||
case 'annual': $interval = new DateInterval('P1Y'); break;
|
case 'quarterly':
|
||||||
case 'biennial': $interval = new DateInterval('P2Y'); break;
|
$interval = new DateInterval('P3M');
|
||||||
case 'triennial': $interval = new DateInterval('P3Y'); break;
|
break;
|
||||||
case 'quadriennial': $interval = new DateInterval('P4Y'); break;
|
case 'semiannual':
|
||||||
case 'quinquennial': $interval = new DateInterval('P5Y'); break;
|
$interval = new DateInterval('P6M');
|
||||||
case 'decennial': $interval = new DateInterval('P10Y'); break;
|
break;
|
||||||
case 'quindecennial': $interval = new DateInterval('P15Y'); break;
|
case 'annual':
|
||||||
default: $interval = null;
|
$interval = new DateInterval('P1Y');
|
||||||
|
break;
|
||||||
|
case 'biennial':
|
||||||
|
$interval = new DateInterval('P2Y');
|
||||||
|
break;
|
||||||
|
case 'triennial':
|
||||||
|
$interval = new DateInterval('P3Y');
|
||||||
|
break;
|
||||||
|
case 'quadriennial':
|
||||||
|
$interval = new DateInterval('P4Y');
|
||||||
|
break;
|
||||||
|
case 'quinquennial':
|
||||||
|
$interval = new DateInterval('P5Y');
|
||||||
|
break;
|
||||||
|
case 'decennial':
|
||||||
|
$interval = new DateInterval('P10Y');
|
||||||
|
break;
|
||||||
|
case 'quindecennial':
|
||||||
|
$interval = new DateInterval('P15Y');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$interval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($interval) {
|
if ($interval) {
|
||||||
@@ -70,17 +91,27 @@ try {
|
|||||||
|
|
||||||
$ins = $pdo->prepare("
|
$ins = $pdo->prepare("
|
||||||
INSERT INTO scad_deadlines
|
INSERT INTO scad_deadlines
|
||||||
(subject_id, function_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
(subject_id, function_id, notify_function, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
document_date, notification_days, storage_location, notes, variazioni, attiva_alert, created_by, departments)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
");
|
");
|
||||||
$ins->execute([
|
$ins->execute([
|
||||||
$deadline['subject_id'], $deadline['function_id'], $deadline['topic'], $deadline['law_regulation'],
|
$deadline['subject_id'],
|
||||||
$deadline['recurrence_type'], $dueDate->format('Y-m-d'),
|
$deadline['function_id'],
|
||||||
|
$deadline['notify_function'],
|
||||||
|
$deadline['topic'],
|
||||||
|
$deadline['law_regulation'],
|
||||||
|
$deadline['recurrence_type'],
|
||||||
|
$dueDate->format('Y-m-d'),
|
||||||
$checkDate ? $checkDate->format('Y-m-d') : null,
|
$checkDate ? $checkDate->format('Y-m-d') : null,
|
||||||
$documentDate ? $documentDate->format('Y-m-d') : null,
|
$documentDate ? $documentDate->format('Y-m-d') : null,
|
||||||
$deadline['notification_days'], $deadline['storage_location'],
|
$deadline['notification_days'],
|
||||||
$deadline['notes'], $deadline['created_by'], $deadline['departments']
|
$deadline['storage_location'],
|
||||||
|
$deadline['notes'],
|
||||||
|
$deadline['variazioni'],
|
||||||
|
$deadline['attiva_alert'],
|
||||||
|
$deadline['created_by'],
|
||||||
|
$deadline['departments']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$newId = $pdo->lastInsertId();
|
$newId = $pdo->lastInsertId();
|
||||||
@@ -137,7 +168,6 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode(['success' => true, 'message' => $msg, 'new_id' => $newId]);
|
echo json_encode(['success' => true, 'message' => $msg, 'new_id' => $newId]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if (isset($pdo) && $pdo->inTransaction()) {
|
if (isset($pdo) && $pdo->inTransaction()) {
|
||||||
$pdo->rollBack();
|
$pdo->rollBack();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ try {
|
|||||||
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
|
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
|
||||||
$subject_id = isset($_POST['subject_id']) && is_numeric($_POST['subject_id']) && (int)$_POST['subject_id'] > 0 ? (int)$_POST['subject_id'] : null;
|
$subject_id = isset($_POST['subject_id']) && is_numeric($_POST['subject_id']) && (int)$_POST['subject_id'] > 0 ? (int)$_POST['subject_id'] : null;
|
||||||
$function_id = isset($_POST['function_id']) && is_numeric($_POST['function_id']) && (int)$_POST['function_id'] > 0 ? (int)$_POST['function_id'] : null;
|
$function_id = isset($_POST['function_id']) && is_numeric($_POST['function_id']) && (int)$_POST['function_id'] > 0 ? (int)$_POST['function_id'] : null;
|
||||||
|
$notify_function = isset($_POST['notify_function']) && (int)$_POST['notify_function'] === 1 ? 1 : 0;
|
||||||
$topic = trim($_POST['topic'] ?? '');
|
$topic = trim($_POST['topic'] ?? '');
|
||||||
$law_regulation = trim($_POST['law_regulation'] ?? '') ?: null;
|
$law_regulation = trim($_POST['law_regulation'] ?? '') ?: null;
|
||||||
$recurrence_type = $_POST['recurrence_type'] ?? 'once';
|
$recurrence_type = $_POST['recurrence_type'] ?? 'once';
|
||||||
@@ -19,6 +20,8 @@ try {
|
|||||||
$notification_days = isset($_POST['notification_days']) && is_numeric($_POST['notification_days']) ? (int)$_POST['notification_days'] : 7;
|
$notification_days = isset($_POST['notification_days']) && is_numeric($_POST['notification_days']) ? (int)$_POST['notification_days'] : 7;
|
||||||
$storage_location = trim($_POST['storage_location'] ?? '') ?: null;
|
$storage_location = trim($_POST['storage_location'] ?? '') ?: null;
|
||||||
$notes = trim($_POST['notes'] ?? '') ?: null;
|
$notes = trim($_POST['notes'] ?? '') ?: null;
|
||||||
|
$variazioni = !empty($_POST['variazioni']) ? 1 : 0;
|
||||||
|
$attiva_alert = !empty($_POST['attiva_alert']) ? 1 : 0;
|
||||||
$employee_ids = $_POST['employee_ids'] ?? [];
|
$employee_ids = $_POST['employee_ids'] ?? [];
|
||||||
$department_names = $_POST['department_names'] ?? [];
|
$department_names = $_POST['department_names'] ?? [];
|
||||||
|
|
||||||
@@ -53,14 +56,15 @@ try {
|
|||||||
if ($id) {
|
if ($id) {
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
UPDATE scad_deadlines SET
|
UPDATE scad_deadlines SET
|
||||||
subject_id = ?, function_id = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
|
subject_id = ?, function_id = ?, notify_function = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
|
||||||
due_date = ?, check_date = ?, document_date = ?, notification_days = ?,
|
due_date = ?, check_date = ?, document_date = ?, notification_days = ?,
|
||||||
storage_location = ?, notes = ?, departments = ?
|
storage_location = ?, notes = ?, variazioni = ?, attiva_alert = ?, departments = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
");
|
");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$subject_id,
|
$subject_id,
|
||||||
$function_id,
|
$function_id,
|
||||||
|
$notify_function,
|
||||||
$topic,
|
$topic,
|
||||||
$law_regulation,
|
$law_regulation,
|
||||||
$recurrence_type,
|
$recurrence_type,
|
||||||
@@ -70,6 +74,8 @@ try {
|
|||||||
$notification_days,
|
$notification_days,
|
||||||
$storage_location,
|
$storage_location,
|
||||||
$notes,
|
$notes,
|
||||||
|
$variazioni,
|
||||||
|
$attiva_alert,
|
||||||
$departmentsStr,
|
$departmentsStr,
|
||||||
$id
|
$id
|
||||||
]);
|
]);
|
||||||
@@ -86,13 +92,14 @@ try {
|
|||||||
// INSERT
|
// INSERT
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
INSERT INTO scad_deadlines
|
INSERT INTO scad_deadlines
|
||||||
(subject_id, function_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
(subject_id, function_id, notify_function, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
document_date, notification_days, storage_location, notes, variazioni, attiva_alert, created_by, departments)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
");
|
");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$subject_id,
|
$subject_id,
|
||||||
$function_id,
|
$function_id,
|
||||||
|
$notify_function,
|
||||||
$topic,
|
$topic,
|
||||||
$law_regulation,
|
$law_regulation,
|
||||||
$recurrence_type,
|
$recurrence_type,
|
||||||
@@ -102,6 +109,8 @@ try {
|
|||||||
$notification_days,
|
$notification_days,
|
||||||
$storage_location,
|
$storage_location,
|
||||||
$notes,
|
$notes,
|
||||||
|
$variazioni,
|
||||||
|
$attiva_alert,
|
||||||
$currentUserId,
|
$currentUserId,
|
||||||
$departmentsStr
|
$departmentsStr
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scadenzario — Email notification cron script
|
* Scadenzario — Email notification cron script
|
||||||
* Run daily: 0 7 * * * php /var/www/html/public/userarea/scadenzario/cron/send_notifications.php
|
* Run daily: 0 7 * * * php /var/www/html/public/userarea/scadenzario/cron/send_notifications.php
|
||||||
@@ -42,16 +43,27 @@ $errors = 0;
|
|||||||
|
|
||||||
// Get active deadlines that are approaching or overdue
|
// Get active deadlines that are approaching or overdue
|
||||||
$stmt = $pdo->query("
|
$stmt = $pdo->query("
|
||||||
SELECT d.id, d.topic, s.name AS subject_name, d.due_date, d.notification_days
|
SELECT
|
||||||
|
d.id,
|
||||||
|
d.topic,
|
||||||
|
s.name AS subject_name,
|
||||||
|
d.due_date,
|
||||||
|
d.notification_days,
|
||||||
|
d.notify_function,
|
||||||
|
f.email AS function_email,
|
||||||
|
f.person_full_name AS function_person,
|
||||||
|
f.name AS function_name
|
||||||
FROM scad_deadlines d
|
FROM scad_deadlines d
|
||||||
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
||||||
|
LEFT JOIN scad_functions f ON f.id = d.function_id
|
||||||
WHERE d.status = 'active'
|
WHERE d.status = 'active'
|
||||||
|
AND d.attiva_alert = 1
|
||||||
AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)
|
AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)
|
||||||
");
|
");
|
||||||
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if (empty($deadlines)) {
|
if (empty($deadlines)) {
|
||||||
echo date('Y-m-d H:i:s') . " — Nessuna scadenza da notificare.\n";
|
echo date('Y-m-d H:i:s') . " — Nessuna scadenza da notificare (alert attivo).\n";
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,20 +113,28 @@ foreach ($deadlines as $dl) {
|
|||||||
$type = $isOverdue ? 'overdue' : 'approaching';
|
$type = $isOverdue ? 'overdue' : 'approaching';
|
||||||
$daysLeft = (int)((strtotime($dl['due_date']) - strtotime($today)) / 86400);
|
$daysLeft = (int)((strtotime($dl['due_date']) - strtotime($today)) / 86400);
|
||||||
|
|
||||||
// Collect all recipients (direct + department)
|
// Collect all recipients (direct + department + optional function email)
|
||||||
$recipients = [];
|
$recipients = [];
|
||||||
|
$functionRecipient = null;
|
||||||
|
|
||||||
$getRecipients->execute([$dl['id']]);
|
$getRecipients->execute([$dl['id']]);
|
||||||
foreach ($getRecipients->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
foreach ($getRecipients->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||||
$recipients[$r['employee_id']] = $r;
|
$recipients[$r['employee_id']] = $r;
|
||||||
}
|
}
|
||||||
|
|
||||||
$getDeptRecipients->execute([$dl['id']]);
|
// Optional: also notify the linked function email if enabled on the deadline.
|
||||||
foreach ($getDeptRecipients->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
if (
|
||||||
$recipients[$r['employee_id']] = $r;
|
!empty($dl['notify_function'])
|
||||||
|
&& !empty($dl['function_email'])
|
||||||
|
&& filter_var($dl['function_email'], FILTER_VALIDATE_EMAIL)
|
||||||
|
) {
|
||||||
|
$functionRecipient = [
|
||||||
|
'email' => $dl['function_email'],
|
||||||
|
'name' => trim(($dl['function_person'] ?? '') !== '' ? $dl['function_person'] : ($dl['function_name'] ?? 'Funzione')),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($recipients)) {
|
if (empty($recipients) && empty($functionRecipient)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,15 +213,99 @@ foreach ($deadlines as $dl) {
|
|||||||
$sent++;
|
$sent++;
|
||||||
|
|
||||||
echo date('H:i:s') . " ✓ {$type} → {$emp['email']} — {$dl['topic']}\n";
|
echo date('H:i:s') . " ✓ {$type} → {$emp['email']} — {$dl['topic']}\n";
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$errors++;
|
$errors++;
|
||||||
echo date('H:i:s') . " ✗ Errore {$emp['email']}: {$e->getMessage()}\n";
|
echo date('H:i:s') . " ✗ Errore {$emp['email']}: {$e->getMessage()}\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Send notification to function email if enabled.
|
||||||
|
// It is tracked with employee_id = 0 to avoid duplicate daily sends.
|
||||||
|
if ($functionRecipient) {
|
||||||
|
$functionEmployeeId = 0;
|
||||||
|
|
||||||
|
$checkSent->execute([$dl['id'], $functionEmployeeId, $type]);
|
||||||
|
if ($checkSent->fetchColumn() > 0) {
|
||||||
|
$skipped++;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$mail = new PHPMailer(true);
|
||||||
|
|
||||||
|
$mailer = $_ENV['MAIL_MAILER'] ?? 'mail';
|
||||||
|
if ($mailer === 'smtp') {
|
||||||
|
$mail->isSMTP();
|
||||||
|
$mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost';
|
||||||
|
$mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587);
|
||||||
|
|
||||||
|
if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') {
|
||||||
|
$mail->SMTPAuth = true;
|
||||||
|
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||||
|
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$enc = $_ENV['MAIL_ENCRYPTION'] ?? '';
|
||||||
|
if ($enc && $enc !== 'null') {
|
||||||
|
$mail->SMTPSecure = $enc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->CharSet = 'UTF-8';
|
||||||
|
$mail->setFrom(
|
||||||
|
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
|
||||||
|
$_ENV['MAIL_FROM_NAME'] ?? 'Scadenzario ZIBOGOMMA'
|
||||||
|
);
|
||||||
|
|
||||||
|
$mail->addAddress($functionRecipient['email'], $functionRecipient['name']);
|
||||||
|
|
||||||
|
if ($managerCcEmail && strcasecmp($managerCcEmail, $functionRecipient['email']) !== 0) {
|
||||||
|
$mail->addCC($managerCcEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
$detailUrl = $appUrl . '/userarea/scadenzario/detail.php?id=' . $dl['id'];
|
||||||
|
$topicText = (!empty($dl['subject_name']) ? $dl['subject_name'] . ' — ' : '') . $dl['topic'];
|
||||||
|
|
||||||
|
if ($isOverdue) {
|
||||||
|
$mail->Subject = '⚠️ Scadenza superata: ' . $dl['topic'];
|
||||||
|
$mail->Body = buildHtml(
|
||||||
|
'Scadenza superata',
|
||||||
|
$topicText,
|
||||||
|
'La scadenza era prevista per il <strong>' . date('d/m/Y', strtotime($dl['due_date'])) . '</strong> ed è stata superata da <strong>' . abs($daysLeft) . ' giorni</strong>.',
|
||||||
|
'#dc3545',
|
||||||
|
$detailUrl
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$mail->Subject = '📅 Scadenza in arrivo: ' . $dl['topic'];
|
||||||
|
$daysText = $daysLeft === 0 ? 'oggi' : 'tra <strong>' . $daysLeft . ' giorni</strong>';
|
||||||
|
$mail->Body = buildHtml(
|
||||||
|
'Scadenza in arrivo',
|
||||||
|
$topicText,
|
||||||
|
'La scadenza è prevista per il <strong>' . date('d/m/Y', strtotime($dl['due_date'])) . '</strong> (' . $daysText . ').',
|
||||||
|
'#e8930c',
|
||||||
|
$detailUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->isHTML(true);
|
||||||
|
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
|
||||||
|
|
||||||
|
$mail->send();
|
||||||
|
|
||||||
|
$insertNotif->execute([$dl['id'], $functionEmployeeId, $type]);
|
||||||
|
$sent++;
|
||||||
|
|
||||||
|
echo date('H:i:s') . " ✓ {$type} → funzione {$functionRecipient['email']} — {$dl['topic']}\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errors++;
|
||||||
|
echo date('H:i:s') . " ✗ Errore funzione {$functionRecipient['email']}: {$e->getMessage()}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// History (one per deadline, not per recipient)
|
// History (one per deadline, not per recipient)
|
||||||
$recipientNames = implode(', ', array_map(fn($r) => trim($r['first_name'] . ' ' . $r['last_name']), $recipients));
|
$recipientNames = implode(', ', array_map(fn($r) => trim($r['first_name'] . ' ' . $r['last_name']), $recipients));
|
||||||
|
|
||||||
|
if ($functionRecipient) {
|
||||||
|
$recipientNames .= ($recipientNames !== '' ? ', ' : '') . 'Funzione: ' . $functionRecipient['name'] . ' <' . $functionRecipient['email'] . '>';
|
||||||
|
}
|
||||||
|
|
||||||
$insertHistory->execute([$dl['id'], "Notifica {$type} inviata a: {$recipientNames}"]);
|
$insertHistory->execute([$dl['id'], "Notifica {$type} inviata a: {$recipientNames}"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -646,6 +646,24 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
|||||||
<div class="detail-label">Note</div>
|
<div class="detail-label">Note</div>
|
||||||
<div class="detail-value"><?= nl2br(htmlspecialchars($deadline['notes'], ENT_QUOTES, 'UTF-8')) ?></div>
|
<div class="detail-value"><?= nl2br(htmlspecialchars($deadline['notes'], ENT_QUOTES, 'UTF-8')) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="detail-label">Alert</div>
|
||||||
|
<div class="detail-value">
|
||||||
|
<?php if (!empty($deadline['attiva_alert'])): ?>
|
||||||
|
<i class="fa-solid fa-bell text-warning me-1"></i> Attivo
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-muted">Non attivo</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-label">In caso di variazioni</div>
|
||||||
|
<div class="detail-value">
|
||||||
|
<?php if (!empty($deadline['variazioni'])): ?>
|
||||||
|
<i class="fa-solid fa-arrows-rotate text-info me-1"></i> Sì
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-muted">No</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,122 @@
|
|||||||
<?php include('../../include/headscript.php'); ?>
|
<?php include('../../include/headscript.php'); ?>
|
||||||
<?php
|
<?php
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
$db = DBHandlerSelect::getInstance();
|
$db = DBHandlerSelect::getInstance();
|
||||||
$pdo = $db->getConnection();
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
$functions = $pdo->query("
|
function scadJsonResponse(array $data): void
|
||||||
SELECT f.*,
|
{
|
||||||
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id) AS deadline_count,
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id AND d.status <> 'completed') AS open_count
|
echo json_encode($data);
|
||||||
FROM scad_functions f
|
exit;
|
||||||
ORDER BY f.name ASC
|
}
|
||||||
")->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
function scadNullableString($value): ?string
|
||||||
|
{
|
||||||
|
$value = trim((string)($value ?? ''));
|
||||||
|
return $value !== '' ? $value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scadNormalizeStatus(string $status): string
|
||||||
|
{
|
||||||
|
return in_array($status, ['active', 'inactive'], true) ? $status : 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
function scadValidateEmail($email): ?string
|
||||||
|
{
|
||||||
|
$email = scadNullableString($email);
|
||||||
|
|
||||||
|
if ($email !== null && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
throw new Exception('Email non valida.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $email;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['ajax'] === '1') {
|
||||||
|
try {
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'save') {
|
||||||
|
$id = isset($_POST['id']) && $_POST['id'] !== '' ? (int)$_POST['id'] : 0;
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$description = scadNullableString($_POST['description'] ?? null);
|
||||||
|
$personFullName = scadNullableString($_POST['person_full_name'] ?? null);
|
||||||
|
$phone = scadNullableString($_POST['phone'] ?? null);
|
||||||
|
$email = scadValidateEmail($_POST['email'] ?? null);
|
||||||
|
$notes = scadNullableString($_POST['notes'] ?? null);
|
||||||
|
$sortOrder = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 0;
|
||||||
|
$status = scadNormalizeStatus(trim($_POST['status'] ?? 'active'));
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
scadJsonResponse(['success' => false, 'message' => 'Il nome funzione è obbligatorio.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($id > 0) {
|
||||||
|
$stmt = $pdo->prepare("\n UPDATE scad_functions\n SET name = :name,\n description = :description,\n person_full_name = :person_full_name,\n phone = :phone,\n email = :email,\n notes = :notes,\n sort_order = :sort_order,\n status = :status,\n updated_at = NOW()\n WHERE id = :id\n ");
|
||||||
|
$stmt->execute([
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description,
|
||||||
|
'person_full_name' => $personFullName,
|
||||||
|
'phone' => $phone,
|
||||||
|
'email' => $email,
|
||||||
|
'notes' => $notes,
|
||||||
|
'sort_order' => $sortOrder,
|
||||||
|
'status' => $status,
|
||||||
|
'id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
scadJsonResponse(['success' => true, 'message' => 'Funzione aggiornata correttamente.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("\n INSERT INTO scad_functions\n (name, description, person_full_name, phone, email, notes, sort_order, status, created_at, updated_at)\n VALUES\n (:name, :description, :person_full_name, :phone, :email, :notes, :sort_order, :status, NOW(), NOW())\n ");
|
||||||
|
$stmt->execute([
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description,
|
||||||
|
'person_full_name' => $personFullName,
|
||||||
|
'phone' => $phone,
|
||||||
|
'email' => $email,
|
||||||
|
'notes' => $notes,
|
||||||
|
'sort_order' => $sortOrder,
|
||||||
|
'status' => $status,
|
||||||
|
]);
|
||||||
|
|
||||||
|
scadJsonResponse(['success' => true, 'message' => 'Funzione creata correttamente.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
scadJsonResponse(['success' => false, 'message' => 'ID funzione non valido.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtUse = $pdo->prepare('SELECT COUNT(*) FROM scad_deadlines WHERE function_id = ?');
|
||||||
|
$stmtUse->execute([$id]);
|
||||||
|
$inUse = (int)$stmtUse->fetchColumn();
|
||||||
|
|
||||||
|
if ($inUse > 0) {
|
||||||
|
scadJsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Impossibile eliminare: la funzione è utilizzata in ' . $inUse . ' scadenza/e.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('DELETE FROM scad_functions WHERE id = :id');
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
|
||||||
|
scadJsonResponse(['success' => true, 'message' => 'Funzione eliminata correttamente.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
scadJsonResponse(['success' => false, 'message' => 'Azione non riconosciuta.']);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
scadJsonResponse(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$functions = $pdo->query("\n SELECT f.*,\n (SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id) AS deadline_count,\n (SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id AND d.status <> 'completed') AS open_count\n FROM scad_functions f\n ORDER BY COALESCE(f.sort_order, 0) ASC, f.name ASC\n")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="it">
|
<html lang="it">
|
||||||
@@ -24,11 +131,13 @@ $functions = $pdo->query("
|
|||||||
<base href="<?= $baseHref ?>">
|
<base href="<?= $baseHref ?>">
|
||||||
<?php include('../../cssinclude.php'); ?>
|
<?php include('../../cssinclude.php'); ?>
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
<title>Scadenzario - Funzioni</title>
|
<title>Scadenzario - Funzioni</title>
|
||||||
<script>
|
<script>
|
||||||
if (window.innerWidth > 1024) {
|
if (window.innerWidth > 1024) {
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
document.getElementById('appWrapper').classList.add('toggled');
|
const wrapper = document.getElementById('appWrapper');
|
||||||
|
if (wrapper) wrapper.classList.add('toggled');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -39,6 +148,7 @@ $functions = $pdo->query("
|
|||||||
--scad-heading: #2c3e6b;
|
--scad-heading: #2c3e6b;
|
||||||
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
|
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
|
||||||
--scad-card-border: #dde4f0;
|
--scad-card-border: #dde4f0;
|
||||||
|
--scad-muted: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scad-card {
|
.scad-card {
|
||||||
@@ -127,6 +237,63 @@ $functions = $pdo->query("
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.function-table th {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--scad-heading);
|
||||||
|
background: #f8fafc;
|
||||||
|
border-bottom: 1px solid var(--scad-card-border);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-table td {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-name {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--scad-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-description,
|
||||||
|
.function-notes {
|
||||||
|
color: var(--scad-muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
margin-top: 2px;
|
||||||
|
max-width: 280px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-line {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-line a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-function-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-function-status.active {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-function-status.inactive {
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
.function-card {
|
.function-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid var(--scad-card-border);
|
border: 1px solid var(--scad-card-border);
|
||||||
@@ -141,12 +308,19 @@ $functions = $pdo->query("
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.function-card .fc-meta {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--scad-muted);
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
.function-card .fc-stats {
|
.function-card .fc-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #6c757d;
|
color: var(--scad-muted);
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.function-card .fc-stats strong {
|
.function-card .fc-stats strong {
|
||||||
@@ -156,7 +330,7 @@ $functions = $pdo->query("
|
|||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 3rem 1rem;
|
padding: 3rem 1rem;
|
||||||
color: #6c757d;
|
color: var(--scad-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state i {
|
.empty-state i {
|
||||||
@@ -165,6 +339,15 @@ $functions = $pdo->query("
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background: var(--scad-card-bg);
|
||||||
|
border-bottom: 1px solid var(--scad-card-border);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 575.98px) {
|
@media (max-width: 575.98px) {
|
||||||
.scad-card .card-header {
|
.scad-card .card-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -223,25 +406,48 @@ $functions = $pdo->query("
|
|||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
|
|
||||||
<div id="functionsList">
|
<div id="functionsList">
|
||||||
|
|
||||||
<div class="d-md-none">
|
<div class="d-md-none">
|
||||||
<?php foreach ($functions as $f): ?>
|
<?php foreach ($functions as $f): ?>
|
||||||
|
<?php
|
||||||
|
$status = $f['status'] === 'inactive' ? 'inactive' : 'active';
|
||||||
|
$statusLabel = $status === 'active' ? 'Attiva' : 'Non attiva';
|
||||||
|
?>
|
||||||
<div class="function-card"
|
<div class="function-card"
|
||||||
data-id="<?= (int)$f['id'] ?>"
|
data-id="<?= (int)$f['id'] ?>"
|
||||||
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
|
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
|
||||||
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
data-status="<?= htmlspecialchars($f['status'], ENT_QUOTES, 'UTF-8') ?>"
|
data-person-full-name="<?= htmlspecialchars($f['person_full_name'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-phone="<?= htmlspecialchars($f['phone'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-email="<?= htmlspecialchars($f['email'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-notes="<?= htmlspecialchars($f['notes'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-sort-order="<?= (int)($f['sort_order'] ?? 0) ?>"
|
||||||
|
data-status="<?= htmlspecialchars($status, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
data-in-use="<?= (int)$f['deadline_count'] ?>">
|
data-in-use="<?= (int)$f['deadline_count'] ?>">
|
||||||
|
|
||||||
<div class="fc-name"><?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?></div>
|
<div class="d-flex justify-content-between align-items-start gap-2">
|
||||||
|
<div class="fc-name"><?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
<span class="badge-function-status <?= $status ?>"><?= $statusLabel ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php if (!empty($f['description'])): ?>
|
<?php if (!empty($f['description'])): ?>
|
||||||
<div class="text-muted small mt-1"><?= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?></div>
|
<div class="fc-meta"><?= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($f['person_full_name'])): ?>
|
||||||
|
<div class="fc-meta"><strong>Referente:</strong> <?= htmlspecialchars($f['person_full_name'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($f['phone']) || !empty($f['email'])): ?>
|
||||||
|
<div class="fc-meta">
|
||||||
|
<?php if (!empty($f['phone'])): ?>📞 <?= htmlspecialchars($f['phone'], ENT_QUOTES, 'UTF-8') ?><?php endif; ?>
|
||||||
|
<?php if (!empty($f['email'])): ?><?= !empty($f['phone']) ? '<br>' : '' ?>✉️ <?= htmlspecialchars($f['email'], ENT_QUOTES, 'UTF-8') ?><?php endif; ?>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="fc-stats">
|
<div class="fc-stats">
|
||||||
<span>Scadenze: <strong><?= (int)$f['deadline_count'] ?></strong></span>
|
<span>Scadenze: <strong><?= (int)$f['deadline_count'] ?></strong></span>
|
||||||
<span>Aperte: <strong><?= (int)$f['open_count'] ?></strong></span>
|
<span>Aperte: <strong><?= (int)$f['open_count'] ?></strong></span>
|
||||||
|
<span>Ordine: <strong><?= (int)($f['sort_order'] ?? 0) ?></strong></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-1 justify-content-end">
|
<div class="d-flex gap-1 justify-content-end">
|
||||||
@@ -258,30 +464,63 @@ $functions = $pdo->query("
|
|||||||
|
|
||||||
<div class="d-none d-md-block">
|
<div class="d-none d-md-block">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0 function-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nome</th>
|
<th style="width:70px" class="text-center">Ord.</th>
|
||||||
<th>Descrizione</th>
|
<th>Funzione</th>
|
||||||
<th class="text-center" style="width:120px">Scadenze</th>
|
<th style="width:220px">Referente</th>
|
||||||
<th class="text-center" style="width:120px">Aperte</th>
|
<th style="width:230px">Contatti</th>
|
||||||
<th class="text-center" style="width:120px">Azioni</th>
|
<th style="width:120px" class="text-center">Stato</th>
|
||||||
|
<th style="width:120px" class="text-center">Scadenze</th>
|
||||||
|
<th style="width:120px" class="text-center">Aperte</th>
|
||||||
|
<th style="width:120px" class="text-center">Azioni</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($functions as $f): ?>
|
<?php foreach ($functions as $f): ?>
|
||||||
|
<?php
|
||||||
|
$status = $f['status'] === 'inactive' ? 'inactive' : 'active';
|
||||||
|
$statusLabel = $status === 'active' ? 'Attiva' : 'Non attiva';
|
||||||
|
?>
|
||||||
<tr data-id="<?= (int)$f['id'] ?>"
|
<tr data-id="<?= (int)$f['id'] ?>"
|
||||||
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
|
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
|
||||||
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
data-status="<?= htmlspecialchars($f['status'], ENT_QUOTES, 'UTF-8') ?>"
|
data-person-full-name="<?= htmlspecialchars($f['person_full_name'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-phone="<?= htmlspecialchars($f['phone'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-email="<?= htmlspecialchars($f['email'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-notes="<?= htmlspecialchars($f['notes'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-sort-order="<?= (int)($f['sort_order'] ?? 0) ?>"
|
||||||
|
data-status="<?= htmlspecialchars($status, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
data-in-use="<?= (int)$f['deadline_count'] ?>">
|
data-in-use="<?= (int)$f['deadline_count'] ?>">
|
||||||
|
|
||||||
<td class="fw-semibold" style="color:var(--scad-heading)">
|
<td class="text-center"><?= (int)($f['sort_order'] ?? 0) ?></td>
|
||||||
<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>
|
<td>
|
||||||
|
<div class="function-name"><?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
<?php if (!empty($f['description'])): ?>
|
||||||
|
<div class="function-description" title="<?= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
<?= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($f['notes'])): ?>
|
||||||
|
<div class="function-notes" title="<?= htmlspecialchars($f['notes'], ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
Note: <?= htmlspecialchars($f['notes'], ENT_QUOTES, 'UTF-8') ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-muted">
|
<td><?= !empty($f['person_full_name']) ? htmlspecialchars($f['person_full_name'], ENT_QUOTES, 'UTF-8') : '<span class="text-muted">—</span>' ?></td>
|
||||||
<?= htmlspecialchars($f['description'] ?? '—', ENT_QUOTES, 'UTF-8') ?>
|
<td>
|
||||||
|
<?php if (!empty($f['phone'])): ?>
|
||||||
|
<div class="contact-line">📞 <a href="tel:<?= htmlspecialchars($f['phone'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($f['phone'], ENT_QUOTES, 'UTF-8') ?></a></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($f['email'])): ?>
|
||||||
|
<div class="contact-line">✉️ <a href="mailto:<?= htmlspecialchars($f['email'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($f['email'], ENT_QUOTES, 'UTF-8') ?></a></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (empty($f['phone']) && empty($f['email'])): ?>
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-center"><span class="badge-function-status <?= $status ?>"><?= $statusLabel ?></span></td>
|
||||||
<td class="text-center"><?= (int)$f['deadline_count'] ?></td>
|
<td class="text-center"><?= (int)$f['deadline_count'] ?></td>
|
||||||
<td class="text-center"><?= (int)$f['open_count'] ?></td>
|
<td class="text-center"><?= (int)$f['open_count'] ?></td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
@@ -300,9 +539,7 @@ $functions = $pdo->query("
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -314,7 +551,7 @@ $functions = $pdo->query("
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" id="functionModal" tabindex="-1" aria-hidden="true">
|
<div class="modal fade" id="functionModal" tabindex="-1" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered modal-lg modal-dialog-scrollable">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="functionModalTitle">Nuova Funzione</h5>
|
<h5 class="modal-title" id="functionModalTitle">Nuova Funzione</h5>
|
||||||
@@ -325,20 +562,57 @@ $functions = $pdo->query("
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<input type="hidden" id="functionId" name="id" value="">
|
<input type="hidden" id="functionId" name="id" value="">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="row">
|
||||||
<label for="functionName" class="form-label fw-semibold">Nome <span class="text-danger">*</span></label>
|
<div class="col-12 col-md-8 mb-3">
|
||||||
<input type="text" class="form-control" id="functionName" name="name" maxlength="255" required>
|
<label for="functionName" class="form-label fw-semibold">Nome funzione <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="functionName" name="name" maxlength="255" required placeholder="Es. RSPP, Medico del lavoro, RLS">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4 mb-3">
|
||||||
|
<label for="functionSortOrder" class="form-label fw-semibold">Ordine</label>
|
||||||
|
<input type="number" class="form-control" id="functionSortOrder" name="sort_order" min="0" step="1" value="0">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="functionDescription" class="form-label fw-semibold">Descrizione</label>
|
<label for="functionDescription" class="form-label fw-semibold">Descrizione</label>
|
||||||
<textarea class="form-control" id="functionDescription" name="description" rows="3"></textarea>
|
<textarea class="form-control" id="functionDescription" name="description" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="functionPersonFullName" class="form-label fw-semibold">Nome e cognome referente</label>
|
||||||
|
<input type="text" class="form-control" id="functionPersonFullName" name="person_full_name" maxlength="200" placeholder="Es. Mario Rossi">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label for="functionPhone" class="form-label fw-semibold">Telefono</label>
|
||||||
|
<input type="text" class="form-control" id="functionPhone" name="phone" maxlength="80">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label for="functionEmail" class="form-label fw-semibold">Email</label>
|
||||||
|
<input type="email" class="form-control" id="functionEmail" name="email" maxlength="190">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label for="functionStatus" class="form-label fw-semibold">Stato</label>
|
||||||
|
<select class="form-select" id="functionStatus" name="status">
|
||||||
|
<option value="active">Attiva</option>
|
||||||
|
<option value="inactive">Non attiva</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="functionNotes" class="form-label fw-semibold">Note operative</label>
|
||||||
|
<textarea class="form-control" id="functionNotes" name="notes" rows="3"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||||
<button type="submit" class="btn btn-scad-primary">Salva</button>
|
<button type="submit" class="btn btn-scad-primary" id="functionSaveBtn">Salva</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -349,6 +623,33 @@ $functions = $pdo->query("
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(function() {
|
$(function() {
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value == null ? '' : value).replace(/[&<>'"]/g, function(c) {
|
||||||
|
return ({
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
"'": ''',
|
||||||
|
'"': '"'
|
||||||
|
})[c];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowData($row) {
|
||||||
|
return {
|
||||||
|
id: $row.data('id') || '',
|
||||||
|
name: $row.data('name') || '',
|
||||||
|
description: $row.data('description') || '',
|
||||||
|
person_full_name: $row.data('person-full-name') || '',
|
||||||
|
phone: $row.data('phone') || '',
|
||||||
|
email: $row.data('email') || '',
|
||||||
|
notes: $row.data('notes') || '',
|
||||||
|
sort_order: $row.data('sort-order') || 0,
|
||||||
|
status: $row.data('status') || 'active',
|
||||||
|
in_use: parseInt($row.data('in-use') || 0, 10)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function openModal(data) {
|
function openModal(data) {
|
||||||
const isEdit = !!data;
|
const isEdit = !!data;
|
||||||
|
|
||||||
@@ -356,6 +657,13 @@ $functions = $pdo->query("
|
|||||||
$('#functionId').val(isEdit ? data.id : '');
|
$('#functionId').val(isEdit ? data.id : '');
|
||||||
$('#functionName').val(isEdit ? data.name : '');
|
$('#functionName').val(isEdit ? data.name : '');
|
||||||
$('#functionDescription').val(isEdit ? data.description : '');
|
$('#functionDescription').val(isEdit ? data.description : '');
|
||||||
|
$('#functionPersonFullName').val(isEdit ? data.person_full_name : '');
|
||||||
|
$('#functionPhone').val(isEdit ? data.phone : '');
|
||||||
|
$('#functionEmail').val(isEdit ? data.email : '');
|
||||||
|
$('#functionNotes').val(isEdit ? data.notes : '');
|
||||||
|
$('#functionSortOrder').val(isEdit ? data.sort_order : 0);
|
||||||
|
$('#functionStatus').val(isEdit ? data.status : 'active');
|
||||||
|
$('#functionSaveBtn').prop('disabled', false).html('Salva');
|
||||||
|
|
||||||
new bootstrap.Modal('#functionModal').show();
|
new bootstrap.Modal('#functionModal').show();
|
||||||
}
|
}
|
||||||
@@ -366,30 +674,24 @@ $functions = $pdo->query("
|
|||||||
|
|
||||||
$('#functionsList').on('click', '.btn-edit', function() {
|
$('#functionsList').on('click', '.btn-edit', function() {
|
||||||
const $row = $(this).closest('[data-id]');
|
const $row = $(this).closest('[data-id]');
|
||||||
|
openModal(getRowData($row));
|
||||||
openModal({
|
|
||||||
id: $row.data('id'),
|
|
||||||
name: $row.data('name'),
|
|
||||||
description: $row.data('description')
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#functionsList').on('click', '.btn-delete', function() {
|
$('#functionsList').on('click', '.btn-delete', function() {
|
||||||
const $row = $(this).closest('[data-id]');
|
const $row = $(this).closest('[data-id]');
|
||||||
const inUse = parseInt($row.data('in-use') || 0, 10);
|
const data = getRowData($row);
|
||||||
const name = $row.data('name');
|
|
||||||
|
|
||||||
if (inUse > 0) {
|
if (data.in_use > 0) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
title: 'Impossibile eliminare',
|
title: 'Impossibile eliminare',
|
||||||
text: `La funzione "${name}" è utilizzata in ${inUse} scadenz${inUse === 1 ? 'a' : 'e'}.`
|
text: 'La funzione "' + data.name + '" è utilizzata in ' + data.in_use + ' scadenza/e.'
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: `Eliminare "${name}"?`,
|
title: 'Eliminare "' + data.name + '"?',
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonText: 'Elimina',
|
confirmButtonText: 'Elimina',
|
||||||
@@ -398,8 +700,10 @@ $functions = $pdo->query("
|
|||||||
}).then(function(result) {
|
}).then(function(result) {
|
||||||
if (!result.isConfirmed) return;
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
$.post('scadenzario/functions/ajax/delete_function.php', {
|
$.post(window.location.href.split('#')[0], {
|
||||||
id: $row.data('id')
|
ajax: '1',
|
||||||
|
action: 'delete',
|
||||||
|
id: data.id
|
||||||
})
|
})
|
||||||
.done(function(res) {
|
.done(function(res) {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
@@ -408,7 +712,7 @@ $functions = $pdo->query("
|
|||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Errore',
|
title: 'Errore',
|
||||||
text: res.message
|
text: res.message || 'Impossibile eliminare.'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -424,10 +728,19 @@ $functions = $pdo->query("
|
|||||||
$('#functionForm').on('submit', function(e) {
|
$('#functionForm').on('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
const $btn = $('#functionSaveBtn');
|
||||||
const payload = {
|
const payload = {
|
||||||
|
ajax: '1',
|
||||||
|
action: 'save',
|
||||||
id: $('#functionId').val(),
|
id: $('#functionId').val(),
|
||||||
name: $('#functionName').val().trim(),
|
name: $('#functionName').val().trim(),
|
||||||
description: $('#functionDescription').val().trim()
|
description: $('#functionDescription').val().trim(),
|
||||||
|
person_full_name: $('#functionPersonFullName').val().trim(),
|
||||||
|
phone: $('#functionPhone').val().trim(),
|
||||||
|
email: $('#functionEmail').val().trim(),
|
||||||
|
notes: $('#functionNotes').val().trim(),
|
||||||
|
sort_order: $('#functionSortOrder').val(),
|
||||||
|
status: $('#functionStatus').val()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!payload.name) {
|
if (!payload.name) {
|
||||||
@@ -438,19 +751,23 @@ $functions = $pdo->query("
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$.post('scadenzario/functions/ajax/save_function.php', payload)
|
$btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Salvataggio...');
|
||||||
|
|
||||||
|
$.post(window.location.href.split('#')[0], payload)
|
||||||
.done(function(res) {
|
.done(function(res) {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
|
$btn.prop('disabled', false).html('Salva');
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Errore',
|
title: 'Errore',
|
||||||
text: res.message
|
text: res.message || 'Impossibile salvare.'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.fail(function() {
|
.fail(function() {
|
||||||
|
$btn.prop('disabled', false).html('Salva');
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Errore di rete'
|
title: 'Errore di rete'
|
||||||
|
|||||||
@@ -51,6 +51,15 @@
|
|||||||
<i class="fa-solid fa-gear"></i>
|
<i class="fa-solid fa-gear"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="notify_function" name="notify_function" value="1">
|
||||||
|
<label class="form-check-label" for="notify_function">
|
||||||
|
Invia promemoria anche alla funzione selezionata
|
||||||
|
</label>
|
||||||
|
<div class="form-text">
|
||||||
|
Se attivo, la mail giornaliera verrà inviata anche all’email collegata alla funzione.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<label for="dlLaw" class="form-label fw-semibold">Legge / Articolo</label>
|
<label for="dlLaw" class="form-label fw-semibold">Legge / Articolo</label>
|
||||||
@@ -96,7 +105,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Group 3: Responsabili -->
|
<!-- Group 3: Responsabili -->
|
||||||
<div class="form-section-title">Responsabili</div>
|
<div class="form-section-title">Esecutore</div>
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label for="dlDepartments" class="form-label fw-semibold">Reparti</label>
|
<label for="dlDepartments" class="form-label fw-semibold">Reparti</label>
|
||||||
@@ -137,6 +146,22 @@
|
|||||||
<label for="dlNotes" class="form-label fw-semibold">Note</label>
|
<label for="dlNotes" class="form-label fw-semibold">Note</label>
|
||||||
<textarea class="form-control" id="dlNotes" name="notes" rows="3" placeholder="es. Scadenza 09/06/2026, Attività in appalto a Ditta specializzata..."></textarea>
|
<textarea class="form-control" id="dlNotes" name="notes" rows="3" placeholder="es. Scadenza 09/06/2026, Attività in appalto a Ditta specializzata..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="dlVariazioni" name="variazioni" value="1">
|
||||||
|
<label class="form-check-label fw-semibold" for="dlVariazioni">
|
||||||
|
In caso di variazioni
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="dlAttivaAlert" name="attiva_alert" value="1">
|
||||||
|
<label class="form-check-label fw-semibold" for="dlAttivaAlert">
|
||||||
|
<i class="fa-solid fa-bell me-1 text-warning"></i> Attiva alert
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Group 5: Allegati -->
|
<!-- Group 5: Allegati -->
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user