Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55d83c12b0 | ||
|
|
41c4838dab | ||
|
|
35e57c303f | ||
|
|
e0fbd26207 | ||
|
|
b430c798dc | ||
|
|
135519ae5e | ||
|
|
d564561a4e | ||
|
|
0427c73e8a | ||
|
|
151e3bbe95 | ||
|
|
c11adf19e8 | ||
|
|
6d60f89be2 | ||
|
|
f2cb6343f7 | ||
|
|
58af735082 | ||
|
|
4e61426be7 | ||
|
|
1ef6cca149 | ||
|
|
4fc2828aaf | ||
|
|
db6e73bde5 | ||
|
|
376252e263 | ||
|
|
c675eb766e | ||
|
|
27cbc9f449 | ||
|
|
4c09a0dcb4 | ||
|
|
8bb23ee563 | ||
|
|
20571c9e4b | ||
|
|
fdde16b113 | ||
|
|
33b627f328 | ||
|
|
d96b4be9e0 | ||
|
|
088e518db1 | ||
|
|
789c547bc7 | ||
|
|
e5bf546ae7 | ||
|
|
6dd13e5d7d | ||
|
|
b1f2bb60e3 | ||
|
|
f7e97f55e9 | ||
|
|
70b712ff3b |
+7
-1
@@ -46,6 +46,7 @@ public/userarea/last_url.txt
|
||||
public/userarea/class/curl_auth_debug.log
|
||||
public/userarea/class/curl_request_debug.log
|
||||
|
||||
public/userarea/uploads/cad_area/originals/*
|
||||
# Ignora tutti i log
|
||||
*.log
|
||||
|
||||
@@ -68,4 +69,9 @@ public/userarea/logsapi/commessaweb_invia_763.json
|
||||
public/userarea/logsapi/last_auth_url.txt
|
||||
|
||||
# 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');
|
||||
} elseif ($user->hasRole('HR')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('employee-hr')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('SuperUser')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('Management')) {
|
||||
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')) {
|
||||
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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,38 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
include('../../include/headscript.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 DPI non valido.']);
|
||||
exit;
|
||||
}
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM employee_ppe WHERE id = :id");
|
||||
$stmt->execute(['id' => $id]);
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
$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,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,86 @@
|
||||
<?php
|
||||
/**
|
||||
* Bulk-assign a single DPI (PPE) item to several employees at once:
|
||||
* one employee_ppe row per selected employee, all sharing the same
|
||||
* item name / delivery date / delivered-by / notes.
|
||||
* Mirrors ajax/trainings/save_bulk_training.php. HR-only.
|
||||
*/
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// $pdo and $currentUserId from hr_auth_check.php
|
||||
|
||||
$itemName = trim($_POST['item_name'] ?? '');
|
||||
$deliveryDate = trim($_POST['delivery_date'] ?? '');
|
||||
$deliveredBy = trim($_POST['delivered_by'] ?? '');
|
||||
$notes = trim($_POST['notes'] ?? '');
|
||||
$employeeIds = $_POST['employee_ids'] ?? [];
|
||||
|
||||
if (!is_array($employeeIds)) {
|
||||
$employeeIds = [];
|
||||
}
|
||||
$employeeIds = array_values(array_unique(array_filter(array_map('intval', $employeeIds), fn($v) => $v > 0)));
|
||||
|
||||
if ($itemName === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Il nome del DPI è obbligatorio.']);
|
||||
exit;
|
||||
}
|
||||
if ($deliveryDate !== '' && !DateTime::createFromFormat('Y-m-d', $deliveryDate)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Data di consegna non valida.']);
|
||||
exit;
|
||||
}
|
||||
if (empty($employeeIds)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un dipendente.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$deliveryDate = $deliveryDate === '' ? null : $deliveryDate;
|
||||
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
|
||||
$notes = $notes !== '' ? $notes : null;
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Only insert for employees that actually exist
|
||||
$checkEmp = $pdo->prepare("SELECT id FROM employees WHERE id = :id");
|
||||
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO employee_ppe
|
||||
(employee_id, item_name, delivery_date, delivered_by, notes, created_by, created_at, updated_at)
|
||||
VALUES
|
||||
(:employee_id, :item_name, :delivery_date, :delivered_by, :notes, :created_by, NOW(), NOW())
|
||||
");
|
||||
|
||||
$created = 0;
|
||||
foreach ($employeeIds as $eid) {
|
||||
$checkEmp->execute(['id' => $eid]);
|
||||
if (!$checkEmp->fetchColumn()) {
|
||||
continue;
|
||||
}
|
||||
$ins->execute([
|
||||
'employee_id' => $eid,
|
||||
'item_name' => $itemName,
|
||||
'delivery_date' => $deliveryDate,
|
||||
'delivered_by' => $deliveredBy,
|
||||
'notes' => $notes,
|
||||
'created_by' => $currentUserId,
|
||||
]);
|
||||
$created++;
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'created' => $created,
|
||||
'message' => 'DPI assegnato a ' . $created . ' dipendent' . ($created === 1 ? 'e' : 'i') . '.',
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -1,82 +1,153 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
include('../../include/headscript.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);
|
||||
$itemName = trim($_POST['item_name'] ?? '');
|
||||
$deliveryDate = trim($_POST['delivery_date'] ?? '');
|
||||
$deliveredBy = trim($_POST['delivered_by'] ?? '');
|
||||
$notes = trim($_POST['notes'] ?? '');
|
||||
|
||||
if ($employeeId <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
|
||||
exit;
|
||||
}
|
||||
if ($itemName === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Il nome del DPI è obbligatorio.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$deliveryDate = $deliveryDate === '' ? null : $deliveryDate;
|
||||
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
|
||||
$notes = $notes !== '' ? $notes : null;
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
if ($id > 0) {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE employee_ppe
|
||||
SET item_name = :item_name,
|
||||
delivery_date = :delivery_date,
|
||||
delivered_by = :delivered_by,
|
||||
notes = :notes,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id AND employee_id = :eid
|
||||
");
|
||||
$stmt->execute([
|
||||
'item_name' => $itemName,
|
||||
'delivery_date' => $deliveryDate,
|
||||
'delivered_by' => $deliveredBy,
|
||||
'notes' => $notes,
|
||||
'id' => $id,
|
||||
'eid' => $employeeId,
|
||||
$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.'
|
||||
]);
|
||||
echo json_encode(['success' => true, 'id' => $id]);
|
||||
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.']);
|
||||
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
|
||||
(employee_id, item_name, delivery_date, delivered_by, notes, created_by, created_at, updated_at)
|
||||
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, :item_name, :delivery_date, :delivered_by, :notes, :created_by, NOW(), NOW())
|
||||
(
|
||||
:employee_id,
|
||||
:ppe_item_id,
|
||||
:assigned_date,
|
||||
:expiry_date,
|
||||
:delivered_by,
|
||||
1,
|
||||
:status,
|
||||
:notes,
|
||||
:created_by,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
'employee_id' => $employeeId,
|
||||
'item_name' => $itemName,
|
||||
'delivery_date' => $deliveryDate,
|
||||
'delivered_by' => $deliveredBy,
|
||||
'notes' => $notes,
|
||||
'created_by' => $currentUserId,
|
||||
'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, 'id' => (int)$pdo->lastInsertId()]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'DPI assegnato.'
|
||||
]);
|
||||
exit;
|
||||
} catch (Throwable $e) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?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');
|
||||
|
||||
@@ -24,16 +25,58 @@ $is_mandatory = isset($_POST['is_mandatory']) && (int)$_POST['is_mandatory'] ===
|
||||
$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;
|
||||
}
|
||||
@@ -61,13 +104,27 @@ try {
|
||||
'id' => $id,
|
||||
]);
|
||||
|
||||
echo json_encode(['success' => true, '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;
|
||||
}
|
||||
@@ -88,7 +145,14 @@ try {
|
||||
'is_mandatory' => $is_mandatory,
|
||||
]);
|
||||
|
||||
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
|
||||
$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()]);
|
||||
}
|
||||
|
||||
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,50 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID mescola non valido']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
// Blocca la cancellazione se ci sono dati collegati: meglio disattivare
|
||||
$chk = $pdo->prepare("SELECT COUNT(*) FROM mescole_supplier_lots WHERE idmescola = ?");
|
||||
$chk->execute([$id]);
|
||||
$nLotti = (int)$chk->fetchColumn();
|
||||
|
||||
$chk = $pdo->prepare("SELECT COUNT(*) FROM mescole_files WHERE idmescola = ?");
|
||||
$chk->execute([$id]);
|
||||
$nFile = (int)$chk->fetchColumn();
|
||||
|
||||
if ($nLotti > 0 || $nFile > 0) {
|
||||
$parti = [];
|
||||
if ($nLotti > 0) $parti[] = $nLotti . ' lotto/i fornitore';
|
||||
if ($nFile > 0) $parti[] = $nFile . ' file';
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Impossibile eliminare: la mescola ha ' . implode(' e ', $parti)
|
||||
. '. Rimuovili prima, oppure disattiva la mescola.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Le associazioni con le linee non sono dati storici: si possono rimuovere
|
||||
$pdo->prepare("DELETE FROM mescole_lines WHERE idmescola = ?")->execute([$id]);
|
||||
$pdo->prepare("DELETE FROM mescole WHERE id = ?")->execute([$id]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Throwable $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
echo json_encode(['success' => false, 'message' => 'Errore database: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT idmescola, filename FROM mescole_files WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
echo json_encode(['success' => false, 'message' => 'File non trovato']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$del = $pdo->prepare("DELETE FROM mescole_files WHERE id = ?");
|
||||
$del->execute([$id]);
|
||||
|
||||
$path = __DIR__ . '/uploads/mescole/' . $row['idmescola'] . '/' . $row['filename'];
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Throwable $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore database: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -6,40 +6,37 @@ header('Content-Type: application/json');
|
||||
|
||||
require_once 'include/headscript.php';
|
||||
|
||||
/**
|
||||
* Soft-deletes a tool: it is marked as decommissioned, not removed, because
|
||||
* production records reference it (productiondata_tools.tool_id).
|
||||
*
|
||||
* production_tools is now the unified registry inv_equipment.
|
||||
*
|
||||
* Note: the page calls this endpoint with GET ?id=..., while the previous
|
||||
* version only read $_POST['id'] and required a name — so deletion always
|
||||
* failed with "Invalid ID.". Both methods are accepted now.
|
||||
*/
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$tool_type = trim($_POST['tool_type'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 1;
|
||||
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid ID.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($name === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Name is required.']);
|
||||
$stmt = $pdo->prepare("SELECT id FROM inv_equipment WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
if (!$stmt->fetchColumn()) {
|
||||
echo json_encode(['success' => false, 'message' => 'Strumento non trovato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = "UPDATE production_tools
|
||||
SET name = :name,
|
||||
tool_type = :tool_type,
|
||||
description = :description,
|
||||
is_active = :is_active
|
||||
WHERE id = :id";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
'name' => $name,
|
||||
'tool_type' => $tool_type ?: null,
|
||||
'description' => $description ?: null,
|
||||
'is_active' => $is_active,
|
||||
'id' => $id
|
||||
]);
|
||||
$stmt = $pdo->prepare("UPDATE inv_equipment SET status = 'decommissioned' WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
|
||||
@@ -17,7 +17,7 @@ try {
|
||||
$toolType = trim($_POST['tool_type'] ?? '');
|
||||
$manufacturer = trim($_POST['manufacturer'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$isActive = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 1;
|
||||
$status = (string)($_POST['status'] ?? 'active');
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid ID.']);
|
||||
@@ -29,14 +29,19 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = "UPDATE production_tools
|
||||
if (!in_array($status, ['active', 'out_of_service', 'decommissioned'], true)) {
|
||||
$status = 'active';
|
||||
}
|
||||
|
||||
// production_tools is now inv_equipment (see manutenzioni/sql)
|
||||
$sql = "UPDATE inv_equipment
|
||||
SET name = :name,
|
||||
registration_number = :registration_number,
|
||||
serial_number = :serial_number,
|
||||
tool_type = :tool_type,
|
||||
manufacturer = :manufacturer,
|
||||
description = :description,
|
||||
is_active = :is_active
|
||||
status = :status
|
||||
WHERE id = :id";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
@@ -47,7 +52,7 @@ try {
|
||||
'tool_type' => $toolType ?: null,
|
||||
'manufacturer' => $manufacturer ?: null,
|
||||
'description' => $description ?: null,
|
||||
'is_active' => $isActive,
|
||||
'status' => $status,
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1580
-445
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()]);
|
||||
}
|
||||
@@ -289,6 +289,16 @@
|
||||
<i class='bx bx-radio-circle'></i>Dipendenti
|
||||
</a>
|
||||
</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 if (userCan('hr.departments.view')) : ?>
|
||||
@@ -299,33 +309,15 @@
|
||||
</li>
|
||||
<?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')) : ?>
|
||||
<li>
|
||||
<a href="trainings.php">
|
||||
<i class='bx bx-radio-circle'></i>Storico Formazione
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="training_calendar.php">
|
||||
<i class='bx bx-radio-circle'></i>Calendario Formazione
|
||||
<i class='bx bx-radio-circle'></i>Gestione Formazione
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('hr.skills.view')) : ?>
|
||||
@@ -361,11 +353,91 @@
|
||||
<i class='bx bx-radio-circle'></i>Calendario
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="scadenzario/functions/index.php">
|
||||
<i class='bx bx-radio-circle'></i>Funzioni Aziendali
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
$canSeeMaintenance =
|
||||
userCan('maintenance.equipment.view')
|
||||
|| userCan('maintenance.maintenances.view')
|
||||
|| userCan('maintenance.calendar.view')
|
||||
|| userCan('maintenance.categories.view');
|
||||
?>
|
||||
|
||||
<?php if ($canSeeMaintenance) : ?>
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-wrench"></i>
|
||||
</div>
|
||||
<div class="menu-title">Manutenzioni</div>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<?php if (userCan('maintenance.equipment.view')) : ?>
|
||||
<li>
|
||||
<a href="manutenzioni/index.php">
|
||||
<i class='bx bx-radio-circle'></i>Registro Attrezzature
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('maintenance.maintenances.view')) : ?>
|
||||
<li>
|
||||
<a href="manutenzioni/maintenances.php">
|
||||
<i class='bx bx-radio-circle'></i>Manutenzioni
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('maintenance.calendar.view')) : ?>
|
||||
<li>
|
||||
<a href="manutenzioni/calendar.php">
|
||||
<i class='bx bx-radio-circle'></i>Calendario
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<li>
|
||||
<a href="manutenzioni/calendar_global.php">
|
||||
<i class='bx bx-radio-circle'></i>Calendario Cumulativo
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<?php if (userCan('maintenance.categories.view')) : ?>
|
||||
<li>
|
||||
<a href="manutenzioni/categories/index.php">
|
||||
<i class='bx bx-radio-circle'></i>Categorie
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<li>
|
||||
<a href="javascript:;" class="has-arrow">
|
||||
<div class="parent-icon">
|
||||
<i class="bx bx-ruler"></i>
|
||||
</div>
|
||||
<div class="menu-title">Misure</div>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="cad-area.php">
|
||||
<i class='bx bx-radio-circle'></i>Cad Area
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="menu-label">Others</li>
|
||||
|
||||
<li>
|
||||
@@ -393,4 +465,4 @@
|
||||
|
||||
</ul>
|
||||
<!--end navigation-->
|
||||
</div>
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -496,14 +496,41 @@ function h($v)
|
||||
}
|
||||
});
|
||||
|
||||
// Select2 for category filter + modal selects
|
||||
$('#categoryFilter, #add_category, #edit_category').select2({
|
||||
// Select2 solo per il filtro categoria (fuori dai modali)
|
||||
$('#categoryFilter').select2({
|
||||
theme: 'bootstrap4',
|
||||
width: '100%',
|
||||
placeholder: '-- Seleziona --',
|
||||
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
|
||||
$('#categoryFilter').on('change', function() {
|
||||
const v = $(this).val() || '';
|
||||
|
||||
@@ -365,18 +365,13 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
}
|
||||
}
|
||||
|
||||
// Load packaging items from dedicated table
|
||||
$packagingLookup = [
|
||||
'PACKAGING_TYPE' => [],
|
||||
'BOX' => [],
|
||||
'PALLET' => []
|
||||
];
|
||||
// Load ALL packaging items (tutte le categorie, distinct via GROUP nel dropdown)
|
||||
$packagingByCategory = []; // categoria => [ {value, label}, ... ]
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT category, item_name, item_code
|
||||
SELECT id, category, item_name, item_code
|
||||
FROM packaging_items
|
||||
WHERE is_active = 1
|
||||
AND category IN ('PACKAGING_TYPE', 'BOX', 'PALLET')
|
||||
ORDER BY category ASC, item_name ASC
|
||||
");
|
||||
$stmt->execute();
|
||||
@@ -387,12 +382,20 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$label .= ' — ' . $r['item_code'];
|
||||
}
|
||||
|
||||
$packagingLookup[$r['category']][] = [
|
||||
'value' => $r['item_code'],
|
||||
$packagingByCategory[$r['category']][] = [
|
||||
'value' => (int)$r['id'], // salviamo l'ID
|
||||
'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
|
||||
$mescole = $pdo->query("
|
||||
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
|
||||
function h($v)
|
||||
{
|
||||
@@ -1045,14 +1061,28 @@ $isEdit = ($worksheet_id > 0);
|
||||
<div class="soft-badge">Packaging</div>
|
||||
</div>
|
||||
<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="col-lg-4">
|
||||
<label class="form-label">Codice confezione</label>
|
||||
<select class="form-select" name="requested_package_code" id="requested_package_code">
|
||||
<?= packaging_options($packagingLookup['PACKAGING_TYPE'] ?? [], $worksheet['requested_package_code'] ?? '') ?>
|
||||
<label class="form-label">Categoria imballo</label>
|
||||
<select class="form-select" id="packaging_category">
|
||||
<option value="">-- Seleziona --</option>
|
||||
<?php foreach ($packagingCategories as $cat): ?>
|
||||
<option value="<?= h($cat) ?>" <?= $savedPackageCategory === $cat ? 'selected' : '' ?>>
|
||||
<?= h($cat) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</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 class="col-lg-4 col-md-6">
|
||||
@@ -1074,13 +1104,6 @@ $isEdit = ($worksheet_id > 0);
|
||||
value="<?= $worksheet ? h($worksheet['meters_per_package_notes']) : '' ?>">
|
||||
</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">
|
||||
<label class="form-label">N° conf./pezzi per scatola</label>
|
||||
<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']) : '' ?>">
|
||||
</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">
|
||||
<label class="form-label">Scatole/conf. per bancale</label>
|
||||
<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() {
|
||||
if (!$('#idmescola').data('select2')) {
|
||||
$('#idmescola').select2({
|
||||
@@ -1457,6 +1506,11 @@ $isEdit = ($worksheet_id > 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Se stiamo modificando una riga esistente, non resettare il form
|
||||
if (parseInt($('#mixRowId').val() || '0', 10) > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('#mixModalTitle').text('Aggiungi Mescola');
|
||||
$('#mixForm')[0].reset();
|
||||
$('#mixRowId').val('0');
|
||||
@@ -1470,8 +1524,8 @@ $isEdit = ($worksheet_id > 0);
|
||||
let row = $(this).attr('data-row');
|
||||
row = JSON.parse(row);
|
||||
|
||||
$('#mixModalTitle').text('Modifica Mescola');
|
||||
$('#mixForm')[0].reset();
|
||||
$('#mixModalTitle').text('Modifica Mescola');
|
||||
|
||||
$('#mixRowId').val(row.id);
|
||||
$('#mixWorksheetId').val(row.worksheet_id);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
```bash
|
||||
# 1. database — nell'ordine, tutti idempotenti
|
||||
cd public/userarea/manutenzioni/sql
|
||||
for f in 1_rename_production_tools 2_create_tables 3_seed_and_link 4_permissions; do
|
||||
mysql -u <user> -p <database> < $f.sql
|
||||
done
|
||||
|
||||
# 2. dati reali del cliente dagli XLS (facoltativo)
|
||||
mysql -u <user> -p <database> < 5_import_from_xls.sql
|
||||
|
||||
# 3. cartella allegati scrivibile
|
||||
chmod 755 ../attachments && chown www-data:www-data ../attachments
|
||||
|
||||
# 4. cron giornaliero delle notifiche
|
||||
echo '0 7 * * * php /var/www/html/public/userarea/manutenzioni/cron/send_notifications.php' | crontab -
|
||||
```
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Auth + permission guard for the Manutenzioni ajax endpoints.
|
||||
* Include at the top of every handler; it defines $currentUserId.
|
||||
*
|
||||
* Authorisation is enforced here, on the backend — the UI only hides
|
||||
* buttons, it never decides what a request is allowed to do.
|
||||
*/
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (empty($_SESSION['iduserlogin'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'message' => 'Non autorizzato. Effettua il login.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$currentUserId = (int)$_SESSION['iduserlogin'];
|
||||
|
||||
require_once __DIR__ . '/../include/functions.php';
|
||||
|
||||
if (!function_exists('mnt_user_can')) {
|
||||
/**
|
||||
* Permission check straight from the Vanguard RBAC tables
|
||||
* (the Auth facade is not bootstrapped in these endpoints).
|
||||
*/
|
||||
function mnt_user_can(string $permission): bool
|
||||
{
|
||||
global $currentUserId;
|
||||
|
||||
static $permissions = null;
|
||||
|
||||
if ($permissions === null) {
|
||||
$stmt = mnt_pdo()->prepare("
|
||||
SELECT p.name
|
||||
FROM auth_users u
|
||||
INNER JOIN auth_permission_role pr ON pr.role_id = u.role_id
|
||||
INNER JOIN auth_permissions p ON p.id = pr.permission_id
|
||||
WHERE u.id = ?
|
||||
");
|
||||
$stmt->execute([$currentUserId]);
|
||||
$permissions = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
return in_array($permission, $permissions, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('mnt_require_permission')) {
|
||||
function mnt_require_permission(string $permission): void
|
||||
{
|
||||
if (!mnt_user_can($permission)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'message' => 'Permesso negato.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('mnt_json_fail')) {
|
||||
function mnt_json_fail(string $message, int $code = 200): void
|
||||
{
|
||||
if ($code !== 200) {
|
||||
http_response_code($code);
|
||||
}
|
||||
echo json_encode(['success' => false, 'message' => $message]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('mnt_json_ok')) {
|
||||
function mnt_json_ok(array $payload = []): void
|
||||
{
|
||||
echo json_encode(['success' => true] + $payload);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.manage');
|
||||
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
mnt_json_fail('ID non valido.');
|
||||
}
|
||||
|
||||
// The FK is ON DELETE SET NULL, so a delete would silently orphan items:
|
||||
// refuse while the category is still in use.
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM inv_equipment WHERE category_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$inUse = (int)$stmt->fetchColumn();
|
||||
|
||||
if ($inUse > 0) {
|
||||
mnt_json_fail('Impossibile eliminare: la categoria è usata da ' . $inUse . ' attrezzature.');
|
||||
}
|
||||
|
||||
$pdo->prepare("DELETE FROM inv_categories WHERE id = ?")->execute([$id]);
|
||||
|
||||
mnt_json_ok();
|
||||
} catch (Throwable $e) {
|
||||
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.manage');
|
||||
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
mnt_json_fail('ID non valido.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id, name FROM inv_equipment WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$equipment = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$equipment) {
|
||||
mnt_json_fail('Attrezzatura non trovata.');
|
||||
}
|
||||
|
||||
// Legacy production records reference tools without ON DELETE CASCADE:
|
||||
// refuse with a clear message instead of letting the FK throw.
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM productiondata_tools WHERE tool_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$usedInProduction = (int)$stmt->fetchColumn();
|
||||
|
||||
if ($usedInProduction > 0) {
|
||||
mnt_json_fail(
|
||||
'Impossibile eliminare: l\'attrezzatura è usata in ' . $usedInProduction .
|
||||
' registrazioni di produzione. Impostala su "Dismesso" per escluderla dalle pianificazioni.'
|
||||
);
|
||||
}
|
||||
|
||||
// Collect stored files before the cascade removes their rows
|
||||
$storedNames = [];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT stored_name FROM inv_equipment_files WHERE equipment_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT f.stored_name
|
||||
FROM maint_maintenance_files f
|
||||
INNER JOIN maint_maintenances m ON m.id = f.maintenance_id
|
||||
WHERE m.equipment_id = ?
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT f.stored_name
|
||||
FROM maint_intervention_files f
|
||||
INNER JOIN maint_interventions i ON i.id = f.intervention_id
|
||||
WHERE i.equipment_id = ?
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
|
||||
|
||||
$stmt = $pdo->prepare("SELECT signature_path FROM maint_interventions WHERE equipment_id = ? AND signature_path IS NOT NULL");
|
||||
$stmt->execute([$id]);
|
||||
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
|
||||
|
||||
// cover_file_id points at inv_equipment_files, which cascades from the
|
||||
// equipment row: clear it first so the FK does not block the delete.
|
||||
$pdo->beginTransaction();
|
||||
$pdo->prepare("UPDATE inv_equipment SET cover_file_id = NULL WHERE id = ?")->execute([$id]);
|
||||
$pdo->prepare("DELETE FROM inv_equipment WHERE id = ?")->execute([$id]);
|
||||
$pdo->commit();
|
||||
|
||||
foreach (array_filter($storedNames) as $storedName) {
|
||||
mnt_delete_stored_file($storedName);
|
||||
}
|
||||
|
||||
mnt_log($pdo, 'equipment_deleted', null, null, null, $currentUserId, null, $equipment['name']);
|
||||
|
||||
mnt_json_ok();
|
||||
} catch (Throwable $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.manage');
|
||||
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$scope = (string)($_POST['scope'] ?? 'equipment');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
|
||||
$tables = [
|
||||
'equipment' => 'inv_equipment_files',
|
||||
'maintenance' => 'maint_maintenance_files',
|
||||
'intervention' => 'maint_intervention_files',
|
||||
];
|
||||
|
||||
if (!isset($tables[$scope])) {
|
||||
mnt_json_fail('Scope non valido.');
|
||||
}
|
||||
if ($id <= 0) {
|
||||
mnt_json_fail('ID non valido.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM {$tables[$scope]} WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$file = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$file) {
|
||||
mnt_json_fail('File non trovato.');
|
||||
}
|
||||
|
||||
// Clear the cover reference first — the FK would block the delete
|
||||
if ($scope === 'equipment') {
|
||||
$pdo->prepare("UPDATE inv_equipment SET cover_file_id = NULL WHERE cover_file_id = ?")->execute([$id]);
|
||||
}
|
||||
|
||||
$pdo->prepare("DELETE FROM {$tables[$scope]} WHERE id = ?")->execute([$id]);
|
||||
mnt_delete_stored_file((string)$file['stored_name']);
|
||||
|
||||
mnt_log(
|
||||
$pdo,
|
||||
'file_deleted',
|
||||
$scope === 'equipment' ? (int)$file['equipment_id'] : null,
|
||||
$scope === 'maintenance' ? (int)$file['maintenance_id'] : null,
|
||||
$scope === 'intervention' ? (int)$file['intervention_id'] : null,
|
||||
$currentUserId,
|
||||
null,
|
||||
(string)$file['original_name']
|
||||
);
|
||||
|
||||
mnt_json_ok();
|
||||
} catch (Throwable $e) {
|
||||
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.manage');
|
||||
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
mnt_json_fail('ID non valido.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM maint_interventions WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$intervention = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$intervention) {
|
||||
mnt_json_fail('Intervento non trovato.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT stored_name FROM maint_intervention_files WHERE intervention_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$storedNames = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (!empty($intervention['signature_path'])) {
|
||||
$storedNames[] = $intervention['signature_path'];
|
||||
}
|
||||
|
||||
$pdo->prepare("DELETE FROM maint_interventions WHERE id = ?")->execute([$id]);
|
||||
|
||||
foreach (array_filter($storedNames) as $storedName) {
|
||||
mnt_delete_stored_file($storedName);
|
||||
}
|
||||
|
||||
// Removing the last completed intervention must roll the due date back
|
||||
mnt_recalc_maintenance($pdo, (int)$intervention['maintenance_id']);
|
||||
|
||||
mnt_log(
|
||||
$pdo,
|
||||
'intervention_deleted',
|
||||
(int)$intervention['equipment_id'],
|
||||
(int)$intervention['maintenance_id'],
|
||||
null,
|
||||
$currentUserId,
|
||||
null,
|
||||
'Intervento del ' . $intervention['performed_at']
|
||||
);
|
||||
|
||||
mnt_json_ok();
|
||||
} catch (Throwable $e) {
|
||||
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.manage');
|
||||
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
mnt_json_fail('ID non valido.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id, equipment_id, title FROM maint_maintenances WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$maintenance = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$maintenance) {
|
||||
mnt_json_fail('Manutenzione non trovata.');
|
||||
}
|
||||
|
||||
// Files of the maintenance and of its interventions (both cascade in DB)
|
||||
$stmt = $pdo->prepare("SELECT stored_name FROM maint_maintenance_files WHERE maintenance_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$storedNames = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT f.stored_name
|
||||
FROM maint_intervention_files f
|
||||
INNER JOIN maint_interventions i ON i.id = f.intervention_id
|
||||
WHERE i.maintenance_id = ?
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
|
||||
|
||||
$stmt = $pdo->prepare("SELECT signature_path FROM maint_interventions WHERE maintenance_id = ? AND signature_path IS NOT NULL");
|
||||
$stmt->execute([$id]);
|
||||
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
|
||||
|
||||
$pdo->prepare("DELETE FROM maint_maintenances WHERE id = ?")->execute([$id]);
|
||||
|
||||
foreach (array_filter($storedNames) as $storedName) {
|
||||
mnt_delete_stored_file($storedName);
|
||||
}
|
||||
|
||||
mnt_log($pdo, 'maintenance_deleted', (int)$maintenance['equipment_id'], null, null, $currentUserId, null, $maintenance['title']);
|
||||
|
||||
mnt_json_ok();
|
||||
} catch (Throwable $e) {
|
||||
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Serves an attachment. The attachments/ folder itself is closed by
|
||||
* .htaccess — this is the only way to read a file, and it requires a
|
||||
* logged-in session.
|
||||
*
|
||||
* scope = equipment | maintenance | intervention | signature
|
||||
*/
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (empty($_SESSION['iduserlogin'])) {
|
||||
http_response_code(401);
|
||||
exit('Non autorizzato.');
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../include/functions.php';
|
||||
|
||||
$scope = (string)($_GET['scope'] ?? 'equipment');
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
http_response_code(400);
|
||||
exit('ID non valido.');
|
||||
}
|
||||
|
||||
$tables = [
|
||||
'equipment' => 'inv_equipment_files',
|
||||
'maintenance' => 'maint_maintenance_files',
|
||||
'intervention' => 'maint_intervention_files',
|
||||
];
|
||||
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
if ($scope === 'signature') {
|
||||
$stmt = $pdo->prepare("SELECT signature_path AS stored_name, 'firma.png' AS original_name, 'image/png' AS mime_type FROM maint_interventions WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
} elseif (isset($tables[$scope])) {
|
||||
$stmt = $pdo->prepare("SELECT stored_name, original_name, mime_type FROM {$tables[$scope]} WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
} else {
|
||||
http_response_code(400);
|
||||
exit('Scope non valido.');
|
||||
}
|
||||
|
||||
$file = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$file || empty($file['stored_name'])) {
|
||||
http_response_code(404);
|
||||
exit('File non trovato.');
|
||||
}
|
||||
|
||||
// basename() keeps a crafted stored_name from escaping the folder
|
||||
$path = __DIR__ . '/../attachments/' . basename((string)$file['stored_name']);
|
||||
|
||||
if (!is_file($path)) {
|
||||
http_response_code(404);
|
||||
exit('File non presente sul server.');
|
||||
}
|
||||
|
||||
$mimeType = $file['mime_type'] ?: 'application/octet-stream';
|
||||
$disposition = isset($_GET['download']) ? 'attachment' : 'inline';
|
||||
|
||||
header('Content-Type: ' . $mimeType);
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('Content-Disposition: ' . $disposition . '; filename="' . rawurlencode((string)$file['original_name']) . '"');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
readfile($path);
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.calendar.view');
|
||||
|
||||
/**
|
||||
* FullCalendar feed for maintenance due dates.
|
||||
* Optional filters: type=scheduled|extraordinary, category=<id>
|
||||
*/
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$start = (string)($_GET['start'] ?? date('Y-m-01', strtotime('-6 months')));
|
||||
$end = (string)($_GET['end'] ?? date('Y-m-t', strtotime('+12 months')));
|
||||
$type = (string)($_GET['type'] ?? '');
|
||||
$categoryId = (int)($_GET['category'] ?? 0);
|
||||
|
||||
// Le attrezzature dismesse o fuori servizio restano fuori dalle viste di
|
||||
// pianificazione: è a questo che serve lo stato (capitolato §1).
|
||||
$where = [
|
||||
"m.is_active = 1",
|
||||
"e.status = 'active'",
|
||||
"m.next_due_date IS NOT NULL",
|
||||
"m.next_due_date BETWEEN ? AND ?",
|
||||
];
|
||||
$params = [substr($start, 0, 10), substr($end, 0, 10)];
|
||||
|
||||
if (array_key_exists($type, mnt_intervention_types())) {
|
||||
$where[] = "m.intervention_type = ?";
|
||||
$params[] = $type;
|
||||
}
|
||||
if ($categoryId > 0) {
|
||||
$where[] = "e.category_id = ?";
|
||||
$params[] = $categoryId;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT m.id, m.title, m.code, m.next_due_date, m.alert_days, m.is_critical, m.intervention_type,
|
||||
e.id AS equipment_id, e.name AS equipment_name,
|
||||
c.color AS category_color
|
||||
FROM maint_maintenances m
|
||||
INNER JOIN inv_equipment e ON e.id = m.equipment_id
|
||||
LEFT JOIN inv_categories c ON c.id = e.category_id
|
||||
WHERE " . implode(' AND ', $where) . "
|
||||
ORDER BY m.next_due_date ASC
|
||||
");
|
||||
$stmt->execute($params);
|
||||
|
||||
$events = [];
|
||||
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$state = mnt_due_state($row['next_due_date'], (int)$row['alert_days']);
|
||||
|
||||
// Overdue/soon override the category colour so urgency wins visually
|
||||
$color = $state === 'overdue' ? '#dc3545' : ($state === 'due_soon' ? '#f0a202' : ($row['category_color'] ?: '#2f7d8f'));
|
||||
|
||||
$events[] = [
|
||||
'id' => (int)$row['id'],
|
||||
'title' => ($row['code'] ? $row['code'] . ' — ' : '') . $row['equipment_name'] . ': ' . $row['title'],
|
||||
'start' => $row['next_due_date'],
|
||||
'allDay' => true,
|
||||
'backgroundColor' => $color,
|
||||
'borderColor' => $color,
|
||||
'extendedProps' => [
|
||||
'equipmentId' => (int)$row['equipment_id'],
|
||||
'equipmentName' => $row['equipment_name'],
|
||||
'maintenanceTitle' => $row['title'],
|
||||
'isCritical' => (int)$row['is_critical'] === 1,
|
||||
'state' => $state,
|
||||
'type' => $row['intervention_type'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode($events);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.equipment.view');
|
||||
|
||||
try {
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
mnt_json_fail('ID non valido.');
|
||||
}
|
||||
|
||||
$stmt = mnt_pdo()->prepare("SELECT * FROM inv_equipment WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$equipment = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$equipment) {
|
||||
mnt_json_fail('Attrezzatura non trovata.');
|
||||
}
|
||||
|
||||
mnt_json_ok(['equipment' => $equipment]);
|
||||
} catch (Throwable $e) {
|
||||
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
/**
|
||||
* Cumulative calendar feed: deadlines + trainings + maintenances.
|
||||
* Each layer is returned only if the user may see that module, and only
|
||||
* if the layer is requested via ?layers=deadlines,trainings,maintenances
|
||||
*/
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$start = substr((string)($_GET['start'] ?? date('Y-m-01', strtotime('-6 months'))), 0, 10);
|
||||
$end = substr((string)($_GET['end'] ?? date('Y-m-t', strtotime('+12 months'))), 0, 10);
|
||||
|
||||
$requested = array_filter(explode(',', (string)($_GET['layers'] ?? 'deadlines,trainings,maintenances')));
|
||||
$wants = fn(string $layer): bool => in_array($layer, $requested, true);
|
||||
|
||||
$events = [];
|
||||
|
||||
// ---- Maintenances --------------------------------------------------
|
||||
if ($wants('maintenances') && mnt_user_can('maintenance.calendar.view')) {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT m.id, m.title, m.code, m.next_due_date, m.alert_days, m.is_critical,
|
||||
e.id AS equipment_id, e.name AS equipment_name
|
||||
FROM maint_maintenances m
|
||||
INNER JOIN inv_equipment e ON e.id = m.equipment_id
|
||||
WHERE m.is_active = 1
|
||||
AND e.status = 'active'
|
||||
AND m.next_due_date BETWEEN ? AND ?
|
||||
");
|
||||
$stmt->execute([$start, $end]);
|
||||
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$state = mnt_due_state($row['next_due_date'], (int)$row['alert_days']);
|
||||
$color = $state === 'overdue' ? '#dc3545' : '#2f7d8f';
|
||||
|
||||
$events[] = [
|
||||
'id' => 'maint-' . $row['id'],
|
||||
'title' => '🔧 ' . $row['equipment_name'] . ': ' . $row['title'],
|
||||
'start' => $row['next_due_date'],
|
||||
'allDay' => true,
|
||||
'backgroundColor' => $color,
|
||||
'borderColor' => $color,
|
||||
'extendedProps' => [
|
||||
'layer' => 'maintenances',
|
||||
'url' => 'manutenzioni/equipment.php?id=' . (int)$row['equipment_id'],
|
||||
'state' => $state,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Deadlines (scadenzario) --------------------------------------
|
||||
if ($wants('deadlines') && mnt_user_can('deadlines.view')) {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT d.id, d.topic, d.due_date, d.status, d.notification_days, s.name AS subject_name
|
||||
FROM scad_deadlines d
|
||||
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
||||
WHERE d.due_date BETWEEN ? AND ?
|
||||
");
|
||||
$stmt->execute([$start, $end]);
|
||||
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$isCompleted = $row['status'] === 'completed';
|
||||
$state = $isCompleted ? 'ok' : mnt_due_state($row['due_date'], (int)$row['notification_days']);
|
||||
$color = $isCompleted ? '#6c757d' : ($state === 'overdue' ? '#b02a37' : '#6f42c1');
|
||||
|
||||
$events[] = [
|
||||
'id' => 'scad-' . $row['id'],
|
||||
'title' => '📅 ' . $row['topic'],
|
||||
'start' => $row['due_date'],
|
||||
'allDay' => true,
|
||||
'backgroundColor' => $color,
|
||||
'borderColor' => $color,
|
||||
'extendedProps' => [
|
||||
'layer' => 'deadlines',
|
||||
'url' => 'scadenzario/detail.php?id=' . (int)$row['id'],
|
||||
'state' => $state,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Trainings (formazione) ---------------------------------------
|
||||
if ($wants('trainings') && mnt_user_can('hr.trainings.view')) {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT t.id, t.next_due_date, t.reminder_days,
|
||||
tt.name AS topic_name,
|
||||
CONCAT(e.first_name, ' ', e.last_name) AS employee_name
|
||||
FROM employee_trainings t
|
||||
INNER JOIN training_topics tt ON tt.id = t.training_topic_id
|
||||
INNER JOIN employees e ON e.id = t.employee_id
|
||||
WHERE t.next_due_date IS NOT NULL AND t.next_due_date BETWEEN ? AND ?
|
||||
");
|
||||
$stmt->execute([$start, $end]);
|
||||
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$state = mnt_due_state($row['next_due_date'], (int)($row['reminder_days'] ?? 0));
|
||||
$color = $state === 'overdue' ? '#a94442' : '#198754';
|
||||
|
||||
$events[] = [
|
||||
'id' => 'train-' . $row['id'],
|
||||
'title' => '🎓 ' . $row['topic_name'] . ' — ' . $row['employee_name'],
|
||||
'start' => $row['next_due_date'],
|
||||
'allDay' => true,
|
||||
'backgroundColor' => $color,
|
||||
'borderColor' => $color,
|
||||
'extendedProps' => [
|
||||
'layer' => 'trainings',
|
||||
'url' => 'trainings.php',
|
||||
'state' => $state,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($events);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.maintenances.view');
|
||||
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$maintenanceId = (int)($_GET['maintenance_id'] ?? 0);
|
||||
$interventionId = (int)($_GET['id'] ?? 0);
|
||||
|
||||
if ($interventionId > 0) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM maint_interventions WHERE id = ?");
|
||||
$stmt->execute([$interventionId]);
|
||||
$intervention = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$intervention) {
|
||||
mnt_json_fail('Intervento non trovato.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id, original_name FROM maint_intervention_files WHERE intervention_id = ?");
|
||||
$stmt->execute([$interventionId]);
|
||||
|
||||
mnt_json_ok([
|
||||
'intervention' => $intervention,
|
||||
'files' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($maintenanceId <= 0) {
|
||||
mnt_json_fail('ID non valido.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT i.*,
|
||||
CONCAT(e.first_name, ' ', e.last_name) AS operator_full_name,
|
||||
s.supplier_name,
|
||||
(SELECT COUNT(*) FROM maint_intervention_files f WHERE f.intervention_id = i.id) AS file_count
|
||||
FROM maint_interventions i
|
||||
LEFT JOIN employees e ON e.id = i.operator_employee_id
|
||||
LEFT JOIN suppliers s ON s.idsupplier = i.supplier_id
|
||||
WHERE i.maintenance_id = ?
|
||||
ORDER BY i.performed_at DESC, i.id DESC
|
||||
");
|
||||
$stmt->execute([$maintenanceId]);
|
||||
|
||||
mnt_json_ok(['interventions' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
|
||||
} catch (Throwable $e) {
|
||||
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.maintenances.view');
|
||||
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
mnt_json_fail('ID non valido.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT m.*, e.name AS equipment_name
|
||||
FROM maint_maintenances m
|
||||
INNER JOIN inv_equipment e ON e.id = m.equipment_id
|
||||
WHERE m.id = ?
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$maintenance = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$maintenance) {
|
||||
mnt_json_fail('Manutenzione non trovata.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, original_name, size
|
||||
FROM maint_maintenance_files
|
||||
WHERE maintenance_id = ?
|
||||
ORDER BY created_at ASC
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
mnt_json_ok([
|
||||
'maintenance' => $maintenance,
|
||||
'files' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.manage');
|
||||
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
$code = trim((string)($_POST['code'] ?? ''));
|
||||
$color = trim((string)($_POST['color'] ?? '#6c757d'));
|
||||
$requiresLine = !empty($_POST['requires_line']) ? 1 : 0;
|
||||
$sortOrder = (int)($_POST['sort_order'] ?? 999);
|
||||
$isActive = !empty($_POST['is_active']) ? 1 : 0;
|
||||
$description = trim((string)($_POST['description'] ?? ''));
|
||||
|
||||
if ($name === '') {
|
||||
mnt_json_fail('Il nome è obbligatorio.');
|
||||
}
|
||||
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
|
||||
$color = '#6c757d';
|
||||
}
|
||||
|
||||
// name is UNIQUE — check first so the user gets a readable message
|
||||
$stmt = $pdo->prepare("SELECT id FROM inv_categories WHERE name = ? AND id <> ?");
|
||||
$stmt->execute([$name, $id]);
|
||||
if ($stmt->fetchColumn()) {
|
||||
mnt_json_fail('Esiste già una categoria con questo nome.');
|
||||
}
|
||||
|
||||
if ($id > 0) {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE inv_categories
|
||||
SET name = ?, code = ?, color = ?, requires_line = ?, sort_order = ?, is_active = ?, description = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$name, $code ?: null, $color, $requiresLine, $sortOrder, $isActive, $description ?: null, $id]);
|
||||
|
||||
mnt_json_ok(['id' => $id]);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO inv_categories (name, code, color, requires_line, sort_order, is_active, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([$name, $code ?: null, $color, $requiresLine, $sortOrder, $isActive, $description ?: null]);
|
||||
|
||||
mnt_json_ok(['id' => (int)$pdo->lastInsertId()]);
|
||||
} catch (Throwable $e) {
|
||||
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.manage');
|
||||
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
$categoryId = (int)($_POST['category_id'] ?? 0);
|
||||
$lineId = (int)($_POST['line_id'] ?? 0);
|
||||
$registration = trim((string)($_POST['registration_number'] ?? ''));
|
||||
$serial = trim((string)($_POST['serial_number'] ?? ''));
|
||||
$batchLot = trim((string)($_POST['batch_lot'] ?? ''));
|
||||
$manufacturer = trim((string)($_POST['manufacturer'] ?? ''));
|
||||
$purchaseDate = trim((string)($_POST['purchase_date'] ?? ''));
|
||||
$commissioningDate = trim((string)($_POST['commissioning_date'] ?? ''));
|
||||
$toolType = trim((string)($_POST['tool_type'] ?? ''));
|
||||
$status = (string)($_POST['status'] ?? 'active');
|
||||
$departmentId = (int)($_POST['department_id'] ?? 0);
|
||||
$location = trim((string)($_POST['location'] ?? ''));
|
||||
$description = trim((string)($_POST['description'] ?? ''));
|
||||
$notes = trim((string)($_POST['notes'] ?? ''));
|
||||
|
||||
if ($name === '') {
|
||||
mnt_json_fail('Il nome è obbligatorio.');
|
||||
}
|
||||
if ($categoryId <= 0) {
|
||||
mnt_json_fail('La categoria è obbligatoria.');
|
||||
}
|
||||
if (!array_key_exists($status, mnt_equipment_statuses())) {
|
||||
mnt_json_fail('Stato non valido.');
|
||||
}
|
||||
|
||||
// A category flagged requires_line must carry a production line
|
||||
$stmt = $pdo->prepare("SELECT requires_line FROM inv_categories WHERE id = ?");
|
||||
$stmt->execute([$categoryId]);
|
||||
$requiresLine = $stmt->fetchColumn();
|
||||
|
||||
if ($requiresLine === false) {
|
||||
mnt_json_fail('Categoria inesistente.');
|
||||
}
|
||||
if ((int)$requiresLine === 1 && $lineId <= 0) {
|
||||
mnt_json_fail('Per questa categoria la linea di produzione è obbligatoria.');
|
||||
}
|
||||
|
||||
$values = [
|
||||
'name' => $name,
|
||||
'category_id' => $categoryId,
|
||||
'line_id' => $lineId ?: null,
|
||||
'registration_number' => $registration ?: null,
|
||||
'serial_number' => $serial ?: null,
|
||||
'batch_lot' => $batchLot ?: null,
|
||||
'manufacturer' => $manufacturer ?: null,
|
||||
'purchase_date' => $purchaseDate ?: null,
|
||||
'commissioning_date' => $commissioningDate ?: null,
|
||||
'tool_type' => $toolType ?: null,
|
||||
'status' => $status,
|
||||
'department_id' => $departmentId ?: null,
|
||||
'location' => $location ?: null,
|
||||
'description' => $description ?: null,
|
||||
'notes' => $notes ?: null,
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
$assignments = [];
|
||||
foreach (array_keys($values) as $column) {
|
||||
$assignments[] = "$column = :$column";
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE inv_equipment SET " . implode(', ', $assignments) . " WHERE id = :id");
|
||||
$stmt->execute($values + ['id' => $id]);
|
||||
|
||||
mnt_log($pdo, 'equipment_updated', $id, null, null, $currentUserId, $values);
|
||||
mnt_json_ok(['id' => $id]);
|
||||
}
|
||||
|
||||
$columns = array_keys($values);
|
||||
$placeholders = array_map(fn($column) => ':' . $column, $columns);
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO inv_equipment (" . implode(', ', $columns) . ", created_by)
|
||||
VALUES (" . implode(', ', $placeholders) . ", :created_by)"
|
||||
);
|
||||
$stmt->execute($values + ['created_by' => $currentUserId]);
|
||||
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
mnt_log($pdo, 'equipment_created', $newId, null, null, $currentUserId, $values);
|
||||
|
||||
mnt_json_ok(['id' => $newId]);
|
||||
} catch (Throwable $e) {
|
||||
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.manage');
|
||||
|
||||
/**
|
||||
* Registers (or updates) one intervention against a maintenance and
|
||||
* recomputes the maintenance schedule.
|
||||
*
|
||||
* The operator signature arrives as a data URL from the tablet canvas and
|
||||
* is stored as a PNG in attachments/.
|
||||
*/
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$maintenanceId = (int)($_POST['maintenance_id'] ?? 0);
|
||||
$performedAt = trim((string)($_POST['performed_at'] ?? ''));
|
||||
$status = (string)($_POST['status'] ?? 'completed');
|
||||
$result = trim((string)($_POST['result'] ?? ''));
|
||||
$notes = trim((string)($_POST['notes'] ?? ''));
|
||||
$materials = trim((string)($_POST['materials'] ?? ''));
|
||||
$operatorId = (int)($_POST['operator_employee_id'] ?? 0);
|
||||
$operatorName = trim((string)($_POST['operator_name'] ?? ''));
|
||||
$supplierId = (int)($_POST['supplier_id'] ?? 0);
|
||||
$signatureData = (string)($_POST['signature'] ?? '');
|
||||
$clearSignature = !empty($_POST['clear_signature']);
|
||||
|
||||
if ($performedAt === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $performedAt)) {
|
||||
mnt_json_fail('La data dell\'intervento è obbligatoria.');
|
||||
}
|
||||
|
||||
$validStatuses = mnt_lookup($pdo, 'maint_status', ['planned' => '', 'in_progress' => '', 'completed' => '']);
|
||||
if (!array_key_exists($status, $validStatuses)) {
|
||||
mnt_json_fail('Stato intervento non valido.');
|
||||
}
|
||||
|
||||
$validResults = mnt_lookup($pdo, 'maint_result', []);
|
||||
if ($result !== '' && $validResults && !array_key_exists($result, $validResults)) {
|
||||
mnt_json_fail('Esito intervento non valido.');
|
||||
}
|
||||
|
||||
// Existing row: maintenance is taken from the record, not from the client
|
||||
if ($id > 0) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM maint_interventions WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$existing) {
|
||||
mnt_json_fail('Intervento non trovato.');
|
||||
}
|
||||
|
||||
$maintenanceId = (int)$existing['maintenance_id'];
|
||||
$equipmentId = (int)$existing['equipment_id'];
|
||||
$signaturePath = $existing['signature_path'];
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT equipment_id FROM maint_maintenances WHERE id = ?");
|
||||
$stmt->execute([$maintenanceId]);
|
||||
$equipmentId = (int)$stmt->fetchColumn();
|
||||
|
||||
if (!$equipmentId) {
|
||||
mnt_json_fail('Manutenzione non trovata.');
|
||||
}
|
||||
|
||||
$signaturePath = null;
|
||||
}
|
||||
|
||||
// Signature handling
|
||||
if ($clearSignature && $signaturePath) {
|
||||
mnt_delete_stored_file((string)$signaturePath);
|
||||
$signaturePath = null;
|
||||
}
|
||||
|
||||
if ($signatureData !== '' && preg_match('#^data:image/png;base64,#', $signatureData)) {
|
||||
$binary = base64_decode(substr($signatureData, strlen('data:image/png;base64,')), true);
|
||||
|
||||
if ($binary === false || strlen($binary) > 2 * 1024 * 1024) {
|
||||
mnt_json_fail('Firma non valida.');
|
||||
}
|
||||
|
||||
$uploadDir = __DIR__ . '/../attachments/';
|
||||
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true) && !is_dir($uploadDir)) {
|
||||
mnt_json_fail('Cartella allegati non scrivibile.');
|
||||
}
|
||||
|
||||
$newSignature = uniqid('sig_', true) . '.png';
|
||||
$newSignature = preg_replace('/[^A-Za-z0-9._-]/', '_', $newSignature);
|
||||
|
||||
if (file_put_contents($uploadDir . $newSignature, $binary) === false) {
|
||||
mnt_json_fail('Impossibile salvare la firma.');
|
||||
}
|
||||
|
||||
if ($signaturePath) {
|
||||
mnt_delete_stored_file((string)$signaturePath);
|
||||
}
|
||||
|
||||
$signaturePath = $newSignature;
|
||||
}
|
||||
|
||||
$values = [
|
||||
'performed_at' => $performedAt,
|
||||
'status' => $status,
|
||||
'result' => $result ?: null,
|
||||
'notes' => $notes ?: null,
|
||||
'materials' => $materials ?: null,
|
||||
'operator_employee_id' => $operatorId ?: null,
|
||||
'operator_name' => $operatorName ?: null,
|
||||
'supplier_id' => $supplierId ?: null,
|
||||
'signature_path' => $signaturePath,
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
$assignments = [];
|
||||
foreach (array_keys($values) as $column) {
|
||||
$assignments[] = "$column = :$column";
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE maint_interventions SET " . implode(', ', $assignments) . " WHERE id = :id");
|
||||
$stmt->execute($values + ['id' => $id]);
|
||||
|
||||
$interventionId = $id;
|
||||
$action = 'intervention_updated';
|
||||
} else {
|
||||
$values['maintenance_id'] = $maintenanceId;
|
||||
$values['equipment_id'] = $equipmentId;
|
||||
$values['created_by'] = $currentUserId;
|
||||
|
||||
$columns = array_keys($values);
|
||||
$placeholders = array_map(fn($column) => ':' . $column, $columns);
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO maint_interventions (" . implode(', ', $columns) . ")
|
||||
VALUES (" . implode(', ', $placeholders) . ")"
|
||||
);
|
||||
$stmt->execute($values);
|
||||
|
||||
$interventionId = (int)$pdo->lastInsertId();
|
||||
$action = 'intervention_created';
|
||||
}
|
||||
|
||||
// Only completed interventions move the next due date; this call handles it
|
||||
mnt_recalc_maintenance($pdo, $maintenanceId);
|
||||
|
||||
mnt_log($pdo, $action, $equipmentId, $maintenanceId, $interventionId, $currentUserId, $values);
|
||||
|
||||
$stmt = $pdo->prepare("SELECT last_done_date, next_due_date FROM maint_maintenances WHERE id = ?");
|
||||
$stmt->execute([$maintenanceId]);
|
||||
$schedule = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
|
||||
mnt_json_ok(['id' => $interventionId, 'schedule' => $schedule]);
|
||||
} catch (Throwable $e) {
|
||||
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.manage');
|
||||
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$equipmentId = (int)($_POST['equipment_id'] ?? 0);
|
||||
$code = trim((string)($_POST['code'] ?? ''));
|
||||
$title = trim((string)($_POST['title'] ?? ''));
|
||||
$description = trim((string)($_POST['description'] ?? ''));
|
||||
$interventionType = (string)($_POST['intervention_type'] ?? 'scheduled');
|
||||
$executionType = (string)($_POST['execution_type'] ?? 'internal');
|
||||
$isCritical = !empty($_POST['is_critical']) ? 1 : 0;
|
||||
$frequencyUnit = (string)($_POST['frequency_unit'] ?? 'month');
|
||||
$frequencyValue = (int)($_POST['frequency_value'] ?? 0);
|
||||
$frequencyNote = trim((string)($_POST['frequency_note'] ?? ''));
|
||||
$alertDays = (int)($_POST['alert_days'] ?? 7);
|
||||
$assigneeId = (int)($_POST['assignee_employee_id'] ?? 0);
|
||||
$supervisorId = (int)($_POST['supervisor_employee_id'] ?? 0);
|
||||
$supplierId = (int)($_POST['supplier_id'] ?? 0);
|
||||
$isActive = isset($_POST['is_active']) ? (int)!empty($_POST['is_active']) : 1;
|
||||
$notes = trim((string)($_POST['notes'] ?? ''));
|
||||
|
||||
if ($title === '') {
|
||||
mnt_json_fail('Il titolo è obbligatorio.');
|
||||
}
|
||||
if (!array_key_exists($interventionType, mnt_intervention_types())) {
|
||||
mnt_json_fail('Tipo di intervento non valido.');
|
||||
}
|
||||
if (!array_key_exists($executionType, mnt_execution_types())) {
|
||||
mnt_json_fail('Tipo di esecuzione non valido.');
|
||||
}
|
||||
if (!array_key_exists($frequencyUnit, mnt_frequency_units())) {
|
||||
mnt_json_fail('Unità di frequenza non valida.');
|
||||
}
|
||||
if ($frequencyUnit !== 'on_demand' && $frequencyValue <= 0) {
|
||||
mnt_json_fail('Indica ogni quanto va eseguita la manutenzione, oppure scegli "Al bisogno".');
|
||||
}
|
||||
if ($executionType === 'external' && $supplierId <= 0) {
|
||||
mnt_json_fail('Per una manutenzione esterna indica il fornitore.');
|
||||
}
|
||||
|
||||
if ($frequencyUnit === 'on_demand') {
|
||||
$frequencyValue = null;
|
||||
}
|
||||
|
||||
$values = [
|
||||
'code' => $code ?: null,
|
||||
'title' => $title,
|
||||
'description' => $description ?: null,
|
||||
'intervention_type' => $interventionType,
|
||||
'execution_type' => $executionType,
|
||||
'is_critical' => $isCritical,
|
||||
'frequency_value' => $frequencyValue,
|
||||
'frequency_unit' => $frequencyUnit,
|
||||
'frequency_note' => $frequencyNote ?: null,
|
||||
'alert_days' => max(0, $alertDays),
|
||||
'assignee_employee_id' => $assigneeId ?: null,
|
||||
'supervisor_employee_id' => $supervisorId ?: null,
|
||||
'supplier_id' => $supplierId ?: null,
|
||||
'is_active' => $isActive,
|
||||
'notes' => $notes ?: null,
|
||||
];
|
||||
|
||||
if ($id > 0) {
|
||||
$stmt = $pdo->prepare("SELECT equipment_id FROM maint_maintenances WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$equipmentId = (int)$stmt->fetchColumn();
|
||||
|
||||
if (!$equipmentId) {
|
||||
mnt_json_fail('Manutenzione non trovata.');
|
||||
}
|
||||
|
||||
$assignments = [];
|
||||
foreach (array_keys($values) as $column) {
|
||||
$assignments[] = "$column = :$column";
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE maint_maintenances SET " . implode(', ', $assignments) . " WHERE id = :id");
|
||||
$stmt->execute($values + ['id' => $id]);
|
||||
|
||||
// frequency may have changed — the due date must follow
|
||||
mnt_recalc_maintenance($pdo, $id);
|
||||
mnt_log($pdo, 'maintenance_updated', $equipmentId, $id, null, $currentUserId, $values);
|
||||
|
||||
mnt_json_ok(['id' => $id]);
|
||||
}
|
||||
|
||||
if ($equipmentId <= 0) {
|
||||
mnt_json_fail('Attrezzatura non valida.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id FROM inv_equipment WHERE id = ?");
|
||||
$stmt->execute([$equipmentId]);
|
||||
if (!$stmt->fetchColumn()) {
|
||||
mnt_json_fail('Attrezzatura non trovata.');
|
||||
}
|
||||
|
||||
$values['equipment_id'] = $equipmentId;
|
||||
$values['created_by'] = $currentUserId;
|
||||
|
||||
$columns = array_keys($values);
|
||||
$placeholders = array_map(fn($column) => ':' . $column, $columns);
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO maint_maintenances (" . implode(', ', $columns) . ")
|
||||
VALUES (" . implode(', ', $placeholders) . ")"
|
||||
);
|
||||
$stmt->execute($values);
|
||||
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
mnt_log($pdo, 'maintenance_created', $equipmentId, $newId, null, $currentUserId, $values);
|
||||
|
||||
mnt_json_ok(['id' => $newId]);
|
||||
} catch (Throwable $e) {
|
||||
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.manage');
|
||||
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$fileId = (int)($_POST['file_id'] ?? 0);
|
||||
|
||||
if ($fileId <= 0) {
|
||||
mnt_json_fail('ID non valido.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT equipment_id, kind FROM inv_equipment_files WHERE id = ?");
|
||||
$stmt->execute([$fileId]);
|
||||
$file = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$file) {
|
||||
mnt_json_fail('Immagine non trovata.');
|
||||
}
|
||||
if ($file['kind'] !== 'photo') {
|
||||
mnt_json_fail('Solo una foto può essere usata come copertina.');
|
||||
}
|
||||
|
||||
$pdo->prepare("UPDATE inv_equipment SET cover_file_id = ? WHERE id = ?")
|
||||
->execute([$fileId, (int)$file['equipment_id']]);
|
||||
|
||||
mnt_log($pdo, 'cover_changed', (int)$file['equipment_id'], null, null, $currentUserId);
|
||||
|
||||
mnt_json_ok();
|
||||
} catch (Throwable $e) {
|
||||
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
mnt_require_permission('maintenance.manage');
|
||||
|
||||
/**
|
||||
* Multi-file upload for the three attachment tables.
|
||||
* scope = equipment | maintenance | intervention
|
||||
* kind = file | photo (equipment only; photos feed the gallery)
|
||||
*/
|
||||
try {
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$scope = (string)($_POST['scope'] ?? 'equipment');
|
||||
$ownerId = (int)($_POST['owner_id'] ?? 0);
|
||||
$kind = ($_POST['kind'] ?? 'file') === 'photo' ? 'photo' : 'file';
|
||||
|
||||
$config = [
|
||||
'equipment' => ['table' => 'inv_equipment_files', 'owner' => 'equipment_id', 'parent' => 'inv_equipment'],
|
||||
'maintenance' => ['table' => 'maint_maintenance_files', 'owner' => 'maintenance_id', 'parent' => 'maint_maintenances'],
|
||||
'intervention' => ['table' => 'maint_intervention_files', 'owner' => 'intervention_id', 'parent' => 'maint_interventions'],
|
||||
];
|
||||
|
||||
if (!isset($config[$scope])) {
|
||||
mnt_json_fail('Scope non valido.');
|
||||
}
|
||||
if ($ownerId <= 0) {
|
||||
mnt_json_fail('ID non valido.');
|
||||
}
|
||||
if (empty($_FILES['files']['name'][0])) {
|
||||
mnt_json_fail('Nessun file selezionato.');
|
||||
}
|
||||
|
||||
$target = $config[$scope];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id FROM {$target['parent']} WHERE id = ?");
|
||||
$stmt->execute([$ownerId]);
|
||||
if (!$stmt->fetchColumn()) {
|
||||
mnt_json_fail('Record collegato non trovato.');
|
||||
}
|
||||
|
||||
$columns = $scope === 'equipment'
|
||||
? "({$target['owner']}, kind, original_name, stored_name, mime_type, size, uploaded_by)"
|
||||
: "({$target['owner']}, original_name, stored_name, mime_type, size, uploaded_by)";
|
||||
$placeholders = $scope === 'equipment' ? '(?, ?, ?, ?, ?, ?, ?)' : '(?, ?, ?, ?, ?, ?)';
|
||||
|
||||
$insert = $pdo->prepare("INSERT INTO {$target['table']} $columns VALUES $placeholders");
|
||||
|
||||
$uploaded = [];
|
||||
$errors = [];
|
||||
$fileCount = count($_FILES['files']['name']);
|
||||
|
||||
for ($i = 0; $i < $fileCount; $i++) {
|
||||
$file = [
|
||||
'name' => $_FILES['files']['name'][$i],
|
||||
'type' => $_FILES['files']['type'][$i],
|
||||
'tmp_name' => $_FILES['files']['tmp_name'][$i],
|
||||
'error' => $_FILES['files']['error'][$i],
|
||||
'size' => $_FILES['files']['size'][$i],
|
||||
];
|
||||
|
||||
try {
|
||||
$stored = mnt_store_upload($file, $scope);
|
||||
} catch (RuntimeException $e) {
|
||||
$errors[] = $file['name'] . ': ' . $e->getMessage();
|
||||
continue;
|
||||
}
|
||||
|
||||
// A file uploaded as a photo but not actually an image is kept as a file
|
||||
$rowKind = ($kind === 'photo' && $stored['is_image']) ? 'photo' : 'file';
|
||||
|
||||
$values = $scope === 'equipment'
|
||||
? [$ownerId, $rowKind, $stored['original_name'], $stored['stored_name'], $stored['mime_type'], $stored['size'], $currentUserId]
|
||||
: [$ownerId, $stored['original_name'], $stored['stored_name'], $stored['mime_type'], $stored['size'], $currentUserId];
|
||||
|
||||
$insert->execute($values);
|
||||
$uploaded[] = ['id' => (int)$pdo->lastInsertId(), 'name' => $stored['original_name']];
|
||||
}
|
||||
|
||||
if (!$uploaded) {
|
||||
mnt_json_fail($errors ? implode('; ', $errors) : 'Nessun file caricato.');
|
||||
}
|
||||
|
||||
// The first photo of an item becomes its cover automatically
|
||||
if ($scope === 'equipment' && $kind === 'photo') {
|
||||
$stmt = $pdo->prepare("SELECT cover_file_id FROM inv_equipment WHERE id = ?");
|
||||
$stmt->execute([$ownerId]);
|
||||
if (!$stmt->fetchColumn()) {
|
||||
$pdo->prepare("UPDATE inv_equipment SET cover_file_id = ? WHERE id = ?")
|
||||
->execute([$uploaded[0]['id'], $ownerId]);
|
||||
}
|
||||
}
|
||||
|
||||
$equipmentId = $scope === 'equipment' ? $ownerId : null;
|
||||
$maintenanceId = $scope === 'maintenance' ? $ownerId : null;
|
||||
$interventionId = $scope === 'intervention' ? $ownerId : null;
|
||||
|
||||
mnt_log($pdo, 'file_added', $equipmentId, $maintenanceId, $interventionId, $currentUserId, null,
|
||||
implode(', ', array_column($uploaded, 'name')));
|
||||
|
||||
mnt_json_ok(['uploaded' => $uploaded, 'errors' => $errors]);
|
||||
} catch (Throwable $e) {
|
||||
mnt_json_fail('Errore: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/*!
|
||||
FullCalendar Core v6.1.9
|
||||
Docs & License: https://fullcalendar.io
|
||||
(c) 2023 Adam Shaw
|
||||
*/
|
||||
!function(e){"use strict";var t={code:"it",week:{dow:1,doy:4},buttonText:{prev:"Prec",next:"Succ",today:"Oggi",year:"Anno",month:"Mese",week:"Settimana",day:"Giorno",list:"Agenda"},weekText:"Sm",allDayText:"Tutto il giorno",moreLinkText:e=>"+altri "+e,noEventsText:"Non ci sono eventi da visualizzare"};FullCalendar.globalLocales.push(t)}();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
!.htaccess
|
||||
@@ -0,0 +1 @@
|
||||
Deny from all
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php include(__DIR__ . '/../include/headscript.php'); ?>
|
||||
<?php
|
||||
require_once __DIR__ . '/include/functions.php';
|
||||
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
if (!userCan('maintenance.calendar.view')) {
|
||||
http_response_code(403);
|
||||
exit('Permesso negato.');
|
||||
}
|
||||
|
||||
$formData = mnt_form_data($pdo);
|
||||
|
||||
$MNT_TITLE = 'Calendario manutenzioni';
|
||||
$MNT_PLUGINS = ['fullcalendar'];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<?php include __DIR__ . '/include/page_head.php'; ?>
|
||||
<style>
|
||||
#mntCalendar { --fc-border-color: #e6eef0; }
|
||||
#mntCalendar .fc-toolbar-title { font-size: 1.05rem; font-weight: 700; color: var(--mnt-heading); }
|
||||
#mntCalendar .fc-event { cursor: pointer; font-size: 0.75rem; }
|
||||
@media (max-width: 767.98px) {
|
||||
#mntCalendar .fc-toolbar { flex-direction: column; gap: 0.5rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<?php include __DIR__ . '/include/wrapper_open.php'; ?>
|
||||
<?php include(__DIR__ . '/../include/navbar.php'); ?>
|
||||
<?php include(__DIR__ . '/../include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<div class="card mnt-card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h5><i class="fa-solid fa-calendar-days me-2"></i>Calendario manutenzioni</h5>
|
||||
<div class="header-actions d-flex gap-2 flex-wrap">
|
||||
<a href="manutenzioni/calendar_global.php" class="btn btn-mnt-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-layer-group"></i><span>Calendario cumulativo</span>
|
||||
</a>
|
||||
<a href="manutenzioni/maintenances.php" class="btn btn-mnt-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-list"></i><span>Elenco</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="mnt-filter-bar">
|
||||
<select class="form-select" id="filterType">
|
||||
<option value="">Programmate e straordinarie</option>
|
||||
<?php foreach (mnt_intervention_types() as $value => $label): ?>
|
||||
<option value="<?= mnt_h($value) ?>">Solo <?= mnt_h(mb_strtolower($label)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<select class="form-select" id="filterCategory">
|
||||
<option value="">Tutte le categorie</option>
|
||||
<?php foreach ($formData['categories'] as $category): ?>
|
||||
<option value="<?= (int)$category['id'] ?>"><?= mnt_h($category['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-3 flex-wrap mb-3 small">
|
||||
<span><span class="mnt-badge mnt-badge-overdue">Scaduta</span></span>
|
||||
<span><span class="mnt-badge mnt-badge-soon">In scadenza</span></span>
|
||||
<span><span class="mnt-badge mnt-badge-ok">In regola (colore della categoria)</span></span>
|
||||
</div>
|
||||
|
||||
<div id="mntCalendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php include(__DIR__ . '/../include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php include(__DIR__ . '/../jsinclude.php'); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const el = document.getElementById('mntCalendar');
|
||||
const isMobile = window.innerWidth < 768;
|
||||
|
||||
const calendar = new FullCalendar.Calendar(el, {
|
||||
locale: 'it',
|
||||
initialView: isMobile ? 'listMonth' : 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: isMobile ? 'listMonth,dayGridMonth' : 'dayGridMonth,listMonth'
|
||||
},
|
||||
height: 'auto',
|
||||
firstDay: 1,
|
||||
events: function (info, success, failure) {
|
||||
$.getJSON('manutenzioni/ajax/get_calendar_events.php', {
|
||||
start: info.startStr,
|
||||
end: info.endStr,
|
||||
type: $('#filterType').val(),
|
||||
category: $('#filterCategory').val()
|
||||
}).done(success).fail(failure);
|
||||
},
|
||||
eventClick: function (info) {
|
||||
window.location = 'manutenzioni/equipment.php?id=' + info.event.extendedProps.equipmentId;
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
$('#filterType, #filterCategory').on('change', () => calendar.refetchEvents());
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php include(__DIR__ . '/../include/headscript.php'); ?>
|
||||
<?php
|
||||
require_once __DIR__ . '/include/functions.php';
|
||||
|
||||
// Cumulative calendar: deadlines + trainings + maintenances.
|
||||
// Visible to anyone who can see at least one of the three modules; each
|
||||
// layer is filtered again on the backend by its own permission.
|
||||
$layers = [
|
||||
'deadlines' => ['label' => 'Scadenze', 'permission' => 'deadlines.view', 'color' => '#6f42c1', 'icon' => '📅'],
|
||||
'trainings' => ['label' => 'Formazione', 'permission' => 'hr.trainings.view', 'color' => '#198754', 'icon' => '🎓'],
|
||||
'maintenances' => ['label' => 'Manutenzioni', 'permission' => 'maintenance.calendar.view', 'color' => '#2f7d8f', 'icon' => '🔧'],
|
||||
];
|
||||
|
||||
$availableLayers = array_filter($layers, fn($layer) => userCan($layer['permission']));
|
||||
|
||||
if (!$availableLayers) {
|
||||
http_response_code(403);
|
||||
exit('Permesso negato.');
|
||||
}
|
||||
|
||||
$MNT_TITLE = 'Calendario cumulativo';
|
||||
$MNT_PLUGINS = ['fullcalendar'];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<?php include __DIR__ . '/include/page_head.php'; ?>
|
||||
<style>
|
||||
#mntGlobalCalendar { --fc-border-color: #e6eef0; }
|
||||
#mntGlobalCalendar .fc-toolbar-title { font-size: 1.05rem; font-weight: 700; color: var(--mnt-heading); }
|
||||
#mntGlobalCalendar .fc-event { cursor: pointer; font-size: 0.75rem; }
|
||||
.mnt-layer-toggle { display: inline-flex; align-items: center; gap: 0.45rem; padding: 0.35rem 0.75rem; border-radius: 999px; border: 1.5px solid var(--layer-color); color: var(--layer-color); font-weight: 600; font-size: 0.8rem; cursor: pointer; user-select: none; transition: all 0.15s; }
|
||||
.mnt-layer-toggle input { display: none; }
|
||||
.mnt-layer-toggle.is-on { background: var(--layer-color); color: #fff; }
|
||||
@media (max-width: 767.98px) {
|
||||
#mntGlobalCalendar .fc-toolbar { flex-direction: column; gap: 0.5rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<?php include __DIR__ . '/include/wrapper_open.php'; ?>
|
||||
<?php include(__DIR__ . '/../include/navbar.php'); ?>
|
||||
<?php include(__DIR__ . '/../include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<div class="card mnt-card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h5><i class="fa-solid fa-layer-group me-2"></i>Calendario cumulativo</h5>
|
||||
<div class="header-actions d-flex gap-2 flex-wrap">
|
||||
<?php if (userCan('maintenance.calendar.view')): ?>
|
||||
<a href="manutenzioni/calendar.php" class="btn btn-mnt-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-calendar-days"></i><span>Solo manutenzioni</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="d-flex gap-2 flex-wrap mb-3">
|
||||
<?php foreach ($availableLayers as $key => $layer): ?>
|
||||
<label class="mnt-layer-toggle is-on" style="--layer-color: <?= mnt_h($layer['color']) ?>">
|
||||
<input type="checkbox" class="layer-checkbox" value="<?= mnt_h($key) ?>" checked>
|
||||
<span><?= $layer['icon'] ?> <?= mnt_h($layer['label']) ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div id="mntGlobalCalendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php include(__DIR__ . '/../include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php include(__DIR__ . '/../jsinclude.php'); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const el = document.getElementById('mntGlobalCalendar');
|
||||
const isMobile = window.innerWidth < 768;
|
||||
|
||||
function activeLayers() {
|
||||
return $('.layer-checkbox:checked').map((i, cb) => cb.value).get().join(',');
|
||||
}
|
||||
|
||||
const calendar = new FullCalendar.Calendar(el, {
|
||||
locale: 'it',
|
||||
initialView: isMobile ? 'listMonth' : 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: isMobile ? 'listMonth,dayGridMonth' : 'dayGridMonth,listMonth'
|
||||
},
|
||||
height: 'auto',
|
||||
firstDay: 1,
|
||||
events: function (info, success, failure) {
|
||||
const layers = activeLayers();
|
||||
if (!layers) { success([]); return; }
|
||||
|
||||
$.getJSON('manutenzioni/ajax/get_global_calendar_events.php', {
|
||||
start: info.startStr,
|
||||
end: info.endStr,
|
||||
layers: layers
|
||||
}).done(success).fail(failure);
|
||||
},
|
||||
eventClick: function (info) {
|
||||
const url = info.event.extendedProps.url;
|
||||
if (url) { window.location = url; }
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
|
||||
$('.layer-checkbox').on('change', function () {
|
||||
$(this).closest('.mnt-layer-toggle').toggleClass('is-on', this.checked);
|
||||
calendar.refetchEvents();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,278 @@
|
||||
<?php include(__DIR__ . '/../../include/headscript.php'); ?>
|
||||
<?php
|
||||
require_once __DIR__ . '/../include/functions.php';
|
||||
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
if (!userCan('maintenance.categories.view')) {
|
||||
http_response_code(403);
|
||||
exit('Permesso negato.');
|
||||
}
|
||||
|
||||
$canManage = userCan('maintenance.manage');
|
||||
|
||||
$categories = $pdo->query("
|
||||
SELECT c.*,
|
||||
(SELECT COUNT(*) FROM inv_equipment e WHERE e.category_id = c.id) AS equipment_count
|
||||
FROM inv_categories c
|
||||
ORDER BY c.sort_order ASC, c.name ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$MNT_TITLE = 'Categorie attrezzature';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<?php include __DIR__ . '/../include/page_head.php'; ?>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<?php include __DIR__ . '/../include/wrapper_open.php'; ?>
|
||||
<?php include(__DIR__ . '/../../include/navbar.php'); ?>
|
||||
<?php include(__DIR__ . '/../../include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb" style="background:transparent;padding:0;margin:0;font-size:0.85rem">
|
||||
<li class="breadcrumb-item"><a href="manutenzioni/index.php">Registro attrezzature</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Categorie</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="card mnt-card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h5><i class="fa-solid fa-tags me-2"></i>Categorie attrezzature</h5>
|
||||
<div class="header-actions d-flex gap-2 flex-wrap">
|
||||
<a href="manutenzioni/index.php" class="btn btn-mnt-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-arrow-left"></i><span>Registro</span>
|
||||
</a>
|
||||
<?php if ($canManage): ?>
|
||||
<button class="btn btn-mnt-primary d-inline-flex align-items-center gap-2" id="btnAddCategory">
|
||||
<i class="fa-solid fa-plus"></i><span>Nuova categoria</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<?php if (!$categories): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fa-solid fa-tags"></i>
|
||||
<p>Nessuna categoria definita.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div id="categoryList">
|
||||
<!-- MOBILE -->
|
||||
<div class="d-md-none">
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<div class="mnt-item-card" style="--row-color: <?= mnt_h($category['color']) ?>"
|
||||
data-id="<?= (int)$category['id'] ?>"
|
||||
data-json="<?= mnt_h(json_encode($category, JSON_UNESCAPED_UNICODE)) ?>">
|
||||
<div class="ic-title"><?= mnt_h($category['name']) ?></div>
|
||||
<div class="ic-meta">
|
||||
Attrezzature: <strong><?= (int)$category['equipment_count'] ?></strong>
|
||||
<?php if ((int)$category['requires_line'] === 1): ?>
|
||||
· <span class="mnt-badge mnt-badge-soft">Richiede linea</span>
|
||||
<?php endif; ?>
|
||||
<?php if ((int)$category['is_active'] === 0): ?>
|
||||
· <span class="mnt-badge mnt-badge-none">Disattivata</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($canManage): ?>
|
||||
<div class="ic-actions">
|
||||
<button class="btn-action btn-action-edit btn-edit"><i class="fa-solid fa-pen"></i></button>
|
||||
<button class="btn-action btn-action-delete btn-delete"
|
||||
data-name="<?= mnt_h($category['name']) ?>"
|
||||
data-count="<?= (int)$category['equipment_count'] ?>">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- DESKTOP -->
|
||||
<div class="d-none d-md-block table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:60px">Colore</th>
|
||||
<th>Nome</th>
|
||||
<th>Codice</th>
|
||||
<th class="text-center">Richiede linea</th>
|
||||
<th class="text-center">Ordine</th>
|
||||
<th class="text-center">Attrezzature</th>
|
||||
<th class="text-center">Stato</th>
|
||||
<?php if ($canManage): ?><th class="text-center" style="width:120px">Azioni</th><?php endif; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<tr data-id="<?= (int)$category['id'] ?>"
|
||||
data-json="<?= mnt_h(json_encode($category, JSON_UNESCAPED_UNICODE)) ?>">
|
||||
<td><span class="mnt-cat-dot" style="width:22px;height:22px;background: <?= mnt_h($category['color']) ?>"></span></td>
|
||||
<td class="fw-semibold" style="color:var(--mnt-heading)"><?= mnt_h($category['name']) ?></td>
|
||||
<td class="small text-muted"><?= mnt_h($category['code'] ?: '—') ?></td>
|
||||
<td class="text-center">
|
||||
<?= (int)$category['requires_line'] === 1
|
||||
? '<span class="mnt-badge mnt-badge-soft">Richiede linea</span>'
|
||||
: '<span class="text-muted">—</span>' ?>
|
||||
</td>
|
||||
<td class="text-center small"><?= (int)$category['sort_order'] ?></td>
|
||||
<td class="text-center"><?= (int)$category['equipment_count'] ?></td>
|
||||
<td class="text-center">
|
||||
<span class="mnt-badge <?= (int)$category['is_active'] === 1 ? 'mnt-badge-ok' : 'mnt-badge-none' ?>">
|
||||
<?= (int)$category['is_active'] === 1 ? 'Attiva' : 'Disattivata' ?>
|
||||
</span>
|
||||
</td>
|
||||
<?php if ($canManage): ?>
|
||||
<td class="text-center">
|
||||
<div class="d-inline-flex gap-1">
|
||||
<button class="btn-action btn-action-edit btn-edit" title="Modifica"><i class="fa-solid fa-pen"></i></button>
|
||||
<button class="btn-action btn-action-delete btn-delete" title="Elimina"
|
||||
data-name="<?= mnt_h($category['name']) ?>"
|
||||
data-count="<?= (int)$category['equipment_count'] ?>">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php include(__DIR__ . '/../../include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php if ($canManage): ?>
|
||||
<div class="modal fade" id="categoryModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="categoryModalTitle">Nuova categoria</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<form id="categoryForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="id" id="catId" value="">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Nome <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="name" id="catName" maxlength="150" required>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label fw-semibold">Codice</label>
|
||||
<input type="text" class="form-control" name="code" id="catCode" maxlength="50">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label fw-semibold">Ordine</label>
|
||||
<input type="number" class="form-control" name="sort_order" id="catSort" min="0" max="9999" value="999">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Colore</label>
|
||||
<input type="color" class="form-control form-control-color" name="color" id="catColor" value="#6c757d">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Descrizione</label>
|
||||
<textarea class="form-control" name="description" id="catDescription" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" type="checkbox" name="requires_line" id="catRequiresLine" value="1">
|
||||
<label class="form-check-label fw-semibold" for="catRequiresLine">
|
||||
Richiede una linea di produzione
|
||||
</label>
|
||||
<div class="form-text">Le attrezzature di questa categoria devono indicare la linea.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="checkbox" name="is_active" id="catActive" value="1" checked>
|
||||
<label class="form-check-label fw-semibold" for="catActive">Attiva</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-mnt-primary">Salva</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php include(__DIR__ . '/../../jsinclude.php'); ?>
|
||||
<script>
|
||||
$(function () {
|
||||
<?php if ($canManage): ?>
|
||||
function openModal(data) {
|
||||
const isEdit = !!data;
|
||||
$('#categoryModalTitle').text(isEdit ? 'Modifica categoria' : 'Nuova categoria');
|
||||
$('#catId').val(isEdit ? data.id : '');
|
||||
$('#catName').val(isEdit ? data.name : '');
|
||||
$('#catCode').val(isEdit ? (data.code || '') : '');
|
||||
$('#catSort').val(isEdit ? data.sort_order : 999);
|
||||
$('#catColor').val(isEdit ? data.color : '#6c757d');
|
||||
$('#catDescription').val(isEdit ? (data.description || '') : '');
|
||||
$('#catRequiresLine').prop('checked', isEdit ? Number(data.requires_line) === 1 : false);
|
||||
$('#catActive').prop('checked', isEdit ? Number(data.is_active) === 1 : true);
|
||||
new bootstrap.Modal('#categoryModal').show();
|
||||
}
|
||||
|
||||
$('#btnAddCategory').on('click', () => openModal(null));
|
||||
|
||||
$('#categoryList').on('click', '.btn-edit', function () {
|
||||
openModal($(this).closest('[data-json]').data('json'));
|
||||
});
|
||||
|
||||
$('#categoryList').on('click', '.btn-delete', function () {
|
||||
const $btn = $(this);
|
||||
const count = parseInt($btn.data('count') || 0, 10);
|
||||
const id = $btn.closest('[data-id]').data('id');
|
||||
|
||||
if (count > 0) {
|
||||
Swal.fire({
|
||||
icon: 'warning', title: 'Impossibile eliminare',
|
||||
text: `La categoria "${$btn.data('name')}" è usata da ${count} attrezzature.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: `Eliminare "${$btn.data('name')}"?`, icon: 'warning', showCancelButton: true,
|
||||
confirmButtonText: 'Elimina', cancelButtonText: 'Annulla', confirmButtonColor: '#dc3545'
|
||||
}).then(r => {
|
||||
if (!r.isConfirmed) return;
|
||||
$.post('manutenzioni/ajax/delete_category.php', { id: id })
|
||||
.done(res => res.success ? location.reload() : Swal.fire({ icon: 'error', title: 'Errore', text: res.message }));
|
||||
});
|
||||
});
|
||||
|
||||
$('#categoryForm').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
if (!$('#catName').val().trim()) { Swal.fire({ icon: 'warning', title: 'Nome obbligatorio' }); return; }
|
||||
|
||||
$.post('manutenzioni/ajax/save_category.php', $(this).serialize())
|
||||
.done(res => res.success ? location.reload() : Swal.fire({ icon: 'error', title: 'Errore', text: res.message }))
|
||||
.fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
|
||||
});
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Manutenzioni — email notifications
|
||||
* Run daily, e.g.: 0 7 * * * php /var/www/html/public/userarea/manutenzioni/cron/send_notifications.php
|
||||
*
|
||||
* Sends:
|
||||
* - "advance" when next_due_date <= today + alert_days
|
||||
* - "overdue" when next_due_date < today
|
||||
*
|
||||
* Recipients are the assignee and the supervisor of the maintenance
|
||||
* (employees.auth_user_id -> auth_users.email). Deduplicated per
|
||||
* maintenance + email + type + day by maint_notifications.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../class/db-functions.php';
|
||||
require_once __DIR__ . '/../include/functions.php';
|
||||
require_once __DIR__ . '/../../../../vendor/autoload.php';
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
|
||||
$dotenv = Dotenv::createImmutable(__DIR__ . '/../../../../');
|
||||
$dotenv->safeLoad();
|
||||
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/');
|
||||
|
||||
/**
|
||||
* --dry-run (or MNT_MAIL_DRYRUN=1) works out the queue and prints it without
|
||||
* sending anything and without writing to maint_notifications, so a run leaves
|
||||
* no trace and can be repeated. Useful before a change to see what tonight
|
||||
* would deliver, and it is what the test suite drives.
|
||||
*
|
||||
* One line per queued message, tab separated:
|
||||
* QUEUE <type> <email> <maintenance_id> <label>
|
||||
*/
|
||||
$dryRun = in_array('--dry-run', $argv ?? [], true) || !empty($_ENV['MNT_MAIL_DRYRUN']);
|
||||
if ($dryRun) {
|
||||
echo "DRY-RUN — nothing is sent, nothing is recorded\n";
|
||||
}
|
||||
|
||||
$sent = 0;
|
||||
$skipped = 0;
|
||||
$errors = 0;
|
||||
|
||||
// Only active maintenances of active equipment, with a real due date
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT m.id, m.code, m.title, m.next_due_date, m.alert_days, m.is_critical,
|
||||
e.id AS equipment_id, e.name AS equipment_name,
|
||||
m.assignee_employee_id, m.supervisor_employee_id
|
||||
FROM maint_maintenances m
|
||||
INNER JOIN inv_equipment e ON e.id = m.equipment_id
|
||||
WHERE m.is_active = 1
|
||||
AND e.status = 'active'
|
||||
AND m.next_due_date IS NOT NULL
|
||||
AND m.next_due_date <= DATE_ADD(?, INTERVAL m.alert_days DAY)
|
||||
");
|
||||
$stmt->execute([$today]);
|
||||
$maintenances = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$maintenances) {
|
||||
echo date('Y-m-d H:i:s') . " — Nessuna manutenzione da notificare.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$getRecipient = $pdo->prepare("
|
||||
SELECT e.id AS employee_id, e.first_name, e.last_name, u.email
|
||||
FROM employees e
|
||||
INNER JOIN auth_users u ON u.id = e.auth_user_id
|
||||
WHERE e.id = ?
|
||||
AND e.auth_user_id IS NOT NULL
|
||||
AND u.email IS NOT NULL
|
||||
AND u.email <> ''
|
||||
");
|
||||
|
||||
$checkSent = $pdo->prepare("
|
||||
SELECT COUNT(*) FROM maint_notifications
|
||||
WHERE maintenance_id = ? AND email = ? AND type = ? AND sent_date = ?
|
||||
");
|
||||
|
||||
$insertNotification = $pdo->prepare("
|
||||
INSERT INTO maint_notifications (maintenance_id, employee_id, email, type, sent_date)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
");
|
||||
|
||||
// mnt_mail_body() and mnt_notification_mail() live in include/functions.php,
|
||||
// so that the preview renders the very same markup this cron sends.
|
||||
|
||||
foreach ($maintenances as $maintenance) {
|
||||
$isOverdue = $maintenance['next_due_date'] < $today;
|
||||
$type = $isOverdue ? 'overdue' : 'advance';
|
||||
$daysLeft = (int)((strtotime($maintenance['next_due_date']) - strtotime($today)) / 86400);
|
||||
|
||||
// Assignee + supervisor, de-duplicated by employee id
|
||||
$recipients = [];
|
||||
foreach ([$maintenance['assignee_employee_id'], $maintenance['supervisor_employee_id']] as $employeeId) {
|
||||
if (!$employeeId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$getRecipient->execute([(int)$employeeId]);
|
||||
$recipient = $getRecipient->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($recipient) {
|
||||
$recipients[(int)$recipient['employee_id']] = $recipient;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$recipients) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = ($maintenance['code'] ? $maintenance['code'] . ' — ' : '') . $maintenance['title'];
|
||||
$detailUrl = $appUrl . '/userarea/manutenzioni/equipment.php?id=' . (int)$maintenance['equipment_id'];
|
||||
|
||||
foreach ($recipients as $recipient) {
|
||||
$checkSent->execute([$maintenance['id'], $recipient['email'], $type, $today]);
|
||||
if ((int)$checkSent->fetchColumn() > 0) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
printf("QUEUE\t%s\t%s\t%d\t%s\n", $type, $recipient['email'], (int)$maintenance['id'], $label);
|
||||
$sent++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
if (($_ENV['MAIL_MAILER'] ?? 'mail') === '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'] ?? '';
|
||||
}
|
||||
|
||||
$encryption = $_ENV['MAIL_ENCRYPTION'] ?? '';
|
||||
if ($encryption && $encryption !== 'null') {
|
||||
$mail->SMTPSecure = $encryption;
|
||||
}
|
||||
}
|
||||
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->isHTML(true);
|
||||
$mail->setFrom(
|
||||
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
|
||||
$_ENV['MAIL_FROM_NAME'] ?? 'Manutenzioni ZIBOGOMMA'
|
||||
);
|
||||
$mail->addAddress($recipient['email'], trim($recipient['first_name'] . ' ' . $recipient['last_name']));
|
||||
|
||||
$composed = mnt_notification_mail($maintenance, $label, $daysLeft, $detailUrl, $today);
|
||||
$mail->Subject = $composed['subject'];
|
||||
$mail->Body = $composed['body'];
|
||||
|
||||
$mail->send();
|
||||
|
||||
$insertNotification->execute([
|
||||
$maintenance['id'],
|
||||
(int)$recipient['employee_id'],
|
||||
$recipient['email'],
|
||||
$type,
|
||||
$today,
|
||||
]);
|
||||
|
||||
mnt_log(
|
||||
$pdo,
|
||||
'notification_sent',
|
||||
(int)$maintenance['equipment_id'],
|
||||
(int)$maintenance['id'],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$type . ' → ' . $recipient['email']
|
||||
);
|
||||
|
||||
$sent++;
|
||||
} catch (Throwable $e) {
|
||||
$errors++;
|
||||
echo date('Y-m-d H:i:s') . ' — Errore invio a ' . $recipient['email'] . ': ' . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo date('Y-m-d H:i:s')
|
||||
. ($dryRun ? " — In coda: $sent" : " — Inviate: $sent")
|
||||
. ", saltate: $skipped, errori: $errors\n";
|
||||
@@ -0,0 +1,560 @@
|
||||
<?php include(__DIR__ . '/../include/headscript.php'); ?>
|
||||
<?php
|
||||
require_once __DIR__ . '/include/functions.php';
|
||||
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
if (!userCan('maintenance.equipment.view')) {
|
||||
http_response_code(403);
|
||||
exit('Permesso negato.');
|
||||
}
|
||||
|
||||
$canManage = userCan('maintenance.manage');
|
||||
$canSeeMaintenances = userCan('maintenance.maintenances.view');
|
||||
|
||||
$equipmentId = isset($_GET['id']) && is_numeric($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT e.*,
|
||||
c.name AS category_name, c.color AS category_color,
|
||||
pl.name AS line_name, pl.line_number,
|
||||
d.name AS department_name,
|
||||
cf.stored_name AS cover_stored
|
||||
FROM inv_equipment e
|
||||
LEFT JOIN inv_categories c ON c.id = e.category_id
|
||||
LEFT JOIN production_lines pl ON pl.id = e.line_id
|
||||
LEFT JOIN departments d ON d.id = e.department_id
|
||||
LEFT JOIN inv_equipment_files cf ON cf.id = e.cover_file_id
|
||||
WHERE e.id = ?
|
||||
");
|
||||
$stmt->execute([$equipmentId]);
|
||||
$equipment = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$equipment) {
|
||||
http_response_code(404);
|
||||
exit('Attrezzatura non trovata.');
|
||||
}
|
||||
|
||||
// Photos and documents
|
||||
$stmt = $pdo->prepare("SELECT * FROM inv_equipment_files WHERE equipment_id = ? ORDER BY created_at ASC");
|
||||
$stmt->execute([$equipmentId]);
|
||||
$files = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$photos = array_values(array_filter($files, fn($f) => $f['kind'] === 'photo'));
|
||||
$documents = array_values(array_filter($files, fn($f) => $f['kind'] !== 'photo'));
|
||||
|
||||
// Maintenances
|
||||
$maintenances = [];
|
||||
$interventionsByMaintenance = [];
|
||||
|
||||
if ($canSeeMaintenances) {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT m.*,
|
||||
CONCAT(a.first_name, ' ', a.last_name) AS assignee_name,
|
||||
CONCAT(s.first_name, ' ', s.last_name) AS supervisor_name,
|
||||
sup.supplier_name,
|
||||
(SELECT COUNT(*) FROM maint_interventions i WHERE i.maintenance_id = m.id) AS intervention_count,
|
||||
(SELECT COUNT(*) FROM maint_maintenance_files f WHERE f.maintenance_id = m.id) AS file_count
|
||||
FROM maint_maintenances m
|
||||
LEFT JOIN employees a ON a.id = m.assignee_employee_id
|
||||
LEFT JOIN employees s ON s.id = m.supervisor_employee_id
|
||||
LEFT JOIN suppliers sup ON sup.idsupplier = m.supplier_id
|
||||
WHERE m.equipment_id = ?
|
||||
ORDER BY m.is_active DESC, m.code ASC, m.title ASC
|
||||
");
|
||||
$stmt->execute([$equipmentId]);
|
||||
$maintenances = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT i.*,
|
||||
CONCAT(e.first_name, ' ', e.last_name) AS operator_full_name,
|
||||
sup.supplier_name,
|
||||
(SELECT COUNT(*) FROM maint_intervention_files f WHERE f.intervention_id = i.id) AS file_count
|
||||
FROM maint_interventions i
|
||||
LEFT JOIN employees e ON e.id = i.operator_employee_id
|
||||
LEFT JOIN suppliers sup ON sup.idsupplier = i.supplier_id
|
||||
WHERE i.equipment_id = ?
|
||||
ORDER BY i.performed_at DESC, i.id DESC
|
||||
");
|
||||
$stmt->execute([$equipmentId]);
|
||||
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $intervention) {
|
||||
$interventionsByMaintenance[(int)$intervention['maintenance_id']][] = $intervention;
|
||||
}
|
||||
}
|
||||
|
||||
$formData = mnt_form_data($pdo);
|
||||
$statuses = mnt_equipment_statuses();
|
||||
$statusLabels = mnt_lookup($pdo, 'maint_status', ['planned' => 'Pianificato', 'in_progress' => 'In corso', 'completed' => 'Completato']);
|
||||
$resultLabels = mnt_lookup($pdo, 'maint_result', []);
|
||||
$typeLabels = mnt_intervention_types();
|
||||
$executionLabels = mnt_execution_types();
|
||||
|
||||
$MNT_TITLE = $equipment['name'];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<?php include __DIR__ . '/include/page_head.php'; ?>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<?php include __DIR__ . '/include/wrapper_open.php'; ?>
|
||||
<?php include(__DIR__ . '/../include/navbar.php'); ?>
|
||||
<?php include(__DIR__ . '/../include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb" style="background:transparent;padding:0;margin:0;font-size:0.85rem">
|
||||
<li class="breadcrumb-item"><a href="manutenzioni/index.php">Registro attrezzature</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page"><?= mnt_h($equipment['name']) ?></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="card mnt-card mb-3">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h5 class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<span class="mnt-cat-dot" style="background: <?= mnt_h($equipment['category_color'] ?? '#adb5bd') ?>"></span>
|
||||
<?= mnt_h($equipment['name']) ?>
|
||||
<span class="mnt-badge mnt-badge-status-<?= mnt_h($equipment['status']) ?>">
|
||||
<?= mnt_h($statuses[$equipment['status']] ?? $equipment['status']) ?>
|
||||
</span>
|
||||
</h5>
|
||||
<div class="header-actions d-flex gap-2 flex-wrap">
|
||||
<a href="manutenzioni/print.php?id=<?= (int)$equipment['id'] ?>" target="_blank"
|
||||
class="btn btn-mnt-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-print"></i><span>Scheda PDF</span>
|
||||
</a>
|
||||
<?php if ($canManage): ?>
|
||||
<button class="btn btn-mnt-outline d-inline-flex align-items-center gap-2" id="btnEditEquipment">
|
||||
<i class="fa-solid fa-pen"></i><span>Modifica</span>
|
||||
</button>
|
||||
<button class="btn btn-mnt-primary d-inline-flex align-items-center gap-2" id="btnAddMaintenance">
|
||||
<i class="fa-solid fa-plus"></i><span>Nuova manutenzione</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- ------------------------------------------------ left: data -->
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card mnt-card mb-3">
|
||||
<div class="card-header"><h5><i class="fa-solid fa-circle-info me-2"></i>Dati</h5></div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($equipment['cover_stored'])): ?>
|
||||
<img src="manutenzioni/ajax/download_file.php?scope=equipment&id=<?= (int)$equipment['cover_file_id'] ?>"
|
||||
alt="" class="w-100 mb-3" style="border-radius:.5rem;object-fit:cover;max-height:220px">
|
||||
<?php endif; ?>
|
||||
|
||||
<dl class="mb-0">
|
||||
<div class="mnt-kv"><dt>Categoria</dt><dd><?= mnt_h($equipment['category_name'] ?? '—') ?></dd></div>
|
||||
<?php if ($equipment['line_name']): ?>
|
||||
<div class="mnt-kv"><dt>Linea</dt><dd>Linea <?= (int)$equipment['line_number'] ?> — <?= mnt_h($equipment['line_name']) ?></dd></div>
|
||||
<?php endif; ?>
|
||||
<div class="mnt-kv"><dt>Matricola</dt><dd><?= mnt_h($equipment['registration_number'] ?: '—') ?></dd></div>
|
||||
<div class="mnt-kv"><dt>Numero di serie</dt><dd><?= mnt_h($equipment['serial_number'] ?: '—') ?></dd></div>
|
||||
<div class="mnt-kv"><dt>Lotto / partita</dt><dd><?= mnt_h($equipment['batch_lot'] ?: '—') ?></dd></div>
|
||||
<div class="mnt-kv"><dt>Costruttore</dt><dd><?= mnt_h($equipment['manufacturer'] ?: '—') ?></dd></div>
|
||||
<div class="mnt-kv"><dt>Tipo</dt><dd><?= mnt_h($equipment['tool_type'] ?: '—') ?></dd></div>
|
||||
<div class="mnt-kv"><dt>Acquisto</dt><dd><?= mnt_format_date($equipment['purchase_date']) ?></dd></div>
|
||||
<div class="mnt-kv"><dt>Messa in servizio</dt><dd><?= mnt_format_date($equipment['commissioning_date']) ?></dd></div>
|
||||
<div class="mnt-kv"><dt>Reparto</dt><dd><?= mnt_h($equipment['department_name'] ?: '—') ?></dd></div>
|
||||
<div class="mnt-kv"><dt>Ubicazione</dt><dd><?= mnt_h($equipment['location'] ?: '—') ?></dd></div>
|
||||
</dl>
|
||||
|
||||
<?php if ($equipment['description']): ?>
|
||||
<div class="mnt-section-title mt-3">Descrizione</div>
|
||||
<div class="small"><?= nl2br(mnt_h($equipment['description'])) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($equipment['notes']): ?>
|
||||
<div class="mnt-section-title mt-3">Note</div>
|
||||
<div class="small"><?= nl2br(mnt_h($equipment['notes'])) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mnt-card mb-3">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<h5><i class="fa-solid fa-images me-2"></i>Foto</h5>
|
||||
<?php if ($canManage): ?>
|
||||
<button class="btn btn-mnt-outline btn-sm" id="btnAddPhotos">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
<input type="file" id="photoInput" accept="image/*" multiple hidden>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!$photos): ?>
|
||||
<p class="text-muted small mb-0">Nessuna foto caricata.</p>
|
||||
<?php else: ?>
|
||||
<div class="mnt-photo-grid">
|
||||
<?php foreach ($photos as $photo): ?>
|
||||
<div class="mnt-photo-item <?= (int)$equipment['cover_file_id'] === (int)$photo['id'] ? 'is-cover' : '' ?>">
|
||||
<a href="manutenzioni/ajax/download_file.php?scope=equipment&id=<?= (int)$photo['id'] ?>" target="_blank">
|
||||
<img src="manutenzioni/ajax/download_file.php?scope=equipment&id=<?= (int)$photo['id'] ?>"
|
||||
alt="<?= mnt_h($photo['original_name']) ?>">
|
||||
</a>
|
||||
<?php if ($canManage): ?>
|
||||
<div class="photo-tools">
|
||||
<button class="btn-set-cover" data-id="<?= (int)$photo['id'] ?>" title="Usa come copertina">
|
||||
<i class="fa-solid fa-star"></i>
|
||||
</button>
|
||||
<button class="btn-del-photo" data-id="<?= (int)$photo['id'] ?>" title="Elimina">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mnt-card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<h5><i class="fa-solid fa-folder-open me-2"></i>Documenti</h5>
|
||||
<?php if ($canManage): ?>
|
||||
<button class="btn btn-mnt-outline btn-sm" id="btnAddDocs">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
<input type="file" id="docInput" multiple hidden>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!$documents): ?>
|
||||
<p class="text-muted small mb-0">Nessun documento caricato.</p>
|
||||
<?php else: ?>
|
||||
<?php foreach ($documents as $document): ?>
|
||||
<div class="mnt-file-row">
|
||||
<i class="fa-solid fa-file text-muted"></i>
|
||||
<a class="file-name" target="_blank"
|
||||
href="manutenzioni/ajax/download_file.php?scope=equipment&id=<?= (int)$document['id'] ?>">
|
||||
<?= mnt_h($document['original_name']) ?>
|
||||
</a>
|
||||
<?php if ($canManage): ?>
|
||||
<button class="btn-action btn-action-delete btn-del-doc" data-id="<?= (int)$document['id'] ?>">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- --------------------------------------- right: maintenances -->
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card mnt-card">
|
||||
<div class="card-header"><h5><i class="fa-solid fa-screwdriver-wrench me-2"></i>Manutenzioni</h5></div>
|
||||
<div class="card-body">
|
||||
<?php if (!$canSeeMaintenances): ?>
|
||||
<p class="text-muted small mb-0">Non hai i permessi per vedere le manutenzioni.</p>
|
||||
<?php elseif (!$maintenances): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fa-solid fa-screwdriver-wrench"></i>
|
||||
<p>Nessuna manutenzione definita per questa attrezzatura.<br>
|
||||
Un'attrezzatura senza manutenzioni è esclusa dalle pianificazioni.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div id="maintenanceList">
|
||||
<?php foreach ($maintenances as $maintenance): ?>
|
||||
<?php
|
||||
$state = mnt_due_state($maintenance['next_due_date'], (int)$maintenance['alert_days']);
|
||||
$badge = mnt_due_badge($state, $maintenance['frequency_unit']);
|
||||
$interventions = $interventionsByMaintenance[(int)$maintenance['id']] ?? [];
|
||||
?>
|
||||
<div class="mnt-item-card" data-id="<?= (int)$maintenance['id'] ?>"
|
||||
style="--row-color: <?= $maintenance['is_critical'] ? '#dc3545' : 'var(--mnt-primary)' ?>;<?= (int)$maintenance['is_active'] === 0 ? 'opacity:.6' : '' ?>">
|
||||
<div class="d-flex justify-content-between align-items-start gap-2 flex-wrap">
|
||||
<div class="flex-grow-1">
|
||||
<div class="ic-title">
|
||||
<?php if ($maintenance['code']): ?>
|
||||
<span class="mnt-badge mnt-badge-soft me-1"><?= mnt_h($maintenance['code']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?= mnt_h($maintenance['title']) ?>
|
||||
<?php if ($maintenance['is_critical']): ?>
|
||||
<span class="mnt-badge mnt-badge-critical ms-1">Critica</span>
|
||||
<?php endif; ?>
|
||||
<?php if ((int)$maintenance['is_active'] === 0): ?>
|
||||
<span class="mnt-badge mnt-badge-none ms-1">Disattivata</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="ic-meta">
|
||||
<?= mnt_h($typeLabels[$maintenance['intervention_type']] ?? '') ?> ·
|
||||
<?= mnt_h($executionLabels[$maintenance['execution_type']] ?? '') ?>
|
||||
<?php if ($maintenance['supplier_name']): ?>
|
||||
(<?= mnt_h($maintenance['supplier_name']) ?>)
|
||||
<?php endif; ?>
|
||||
· <?= mnt_h(mnt_format_frequency(
|
||||
$maintenance['frequency_value'] !== null ? (int)$maintenance['frequency_value'] : null,
|
||||
$maintenance['frequency_unit'],
|
||||
$maintenance['frequency_note']
|
||||
)) ?>
|
||||
</div>
|
||||
<div class="ic-meta">
|
||||
Ultimo: <strong><?= mnt_format_date($maintenance['last_done_date']) ?></strong> ·
|
||||
Prossimo: <strong><?= mnt_format_date($maintenance['next_due_date']) ?></strong>
|
||||
<span class="mnt-badge <?= $badge['class'] ?> ms-1"><?= $badge['label'] ?></span>
|
||||
</div>
|
||||
<?php if ($maintenance['assignee_name'] || $maintenance['supervisor_name']): ?>
|
||||
<div class="ic-meta">
|
||||
<?php if ($maintenance['assignee_name']): ?>
|
||||
Incaricato: <strong><?= mnt_h($maintenance['assignee_name']) ?></strong>
|
||||
<?php endif; ?>
|
||||
<?php if ($maintenance['supervisor_name']): ?>
|
||||
· Responsabile: <strong><?= mnt_h($maintenance['supervisor_name']) ?></strong>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($maintenance['description']): ?>
|
||||
<div class="ic-meta"><?= nl2br(mnt_h($maintenance['description'])) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ((int)$maintenance['file_count'] > 0): ?>
|
||||
<div class="ic-meta"><i class="fa-solid fa-paperclip"></i> <?= (int)$maintenance['file_count'] ?> istruzioni</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-1 flex-shrink-0">
|
||||
<?php if ($canManage): ?>
|
||||
<button class="btn-action btn-action-done btn-register" title="Registra intervento">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
<button class="btn-action btn-action-edit btn-edit-maintenance" title="Modifica">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
<button class="btn-action btn-action-delete btn-delete-maintenance"
|
||||
title="Elimina" data-title="<?= mnt_h($maintenance['title']) ?>"
|
||||
data-count="<?= (int)$maintenance['intervention_count'] ?>">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($interventions): ?>
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-sm btn-light w-100 btn-toggle-history" type="button">
|
||||
<i class="fa-solid fa-clock-rotate-left me-1"></i>
|
||||
Storico interventi (<?= count($interventions) ?>)
|
||||
</button>
|
||||
<div class="mnt-timeline mt-3" style="display:none">
|
||||
<?php foreach ($interventions as $intervention): ?>
|
||||
<div class="mnt-timeline-item" data-intervention-id="<?= (int)$intervention['id'] ?>">
|
||||
<div class="tl-date">
|
||||
<?= mnt_format_date($intervention['performed_at']) ?>
|
||||
· <?= mnt_h($statusLabels[$intervention['status']] ?? $intervention['status']) ?>
|
||||
<?php if ($intervention['result']): ?>
|
||||
· <?= mnt_h($resultLabels[$intervention['result']] ?? $intervention['result']) ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="tl-body">
|
||||
<?php
|
||||
$operator = $intervention['operator_full_name'] ?: $intervention['operator_name'];
|
||||
?>
|
||||
<?php if ($operator): ?>
|
||||
<strong><?= mnt_h($operator) ?></strong>
|
||||
<?php endif; ?>
|
||||
<?php if ($intervention['supplier_name']): ?>
|
||||
<span class="text-muted">(<?= mnt_h($intervention['supplier_name']) ?>)</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($intervention['notes']): ?>
|
||||
<div><?= nl2br(mnt_h($intervention['notes'])) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($intervention['materials']): ?>
|
||||
<div class="small text-muted">Materiali: <?= mnt_h($intervention['materials']) ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex align-items-center gap-2 mt-1 flex-wrap">
|
||||
<?php if ($intervention['signature_path']): ?>
|
||||
<img src="manutenzioni/ajax/download_file.php?scope=signature&id=<?= (int)$intervention['id'] ?>"
|
||||
alt="Firma" style="height:38px;border:1px solid #e6eef0;border-radius:.3rem;background:#fff">
|
||||
<?php endif; ?>
|
||||
<?php if ((int)$intervention['file_count'] > 0): ?>
|
||||
<span class="small text-muted"><i class="fa-solid fa-paperclip"></i> <?= (int)$intervention['file_count'] ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($canManage): ?>
|
||||
<button class="btn-action btn-action-edit btn-edit-intervention" title="Modifica">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
<button class="btn-action btn-action-delete btn-delete-intervention" title="Elimina">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php include(__DIR__ . '/../include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php if ($canManage): ?>
|
||||
<?php include __DIR__ . '/include/equipment_modal.php'; ?>
|
||||
<?php include __DIR__ . '/include/maintenance_modal.php'; ?>
|
||||
<?php include __DIR__ . '/include/intervention_modal.php'; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php include(__DIR__ . '/../jsinclude.php'); ?>
|
||||
<script>
|
||||
$(function () {
|
||||
const EQUIPMENT_ID = <?= (int)$equipment['id'] ?>;
|
||||
const EQUIPMENT_NAME = <?= json_encode($equipment['name'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
|
||||
$('#maintenanceList').on('click', '.btn-toggle-history', function () {
|
||||
$(this).next('.mnt-timeline').slideToggle(150);
|
||||
});
|
||||
|
||||
<?php if ($canManage): ?>
|
||||
// ---------------------------------------------------- equipment
|
||||
$('#btnEditEquipment').on('click', function () {
|
||||
$.getJSON('manutenzioni/ajax/get_equipment.php', { id: EQUIPMENT_ID })
|
||||
.done(res => {
|
||||
if (res.success) { mntOpenEquipmentModal(res.equipment); }
|
||||
else { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); }
|
||||
});
|
||||
});
|
||||
|
||||
function uploadEquipmentFiles(input, kind) {
|
||||
if (!input.files.length) return;
|
||||
|
||||
const form = new FormData();
|
||||
form.append('scope', 'equipment');
|
||||
form.append('owner_id', EQUIPMENT_ID);
|
||||
form.append('kind', kind);
|
||||
Array.from(input.files).forEach(f => form.append('files[]', f));
|
||||
|
||||
$.ajax({
|
||||
url: 'manutenzioni/ajax/upload_file.php',
|
||||
method: 'POST', data: form, processData: false, contentType: false, dataType: 'json'
|
||||
}).done(res => {
|
||||
if (res.success) { location.reload(); }
|
||||
else { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); }
|
||||
}).fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
|
||||
}
|
||||
|
||||
$('#btnAddPhotos').on('click', () => $('#photoInput').click());
|
||||
$('#photoInput').on('change', function () { uploadEquipmentFiles(this, 'photo'); });
|
||||
$('#btnAddDocs').on('click', () => $('#docInput').click());
|
||||
$('#docInput').on('change', function () { uploadEquipmentFiles(this, 'file'); });
|
||||
|
||||
$('.btn-set-cover').on('click', function () {
|
||||
$.post('manutenzioni/ajax/set_cover.php', { file_id: $(this).data('id') })
|
||||
.done(res => res.success ? location.reload() : Swal.fire({ icon: 'error', title: 'Errore', text: res.message }));
|
||||
});
|
||||
|
||||
$('.btn-del-photo, .btn-del-doc').on('click', function () {
|
||||
const id = $(this).data('id');
|
||||
Swal.fire({
|
||||
title: 'Eliminare il file?', icon: 'warning', showCancelButton: true,
|
||||
confirmButtonText: 'Elimina', cancelButtonText: 'Annulla', confirmButtonColor: '#dc3545'
|
||||
}).then(r => {
|
||||
if (!r.isConfirmed) return;
|
||||
$.post('manutenzioni/ajax/delete_file.php', { scope: 'equipment', id: id })
|
||||
.done(res => res.success ? location.reload() : Swal.fire({ icon: 'error', title: 'Errore', text: res.message }));
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------- maintenance
|
||||
$('#btnAddMaintenance').on('click', function () {
|
||||
mntOpenMaintenanceModal(null, EQUIPMENT_ID);
|
||||
});
|
||||
|
||||
$('#maintenanceList').on('click', '.btn-edit-maintenance', function () {
|
||||
const id = $(this).closest('[data-id]').data('id');
|
||||
$.getJSON('manutenzioni/ajax/get_maintenance.php', { id: id })
|
||||
.done(res => {
|
||||
if (!res.success) { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); return; }
|
||||
const data = res.maintenance;
|
||||
data.files = res.files;
|
||||
mntOpenMaintenanceModal(data, EQUIPMENT_ID);
|
||||
});
|
||||
});
|
||||
|
||||
$('#maintenanceList').on('click', '.btn-delete-maintenance', function () {
|
||||
const $btn = $(this);
|
||||
const id = $btn.closest('[data-id]').data('id');
|
||||
const count = parseInt($btn.data('count') || 0, 10);
|
||||
|
||||
Swal.fire({
|
||||
title: `Eliminare "${$btn.data('title')}"?`,
|
||||
html: count > 0 ? `<p>Verranno eliminati anche <strong>${count}</strong> interventi registrati.</p>` : '',
|
||||
icon: 'warning', showCancelButton: true,
|
||||
confirmButtonText: 'Elimina', cancelButtonText: 'Annulla', confirmButtonColor: '#dc3545'
|
||||
}).then(r => {
|
||||
if (!r.isConfirmed) return;
|
||||
$.post('manutenzioni/ajax/delete_maintenance.php', { id: id })
|
||||
.done(res => res.success ? location.reload() : Swal.fire({ icon: 'error', title: 'Errore', text: res.message }));
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------- interventions
|
||||
$('#maintenanceList').on('click', '.btn-register', function () {
|
||||
const id = $(this).closest('[data-id]').data('id');
|
||||
$.getJSON('manutenzioni/ajax/get_maintenance.php', { id: id })
|
||||
.done(res => {
|
||||
if (!res.success) { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); return; }
|
||||
mntOpenInterventionModal(null, {
|
||||
id: res.maintenance.id,
|
||||
title: res.maintenance.title,
|
||||
equipment_name: EQUIPMENT_NAME,
|
||||
supplier_id: res.maintenance.supplier_id
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('#maintenanceList').on('click', '.btn-edit-intervention', function () {
|
||||
const interventionId = $(this).closest('[data-intervention-id]').data('intervention-id');
|
||||
const maintenanceId = $(this).closest('[data-id]').data('id');
|
||||
|
||||
$.getJSON('manutenzioni/ajax/get_interventions.php', { id: interventionId })
|
||||
.done(res => {
|
||||
if (!res.success) { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); return; }
|
||||
const data = res.intervention;
|
||||
data.files = res.files;
|
||||
mntOpenInterventionModal(data, {
|
||||
id: maintenanceId,
|
||||
title: '',
|
||||
equipment_name: EQUIPMENT_NAME
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('#maintenanceList').on('click', '.btn-delete-intervention', function () {
|
||||
const interventionId = $(this).closest('[data-intervention-id]').data('intervention-id');
|
||||
Swal.fire({
|
||||
title: 'Eliminare l\'intervento?',
|
||||
text: 'La data della prossima manutenzione verrà ricalcolata.',
|
||||
icon: 'warning', showCancelButton: true,
|
||||
confirmButtonText: 'Elimina', cancelButtonText: 'Annulla', confirmButtonColor: '#dc3545'
|
||||
}).then(r => {
|
||||
if (!r.isConfirmed) return;
|
||||
$.post('manutenzioni/ajax/delete_intervention.php', { id: interventionId })
|
||||
.done(res => res.success ? location.reload() : Swal.fire({ icon: 'error', title: 'Errore', text: res.message }));
|
||||
});
|
||||
});
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Create/edit modal for a registry item.
|
||||
* Expects $formData (mnt_form_data) in scope. Defines mntOpenEquipmentModal(data).
|
||||
*/
|
||||
|
||||
$statuses = $statuses ?? mnt_equipment_statuses();
|
||||
?>
|
||||
<div class="modal fade" id="equipmentModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered modal-fullscreen-sm-down">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="equipmentModalTitle">Nuova attrezzatura</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<form id="equipmentForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="id" id="eqId" value="">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Nome <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="name" id="eqName" maxlength="100" required>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Categoria <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="category_id" id="eqCategory" required>
|
||||
<option value="">— seleziona —</option>
|
||||
<?php foreach ($formData['categories'] as $category): ?>
|
||||
<option value="<?= (int)$category['id'] ?>" data-requires-line="<?= (int)$category['requires_line'] ?>">
|
||||
<?= mnt_h($category['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6" id="eqLineWrap" style="display:none">
|
||||
<label class="form-label fw-semibold">Linea di produzione <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="line_id" id="eqLine">
|
||||
<option value="">— seleziona —</option>
|
||||
<?php foreach ($formData['lines'] as $line): ?>
|
||||
<option value="<?= (int)$line['id'] ?>">
|
||||
Linea <?= (int)$line['line_number'] ?> — <?= mnt_h($line['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Matricola</label>
|
||||
<input type="text" class="form-control" name="registration_number" id="eqRegistration" maxlength="100">
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Numero di serie</label>
|
||||
<input type="text" class="form-control" name="serial_number" id="eqSerial" maxlength="100">
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Lotto / partita</label>
|
||||
<input type="text" class="form-control" name="batch_lot" id="eqBatch" maxlength="100">
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Costruttore</label>
|
||||
<input type="text" class="form-control" name="manufacturer" id="eqManufacturer" maxlength="150">
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Data di acquisto</label>
|
||||
<input type="date" class="form-control" name="purchase_date" id="eqPurchase">
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Data di messa in servizio</label>
|
||||
<input type="date" class="form-control" name="commissioning_date" id="eqCommissioning">
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Tipo</label>
|
||||
<input type="text" class="form-control" name="tool_type" id="eqType" maxlength="50">
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Stato</label>
|
||||
<select class="form-select" name="status" id="eqStatus">
|
||||
<?php foreach ($statuses as $value => $label): ?>
|
||||
<option value="<?= mnt_h($value) ?>"><?= mnt_h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Reparto</label>
|
||||
<select class="form-select" name="department_id" id="eqDepartment">
|
||||
<option value="">—</option>
|
||||
<?php foreach ($formData['departments'] as $department): ?>
|
||||
<option value="<?= (int)$department['id'] ?>"><?= mnt_h($department['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Ubicazione</label>
|
||||
<input type="text" class="form-control" name="location" id="eqLocation" maxlength="255">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Descrizione</label>
|
||||
<textarea class="form-control" name="description" id="eqDescription" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Note</label>
|
||||
<textarea class="form-control" name="notes" id="eqNotes" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-mnt-primary">Salva</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Shows the line picker only for categories flagged requires_line
|
||||
function mntSyncLineVisibility() {
|
||||
const requiresLine = $('#eqCategory option:selected').data('requires-line') == 1;
|
||||
$('#eqLineWrap').toggle(requiresLine);
|
||||
$('#eqLine').prop('required', requiresLine);
|
||||
if (!requiresLine) { $('#eqLine').val(''); }
|
||||
}
|
||||
|
||||
function mntOpenEquipmentModal(data) {
|
||||
const isEdit = !!data;
|
||||
$('#equipmentModalTitle').text(isEdit ? 'Modifica attrezzatura' : 'Nuova attrezzatura');
|
||||
|
||||
$('#eqId').val(isEdit ? data.id : '');
|
||||
$('#eqName').val(isEdit ? (data.name || '') : '');
|
||||
$('#eqCategory').val(isEdit && data.category_id ? String(data.category_id) : '');
|
||||
$('#eqLine').val(isEdit && data.line_id ? String(data.line_id) : '');
|
||||
$('#eqRegistration').val(isEdit ? (data.registration_number || '') : '');
|
||||
$('#eqSerial').val(isEdit ? (data.serial_number || '') : '');
|
||||
$('#eqBatch').val(isEdit ? (data.batch_lot || '') : '');
|
||||
$('#eqManufacturer').val(isEdit ? (data.manufacturer || '') : '');
|
||||
$('#eqPurchase').val(isEdit ? (data.purchase_date || '') : '');
|
||||
$('#eqCommissioning').val(isEdit ? (data.commissioning_date || '') : '');
|
||||
$('#eqType').val(isEdit ? (data.tool_type || '') : '');
|
||||
$('#eqStatus').val(isEdit ? (data.status || 'active') : 'active');
|
||||
$('#eqDepartment').val(isEdit && data.department_id ? String(data.department_id) : '');
|
||||
$('#eqLocation').val(isEdit ? (data.location || '') : '');
|
||||
$('#eqDescription').val(isEdit ? (data.description || '') : '');
|
||||
$('#eqNotes').val(isEdit ? (data.notes || '') : '');
|
||||
|
||||
mntSyncLineVisibility();
|
||||
new bootstrap.Modal('#equipmentModal').show();
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$('#eqCategory').on('change', mntSyncLineVisibility);
|
||||
|
||||
$('#equipmentForm').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const name = $('#eqName').val().trim();
|
||||
if (!name) { Swal.fire({ icon: 'warning', title: 'Il nome è obbligatorio' }); return; }
|
||||
if (!$('#eqCategory').val()) { Swal.fire({ icon: 'warning', title: 'Seleziona una categoria' }); return; }
|
||||
if ($('#eqLineWrap').is(':visible') && !$('#eqLine').val()) {
|
||||
Swal.fire({ icon: 'warning', title: 'Seleziona la linea di produzione' });
|
||||
return;
|
||||
}
|
||||
|
||||
$.post('manutenzioni/ajax/save_equipment.php', $(this).serialize())
|
||||
.done(res => {
|
||||
if (res.success) {
|
||||
if (window.mntAfterEquipmentSave) { window.mntAfterEquipmentSave(res); }
|
||||
else { location.reload(); }
|
||||
} else {
|
||||
Swal.fire({ icon: 'error', title: 'Errore', text: res.message });
|
||||
}
|
||||
})
|
||||
.fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,483 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Manutenzioni — shared helpers.
|
||||
*
|
||||
* Included by every page and ajax endpoint of the module. Assumes a PDO
|
||||
* connection is available through DBHandlerSelect (class/db-functions.php).
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../class/db-functions.php';
|
||||
|
||||
if (!function_exists('mnt_pdo')) {
|
||||
function mnt_pdo(): PDO
|
||||
{
|
||||
return DBHandlerSelect::getInstance()->getConnection();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('mnt_h')) {
|
||||
function mnt_h($value): string
|
||||
{
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal HTML email body, same visual language as the scadenzario mails.
|
||||
*
|
||||
* Lives here rather than in cron/send_notifications.php so that the preview
|
||||
* (make mail-preview-html) renders exactly what the cron sends, instead of a
|
||||
* copy that drifts.
|
||||
*/
|
||||
if (!function_exists('mnt_mail_body')) {
|
||||
function mnt_mail_body(string $heading, string $subject, string $message, string $color, string $url): string
|
||||
{
|
||||
$safeHeading = htmlspecialchars($heading, ENT_QUOTES, 'UTF-8');
|
||||
$safeSubject = htmlspecialchars($subject, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
return '<!doctype html><html><body style="margin:0;padding:24px;background:#f4f6f8;font-family:Arial,Helvetica,sans-serif;color:#1e3a44">'
|
||||
. '<div style="max-width:560px;margin:0 auto;background:#fff;border-radius:10px;overflow:hidden;box-shadow:0 2px 10px rgba(0,0,0,.06)">'
|
||||
. '<div style="background:' . $color . ';color:#fff;padding:16px 20px;font-size:17px;font-weight:bold">' . $safeHeading . '</div>'
|
||||
. '<div style="padding:20px">'
|
||||
. '<p style="font-size:15px;font-weight:bold;margin:0 0 10px">' . $safeSubject . '</p>'
|
||||
. '<p style="font-size:14px;line-height:1.55;margin:0 0 18px">' . $message . '</p>'
|
||||
. '<a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" style="display:inline-block;background:#2f7d8f;color:#fff;text-decoration:none;padding:10px 18px;border-radius:6px;font-size:14px">Apri la scheda</a>'
|
||||
. '</div></div></body></html>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subject and body of a notification, for both kinds.
|
||||
*
|
||||
* @param array $maintenance needs equipment_name, next_due_date, is_critical
|
||||
* @return array{subject:string, body:string}
|
||||
*/
|
||||
if (!function_exists('mnt_notification_mail')) {
|
||||
function mnt_notification_mail(array $maintenance, string $label, int $daysLeft, string $detailUrl, ?string $today = null): array
|
||||
{
|
||||
$today = $today ?? date('Y-m-d');
|
||||
$isOverdue = $maintenance['next_due_date'] < $today;
|
||||
$due = date('d/m/Y', strtotime($maintenance['next_due_date']));
|
||||
|
||||
$criticalNote = (int)($maintenance['is_critical'] ?? 0) === 1
|
||||
? '<br><strong style="color:#b02a37">Manutenzione critica.</strong>'
|
||||
: '';
|
||||
|
||||
if ($isOverdue) {
|
||||
return [
|
||||
'subject' => '⚠️ Manutenzione scaduta: ' . $maintenance['equipment_name'],
|
||||
'body' => mnt_mail_body(
|
||||
'Manutenzione scaduta',
|
||||
$maintenance['equipment_name'] . ' — ' . $label,
|
||||
'Era prevista per il <strong>' . $due . '</strong>, in ritardo di <strong>'
|
||||
. abs($daysLeft) . ' giorni</strong>.' . $criticalNote,
|
||||
'#dc3545',
|
||||
$detailUrl
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
$daysText = $daysLeft === 0 ? '<strong>oggi</strong>' : 'tra <strong>' . $daysLeft . ' giorni</strong>';
|
||||
|
||||
return [
|
||||
'subject' => '🔧 Manutenzione in scadenza: ' . $maintenance['equipment_name'],
|
||||
'body' => mnt_mail_body(
|
||||
'Manutenzione in scadenza',
|
||||
$maintenance['equipment_name'] . ' — ' . $label,
|
||||
'È prevista per il <strong>' . $due . '</strong> (' . $daysText . ').' . $criticalNote,
|
||||
'#2f7d8f',
|
||||
$detailUrl
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads ?page and ?per_page for the paged lists.
|
||||
*
|
||||
* per_page is a whitelist, not a free number: it lands in a LIMIT clause and
|
||||
* an open value would let anyone ask for the whole table in one request.
|
||||
* Returns [page, perPage] with page still unbounded above — the caller clamps
|
||||
* it once it knows how many rows the filters actually match.
|
||||
*/
|
||||
if (!function_exists('mnt_page_params')) {
|
||||
function mnt_page_params(int $default = 25): array
|
||||
{
|
||||
$allowed = [25, 50, 100];
|
||||
$perPage = isset($_GET['per_page']) ? (int)$_GET['per_page'] : $default;
|
||||
if (!in_array($perPage, $allowed, true)) {
|
||||
$perPage = $default;
|
||||
}
|
||||
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
|
||||
return [max(1, $page), $perPage];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Frequency units offered in the maintenance form.
|
||||
* 'on_demand' = no automatic due date (the client's "al bisogno" cases).
|
||||
*/
|
||||
if (!function_exists('mnt_frequency_units')) {
|
||||
function mnt_frequency_units(): array
|
||||
{
|
||||
return [
|
||||
'day' => 'Giorni',
|
||||
'week' => 'Settimane',
|
||||
'month' => 'Mesi',
|
||||
'year' => 'Anni',
|
||||
'on_demand' => 'Al bisogno (senza scadenza automatica)',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('mnt_intervention_types')) {
|
||||
function mnt_intervention_types(): array
|
||||
{
|
||||
return ['scheduled' => 'Programmata', 'extraordinary' => 'Straordinaria'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('mnt_execution_types')) {
|
||||
function mnt_execution_types(): array
|
||||
{
|
||||
return ['internal' => 'Interna', 'external' => 'Esterna'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('mnt_equipment_statuses')) {
|
||||
function mnt_equipment_statuses(): array
|
||||
{
|
||||
return [
|
||||
'active' => 'Attivo',
|
||||
'out_of_service' => 'Fuori servizio',
|
||||
'decommissioned' => 'Dismesso',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options managed through the existing lookup_values.php CRUD.
|
||||
* Falls back to sensible defaults if the category has no rows yet.
|
||||
*/
|
||||
if (!function_exists('mnt_lookup')) {
|
||||
function mnt_lookup(PDO $pdo, string $category, array $fallback = []): array
|
||||
{
|
||||
static $cache = [];
|
||||
|
||||
if (!isset($cache[$category])) {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT value, label
|
||||
FROM ws_lookup_options
|
||||
WHERE category = ? AND is_active = 1
|
||||
ORDER BY sort_order ASC, label ASC
|
||||
");
|
||||
$stmt->execute([$category]);
|
||||
|
||||
$options = [];
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$options[$row['value']] = $row['label'];
|
||||
}
|
||||
|
||||
$cache[$category] = $options ?: $fallback;
|
||||
}
|
||||
|
||||
return $cache[$category];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Next due date = last completed intervention + frequency.
|
||||
* Returns null when the maintenance has no automatic schedule or was never done.
|
||||
*/
|
||||
if (!function_exists('mnt_calc_next_due')) {
|
||||
function mnt_calc_next_due(?string $lastDoneDate, ?int $frequencyValue, string $frequencyUnit): ?string
|
||||
{
|
||||
if ($lastDoneDate === null || $frequencyUnit === 'on_demand' || !$frequencyValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$map = ['day' => 'day', 'week' => 'week', 'month' => 'month', 'year' => 'year'];
|
||||
if (!isset($map[$frequencyUnit])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$date = date_create($lastDoneDate);
|
||||
if (!$date) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$dayOfMonth = (int)$date->format('j');
|
||||
$date->modify('+' . $frequencyValue . ' ' . $map[$frequencyUnit]);
|
||||
|
||||
// PHP fa traboccare i mesi corti: 31/01 +1 mese diventa 03/03.
|
||||
// Per una manutenzione la data attesa è l'ultimo giorno del mese.
|
||||
if (in_array($frequencyUnit, ['month', 'year'], true) && (int)$date->format('j') !== $dayOfMonth) {
|
||||
$date->modify('last day of previous month');
|
||||
}
|
||||
|
||||
return $date->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recompute last_done_date / next_due_date of a maintenance from its
|
||||
* completed interventions. Call after any intervention insert/update/delete.
|
||||
*/
|
||||
if (!function_exists('mnt_recalc_maintenance')) {
|
||||
function mnt_recalc_maintenance(PDO $pdo, int $maintenanceId): void
|
||||
{
|
||||
$stmt = $pdo->prepare("SELECT frequency_value, frequency_unit FROM maint_maintenances WHERE id = ?");
|
||||
$stmt->execute([$maintenanceId]);
|
||||
$maintenance = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$maintenance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only completed interventions move the schedule; planned/in-progress do not.
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT MAX(performed_at)
|
||||
FROM maint_interventions
|
||||
WHERE maintenance_id = ? AND status = 'completed'
|
||||
");
|
||||
$stmt->execute([$maintenanceId]);
|
||||
$lastDone = $stmt->fetchColumn() ?: null;
|
||||
|
||||
$nextDue = mnt_calc_next_due(
|
||||
$lastDone,
|
||||
$maintenance['frequency_value'] !== null ? (int)$maintenance['frequency_value'] : null,
|
||||
(string)$maintenance['frequency_unit']
|
||||
);
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE maint_maintenances SET last_done_date = ?, next_due_date = ? WHERE id = ?");
|
||||
$stmt->execute([$lastDone, $nextDue, $maintenanceId]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule state of a maintenance, used for badges and filters.
|
||||
* Returns one of: overdue | due_soon | ok | none
|
||||
*/
|
||||
if (!function_exists('mnt_due_state')) {
|
||||
function mnt_due_state(?string $nextDueDate, int $alertDays = 0, ?string $today = null): string
|
||||
{
|
||||
if (empty($nextDueDate)) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
$today = $today ?: date('Y-m-d');
|
||||
|
||||
if ($nextDueDate < $today) {
|
||||
return 'overdue';
|
||||
}
|
||||
|
||||
$threshold = date('Y-m-d', strtotime($today . ' +' . max(0, $alertDays) . ' days'));
|
||||
|
||||
return $nextDueDate <= $threshold ? 'due_soon' : 'ok';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('mnt_due_badge')) {
|
||||
/**
|
||||
* Lo stato "none" copre due casi diversi che non vanno confusi:
|
||||
* una manutenzione al bisogno (per scelta senza scadenza) e una
|
||||
* programmata mai eseguita (la scadenza arriverà al primo intervento).
|
||||
*/
|
||||
function mnt_due_badge(string $state, ?string $frequencyUnit = null): array
|
||||
{
|
||||
switch ($state) {
|
||||
case 'overdue':
|
||||
return ['label' => 'Scaduta', 'class' => 'mnt-badge-overdue'];
|
||||
case 'due_soon':
|
||||
return ['label' => 'In scadenza', 'class' => 'mnt-badge-soon'];
|
||||
case 'ok':
|
||||
return ['label' => 'In regola', 'class' => 'mnt-badge-ok'];
|
||||
default:
|
||||
if ($frequencyUnit !== null && $frequencyUnit !== 'on_demand') {
|
||||
return ['label' => 'Mai eseguita', 'class' => 'mnt-badge-none'];
|
||||
}
|
||||
|
||||
return ['label' => 'Al bisogno', 'class' => 'mnt-badge-none'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('mnt_format_date')) {
|
||||
function mnt_format_date(?string $date): string
|
||||
{
|
||||
if (empty($date) || $date === '0000-00-00') {
|
||||
return '—';
|
||||
}
|
||||
|
||||
$ts = strtotime($date);
|
||||
|
||||
return $ts ? date('d/m/Y', $ts) : '—';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable frequency ("Ogni 3 mesi", "Al bisogno").
|
||||
*/
|
||||
if (!function_exists('mnt_format_frequency')) {
|
||||
function mnt_format_frequency(?int $value, string $unit, ?string $note = null): string
|
||||
{
|
||||
$units = [
|
||||
'day' => ['giorno', 'giorni'],
|
||||
'week' => ['settimana', 'settimane'],
|
||||
'month' => ['mese', 'mesi'],
|
||||
'year' => ['anno', 'anni'],
|
||||
];
|
||||
|
||||
if ($unit === 'on_demand' || !$value || !isset($units[$unit])) {
|
||||
$text = 'Al bisogno';
|
||||
} else {
|
||||
$word = $value === 1 ? $units[$unit][0] : $units[$unit][1];
|
||||
$text = 'Ogni ' . $value . ' ' . $word;
|
||||
}
|
||||
|
||||
if (!empty($note)) {
|
||||
$text .= ' — ' . $note;
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit log entry. All ids are optional so the same call works for
|
||||
* equipment, maintenance and intervention events.
|
||||
*/
|
||||
if (!function_exists('mnt_log')) {
|
||||
function mnt_log(
|
||||
PDO $pdo,
|
||||
string $action,
|
||||
?int $equipmentId = null,
|
||||
?int $maintenanceId = null,
|
||||
?int $interventionId = null,
|
||||
?int $userId = null,
|
||||
?array $changes = null,
|
||||
?string $notes = null
|
||||
): void {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO maint_histories
|
||||
(equipment_id, maintenance_id, intervention_id, user_id, action, changes, notes)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$equipmentId,
|
||||
$maintenanceId,
|
||||
$interventionId,
|
||||
$userId,
|
||||
$action,
|
||||
$changes !== null ? json_encode($changes, JSON_UNESCAPED_UNICODE) : null,
|
||||
$notes,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store one uploaded file into attachments/ and return its metadata.
|
||||
* Throws RuntimeException on a rejected file.
|
||||
*/
|
||||
if (!function_exists('mnt_store_upload')) {
|
||||
function mnt_store_upload(array $file, string $prefix = 'mnt'): array
|
||||
{
|
||||
$allowedExtensions = [
|
||||
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'txt', 'odt', 'ods',
|
||||
'jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'bmp', 'zip',
|
||||
];
|
||||
$maxBytes = 20 * 1024 * 1024;
|
||||
|
||||
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
throw new RuntimeException('Errore durante il caricamento del file.');
|
||||
}
|
||||
|
||||
if (($file['size'] ?? 0) > $maxBytes) {
|
||||
throw new RuntimeException('File troppo grande (massimo 20 MB).');
|
||||
}
|
||||
|
||||
$originalName = (string)($file['name'] ?? 'file');
|
||||
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||||
|
||||
if (!in_array($extension, $allowedExtensions, true)) {
|
||||
throw new RuntimeException('Tipo di file non consentito: .' . $extension);
|
||||
}
|
||||
|
||||
$uploadDir = __DIR__ . '/../attachments/';
|
||||
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true) && !is_dir($uploadDir)) {
|
||||
throw new RuntimeException('Cartella allegati non scrivibile.');
|
||||
}
|
||||
|
||||
$storedName = uniqid($prefix . '_', true) . '.' . $extension;
|
||||
$safeStored = preg_replace('/[^A-Za-z0-9._-]/', '_', $storedName);
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $uploadDir . $safeStored)) {
|
||||
throw new RuntimeException('Impossibile salvare il file.');
|
||||
}
|
||||
|
||||
return [
|
||||
'original_name' => $originalName,
|
||||
'stored_name' => $safeStored,
|
||||
'mime_type' => $file['type'] ?? null,
|
||||
'size' => (int)($file['size'] ?? 0),
|
||||
'is_image' => in_array($extension, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'], true),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('mnt_delete_stored_file')) {
|
||||
function mnt_delete_stored_file(string $storedName): void
|
||||
{
|
||||
$path = __DIR__ . '/../attachments/' . basename($storedName);
|
||||
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Employees / suppliers / departments pickers, shared by several forms.
|
||||
*/
|
||||
if (!function_exists('mnt_form_data')) {
|
||||
function mnt_form_data(PDO $pdo): array
|
||||
{
|
||||
return [
|
||||
'categories' => $pdo->query("
|
||||
SELECT id, name, color, requires_line
|
||||
FROM inv_categories
|
||||
WHERE is_active = 1
|
||||
ORDER BY sort_order ASC, name ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC),
|
||||
|
||||
'lines' => $pdo->query("
|
||||
SELECT id, line_number, name, color
|
||||
FROM production_lines
|
||||
ORDER BY line_number ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC),
|
||||
|
||||
'departments' => $pdo->query("
|
||||
SELECT id, name
|
||||
FROM departments
|
||||
WHERE is_active = 1
|
||||
ORDER BY sort_order ASC, name ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC),
|
||||
|
||||
'employees' => $pdo->query("
|
||||
SELECT id, CONCAT(first_name, ' ', last_name) AS full_name
|
||||
FROM employees
|
||||
WHERE status = 'active'
|
||||
ORDER BY first_name ASC, last_name ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC),
|
||||
|
||||
'suppliers' => $pdo->query("
|
||||
SELECT idsupplier AS id, supplier_name AS name
|
||||
FROM suppliers
|
||||
ORDER BY supplier_name ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Register/edit one intervention.
|
||||
* Expects $formData and $pdo in scope. Defines mntOpenInterventionModal(data, maintenance).
|
||||
*
|
||||
* The signature is drawn on a canvas (finger on tablet, mouse on PC) and
|
||||
* posted as a PNG data URL.
|
||||
*/
|
||||
|
||||
$statusOptions = mnt_lookup($pdo, 'maint_status', [
|
||||
'planned' => 'Pianificato',
|
||||
'in_progress' => 'In corso',
|
||||
'completed' => 'Completato',
|
||||
]);
|
||||
|
||||
$resultOptions = mnt_lookup($pdo, 'maint_result', [
|
||||
'compliant' => 'Conforme',
|
||||
'not_compliant' => 'Non conforme',
|
||||
'blocked' => 'Bloccato',
|
||||
]);
|
||||
?>
|
||||
<div class="modal fade" id="interventionModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered modal-fullscreen-sm-down">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="interventionModalTitle">Registra intervento</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<form id="interventionForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="id" id="ivId" value="">
|
||||
<input type="hidden" name="maintenance_id" id="ivMaintenanceId" value="">
|
||||
<input type="hidden" name="signature" id="ivSignature" value="">
|
||||
<input type="hidden" name="clear_signature" id="ivClearSignature" value="">
|
||||
|
||||
<div class="alert alert-light border small mb-3" id="ivContext"></div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label fw-semibold">Data <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" name="performed_at" id="ivDate" required>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-4">
|
||||
<label class="form-label fw-semibold">Stato</label>
|
||||
<select class="form-select" name="status" id="ivStatus">
|
||||
<?php foreach ($statusOptions as $value => $label): ?>
|
||||
<option value="<?= mnt_h($value) ?>" <?= $value === 'completed' ? 'selected' : '' ?>>
|
||||
<?= mnt_h($label) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-4">
|
||||
<label class="form-label fw-semibold">Esito</label>
|
||||
<select class="form-select" name="result" id="ivResult">
|
||||
<option value="">—</option>
|
||||
<?php foreach ($resultOptions as $value => $label): ?>
|
||||
<option value="<?= mnt_h($value) ?>"><?= mnt_h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Operatore</label>
|
||||
<select class="form-select" name="operator_employee_id" id="ivOperator">
|
||||
<option value="">—</option>
|
||||
<?php foreach ($formData['employees'] as $employee): ?>
|
||||
<option value="<?= (int)$employee['id'] ?>"><?= mnt_h($employee['full_name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Operatore esterno</label>
|
||||
<input type="text" class="form-control" name="operator_name" id="ivOperatorName" maxlength="191"
|
||||
placeholder="Nome tecnico non presente in anagrafica">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Fornitore</label>
|
||||
<select class="form-select" name="supplier_id" id="ivSupplier">
|
||||
<option value="">—</option>
|
||||
<?php foreach ($formData['suppliers'] as $supplier): ?>
|
||||
<option value="<?= (int)$supplier['id'] ?>"><?= mnt_h($supplier['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Note sull'intervento</label>
|
||||
<textarea class="form-control" name="notes" id="ivNotes" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Materiali / parti sostituite</label>
|
||||
<textarea class="form-control" name="materials" id="ivMaterials" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Firma operatore</label>
|
||||
<div class="mnt-signature-wrap">
|
||||
<canvas id="ivSignatureCanvas"></canvas>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mt-1">
|
||||
<span class="form-text mb-0">Firma con il dito su tablet o con il mouse.</span>
|
||||
<button type="button" class="btn btn-sm btn-light" id="ivSignatureClear">Cancella firma</button>
|
||||
</div>
|
||||
<div id="ivExistingSignature" class="mt-2" style="display:none">
|
||||
<span class="mnt-section-title">Firma registrata</span>
|
||||
<img id="ivExistingSignatureImg" alt="Firma" style="max-height:90px;border:1px solid #e6eef0;border-radius:.4rem;background:#fff">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Allegati (modulo del fornitore, foto)</label>
|
||||
<div id="ivFilesList" class="mb-2"></div>
|
||||
<input type="file" class="form-control" id="ivFilesInput" multiple>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-mnt-primary">Salva intervento</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
let canvas, ctx, drawing = false, hasStrokes = false;
|
||||
|
||||
function resizeCanvas() {
|
||||
if (!canvas) return;
|
||||
const ratio = window.devicePixelRatio || 1;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
// Preserve what is already drawn across a resize
|
||||
const snapshot = hasStrokes ? canvas.toDataURL() : null;
|
||||
|
||||
canvas.width = rect.width * ratio;
|
||||
canvas.height = rect.height * ratio;
|
||||
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = '#1e3a44';
|
||||
|
||||
if (snapshot) {
|
||||
const img = new Image();
|
||||
img.onload = () => ctx.drawImage(img, 0, 0, rect.width, rect.height);
|
||||
img.src = snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
function pointerPos(event) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const source = event.touches ? event.touches[0] : event;
|
||||
return { x: source.clientX - rect.left, y: source.clientY - rect.top };
|
||||
}
|
||||
|
||||
function startDraw(event) {
|
||||
event.preventDefault();
|
||||
drawing = true;
|
||||
hasStrokes = true;
|
||||
const p = pointerPos(event);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p.x, p.y);
|
||||
}
|
||||
|
||||
function moveDraw(event) {
|
||||
if (!drawing) return;
|
||||
event.preventDefault();
|
||||
const p = pointerPos(event);
|
||||
ctx.lineTo(p.x, p.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function endDraw() { drawing = false; }
|
||||
|
||||
function clearCanvas() {
|
||||
if (!ctx) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
ctx.clearRect(0, 0, rect.width, rect.height);
|
||||
hasStrokes = false;
|
||||
}
|
||||
|
||||
window.mntOpenInterventionModal = function (data, maintenance) {
|
||||
const isEdit = !!data;
|
||||
|
||||
$('#interventionModalTitle').text(isEdit ? 'Modifica intervento' : 'Registra intervento');
|
||||
$('#ivContext').html(
|
||||
'<strong>' + $('<div>').text(maintenance.equipment_name || '').html() + '</strong> — ' +
|
||||
$('<div>').text(maintenance.title || '').html()
|
||||
);
|
||||
|
||||
$('#ivId').val(isEdit ? data.id : '');
|
||||
$('#ivMaintenanceId').val(maintenance.id);
|
||||
$('#ivDate').val(isEdit ? data.performed_at : new Date().toISOString().slice(0, 10));
|
||||
$('#ivStatus').val(isEdit ? data.status : 'completed');
|
||||
$('#ivResult').val(isEdit ? (data.result || '') : '');
|
||||
$('#ivOperator').val(isEdit && data.operator_employee_id ? String(data.operator_employee_id) : '');
|
||||
$('#ivOperatorName').val(isEdit ? (data.operator_name || '') : '');
|
||||
$('#ivSupplier').val(isEdit && data.supplier_id ? String(data.supplier_id) : (maintenance.supplier_id || ''));
|
||||
$('#ivNotes').val(isEdit ? (data.notes || '') : '');
|
||||
$('#ivMaterials').val(isEdit ? (data.materials || '') : '');
|
||||
$('#ivSignature').val('');
|
||||
$('#ivClearSignature').val('');
|
||||
$('#ivFilesInput').val('');
|
||||
$('#ivFilesList').empty();
|
||||
|
||||
if (isEdit && data.signature_path) {
|
||||
$('#ivExistingSignatureImg').attr('src', 'manutenzioni/ajax/download_file.php?scope=signature&id=' + data.id);
|
||||
$('#ivExistingSignature').show();
|
||||
} else {
|
||||
$('#ivExistingSignature').hide();
|
||||
}
|
||||
|
||||
if (isEdit && data.files) {
|
||||
data.files.forEach(f => {
|
||||
$('#ivFilesList').append(
|
||||
$('<div class="mnt-file-row"></div>').append(
|
||||
$('<i class="fa-solid fa-paperclip text-muted"></i>'),
|
||||
$('<a class="file-name" target="_blank"></a>')
|
||||
.attr('href', 'manutenzioni/ajax/download_file.php?scope=intervention&id=' + f.id)
|
||||
.text(f.original_name),
|
||||
$('<button type="button" class="btn-action btn-action-delete btn-del-iv-file"><i class="fa-solid fa-trash"></i></button>')
|
||||
.attr('data-id', f.id)
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const modal = new bootstrap.Modal('#interventionModal');
|
||||
modal.show();
|
||||
|
||||
$('#interventionModal').one('shown.bs.modal', function () {
|
||||
clearCanvas();
|
||||
resizeCanvas();
|
||||
});
|
||||
};
|
||||
|
||||
$(function () {
|
||||
canvas = document.getElementById('ivSignatureCanvas');
|
||||
if (!canvas) return;
|
||||
ctx = canvas.getContext('2d');
|
||||
|
||||
canvas.addEventListener('mousedown', startDraw);
|
||||
canvas.addEventListener('mousemove', moveDraw);
|
||||
window.addEventListener('mouseup', endDraw);
|
||||
canvas.addEventListener('touchstart', startDraw, { passive: false });
|
||||
canvas.addEventListener('touchmove', moveDraw, { passive: false });
|
||||
canvas.addEventListener('touchend', endDraw);
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
$('#ivSignatureClear').on('click', function () {
|
||||
clearCanvas();
|
||||
// Also drop a signature stored earlier
|
||||
if ($('#ivExistingSignature').is(':visible')) {
|
||||
$('#ivClearSignature').val('1');
|
||||
$('#ivExistingSignature').hide();
|
||||
}
|
||||
});
|
||||
|
||||
$('#ivFilesList').on('click', '.btn-del-iv-file', function () {
|
||||
const $row = $(this).closest('.mnt-file-row');
|
||||
$.post('manutenzioni/ajax/delete_file.php', { scope: 'intervention', id: $(this).data('id') })
|
||||
.done(res => {
|
||||
if (res.success) { $row.remove(); }
|
||||
else { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); }
|
||||
});
|
||||
});
|
||||
|
||||
$('#interventionForm').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!$('#ivDate').val()) {
|
||||
Swal.fire({ icon: 'warning', title: 'Indica la data dell\'intervento' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasStrokes) { $('#ivSignature').val(canvas.toDataURL('image/png')); }
|
||||
|
||||
$.post('manutenzioni/ajax/save_intervention.php', $(this).serialize())
|
||||
.done(res => {
|
||||
if (!res.success) {
|
||||
Swal.fire({ icon: 'error', title: 'Errore', text: res.message });
|
||||
return;
|
||||
}
|
||||
|
||||
const input = document.getElementById('ivFilesInput');
|
||||
if (!input.files.length) { location.reload(); return; }
|
||||
|
||||
const form = new FormData();
|
||||
form.append('scope', 'intervention');
|
||||
form.append('owner_id', res.id);
|
||||
Array.from(input.files).forEach(f => form.append('files[]', f));
|
||||
|
||||
$.ajax({
|
||||
url: 'manutenzioni/ajax/upload_file.php',
|
||||
method: 'POST', data: form, processData: false, contentType: false, dataType: 'json'
|
||||
}).always(() => location.reload());
|
||||
})
|
||||
.fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Create/edit modal for a maintenance.
|
||||
* Expects $formData in scope. Defines mntOpenMaintenanceModal(data, equipmentId).
|
||||
*/
|
||||
?>
|
||||
<div class="modal fade" id="maintenanceModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered modal-fullscreen-sm-down">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="maintenanceModalTitle">Nuova manutenzione</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<form id="maintenanceForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="id" id="mtId" value="">
|
||||
<input type="hidden" name="equipment_id" id="mtEquipmentId" value="">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-4 col-md-2">
|
||||
<label class="form-label fw-semibold">Codice</label>
|
||||
<input type="text" class="form-control" name="code" id="mtCode" maxlength="10" placeholder="A">
|
||||
</div>
|
||||
|
||||
<div class="col-8 col-md-10">
|
||||
<label class="form-label fw-semibold">Titolo <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="title" id="mtTitle" maxlength="500" required>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Descrizione dell'intervento</label>
|
||||
<textarea class="form-control" name="description" id="mtDescription" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Tipo</label>
|
||||
<select class="form-select" name="intervention_type" id="mtType">
|
||||
<?php foreach (mnt_intervention_types() as $value => $label): ?>
|
||||
<option value="<?= mnt_h($value) ?>"><?= mnt_h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Esecuzione</label>
|
||||
<select class="form-select" name="execution_type" id="mtExecution">
|
||||
<?php foreach (mnt_execution_types() as $value => $label): ?>
|
||||
<option value="<?= mnt_h($value) ?>"><?= mnt_h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12" id="mtSupplierWrap" style="display:none">
|
||||
<label class="form-label fw-semibold">Fornitore esterno <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="supplier_id" id="mtSupplier">
|
||||
<option value="">— seleziona —</option>
|
||||
<?php foreach ($formData['suppliers'] as $supplier): ?>
|
||||
<option value="<?= (int)$supplier['id'] ?>"><?= mnt_h($supplier['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="mnt-section-title mb-0 mt-2">Frequenza</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<label class="form-label fw-semibold">Ogni</label>
|
||||
<input type="number" class="form-control" name="frequency_value" id="mtFrequencyValue" min="1" max="999" value="1">
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-5">
|
||||
<label class="form-label fw-semibold">Unità</label>
|
||||
<select class="form-select" name="frequency_unit" id="mtFrequencyUnit">
|
||||
<?php foreach (mnt_frequency_units() as $value => $label): ?>
|
||||
<option value="<?= mnt_h($value) ?>" <?= $value === 'month' ? 'selected' : '' ?>><?= mnt_h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label fw-semibold">Preavviso (giorni)</label>
|
||||
<input type="number" class="form-control" name="alert_days" id="mtAlertDays" min="0" max="365" value="7">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Nota sulla frequenza</label>
|
||||
<input type="text" class="form-control" name="frequency_note" id="mtFrequencyNote" maxlength="500"
|
||||
placeholder="Es. prima dell'accensione, d'estate settimanale, dopo lo spegnimento">
|
||||
<div class="form-text">Per i casi che non rientrano in un intervallo fisso.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Incaricato</label>
|
||||
<select class="form-select" name="assignee_employee_id" id="mtAssignee">
|
||||
<option value="">—</option>
|
||||
<?php foreach ($formData['employees'] as $employee): ?>
|
||||
<option value="<?= (int)$employee['id'] ?>"><?= mnt_h($employee['full_name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Responsabile</label>
|
||||
<select class="form-select" name="supervisor_employee_id" id="mtSupervisor">
|
||||
<option value="">—</option>
|
||||
<?php foreach ($formData['employees'] as $employee): ?>
|
||||
<option value="<?= (int)$employee['id'] ?>"><?= mnt_h($employee['full_name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Note</label>
|
||||
<textarea class="form-control" name="notes" id="mtNotes" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-flex flex-wrap gap-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="is_critical" id="mtCritical" value="1">
|
||||
<label class="form-check-label fw-semibold" for="mtCritical">Manutenzione critica</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<!-- Una checkbox non spuntata non viene inviata: senza questo campo
|
||||
nascosto (che deve precederla) l'endpoint riceverebbe sempre
|
||||
"attiva" e la manutenzione non si potrebbe mai disattivare. -->
|
||||
<input type="hidden" name="is_active" value="0">
|
||||
<input class="form-check-input" type="checkbox" name="is_active" id="mtActive" value="1" checked>
|
||||
<label class="form-check-label fw-semibold" for="mtActive">Attiva</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12" id="mtFilesWrap">
|
||||
<div class="mnt-section-title mt-2">Istruzioni allegate</div>
|
||||
<div id="mtFilesList" class="mb-2"></div>
|
||||
<input type="file" class="form-control" id="mtFilesInput" multiple>
|
||||
<div class="form-text">I file vengono caricati dopo il salvataggio.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-mnt-primary">Salva</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function mntSyncMaintenanceForm() {
|
||||
const isExternal = $('#mtExecution').val() === 'external';
|
||||
$('#mtSupplierWrap').toggle(isExternal);
|
||||
|
||||
const onDemand = $('#mtFrequencyUnit').val() === 'on_demand';
|
||||
$('#mtFrequencyValue').prop('disabled', onDemand).closest('.col-6').toggle(!onDemand);
|
||||
$('#mtAlertDays').prop('disabled', onDemand);
|
||||
}
|
||||
|
||||
function mntRenderMaintenanceFiles(files) {
|
||||
const $list = $('#mtFilesList').empty();
|
||||
if (!files || !files.length) { return; }
|
||||
|
||||
files.forEach(f => {
|
||||
$list.append(
|
||||
$('<div class="mnt-file-row"></div>').append(
|
||||
$('<i class="fa-solid fa-paperclip text-muted"></i>'),
|
||||
$('<a class="file-name" target="_blank"></a>')
|
||||
.attr('href', 'manutenzioni/ajax/download_file.php?scope=maintenance&id=' + f.id)
|
||||
.text(f.original_name),
|
||||
$('<button type="button" class="btn-action btn-action-delete btn-del-file"><i class="fa-solid fa-trash"></i></button>')
|
||||
.attr('data-id', f.id)
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function mntOpenMaintenanceModal(data, equipmentId) {
|
||||
const isEdit = !!data;
|
||||
$('#maintenanceModalTitle').text(isEdit ? 'Modifica manutenzione' : 'Nuova manutenzione');
|
||||
|
||||
$('#mtId').val(isEdit ? data.id : '');
|
||||
$('#mtEquipmentId').val(isEdit ? data.equipment_id : (equipmentId || ''));
|
||||
$('#mtCode').val(isEdit ? (data.code || '') : '');
|
||||
$('#mtTitle').val(isEdit ? (data.title || '') : '');
|
||||
$('#mtDescription').val(isEdit ? (data.description || '') : '');
|
||||
$('#mtType').val(isEdit ? data.intervention_type : 'scheduled');
|
||||
$('#mtExecution').val(isEdit ? data.execution_type : 'internal');
|
||||
$('#mtSupplier').val(isEdit && data.supplier_id ? String(data.supplier_id) : '');
|
||||
$('#mtFrequencyValue').val(isEdit && data.frequency_value ? data.frequency_value : 1);
|
||||
$('#mtFrequencyUnit').val(isEdit ? data.frequency_unit : 'month');
|
||||
$('#mtFrequencyNote').val(isEdit ? (data.frequency_note || '') : '');
|
||||
$('#mtAlertDays').val(isEdit ? data.alert_days : 7);
|
||||
$('#mtAssignee').val(isEdit && data.assignee_employee_id ? String(data.assignee_employee_id) : '');
|
||||
$('#mtSupervisor').val(isEdit && data.supervisor_employee_id ? String(data.supervisor_employee_id) : '');
|
||||
$('#mtNotes').val(isEdit ? (data.notes || '') : '');
|
||||
$('#mtCritical').prop('checked', isEdit ? Number(data.is_critical) === 1 : false);
|
||||
$('#mtActive').prop('checked', isEdit ? Number(data.is_active) === 1 : true);
|
||||
|
||||
mntRenderMaintenanceFiles(isEdit ? (data.files || []) : []);
|
||||
$('#mtFilesInput').val('');
|
||||
|
||||
mntSyncMaintenanceForm();
|
||||
new bootstrap.Modal('#maintenanceModal').show();
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$('#mtExecution, #mtFrequencyUnit').on('change', mntSyncMaintenanceForm);
|
||||
|
||||
$('#mtFilesList').on('click', '.btn-del-file', function () {
|
||||
const $row = $(this).closest('.mnt-file-row');
|
||||
$.post('manutenzioni/ajax/delete_file.php', { scope: 'maintenance', id: $(this).data('id') })
|
||||
.done(res => {
|
||||
if (res.success) { $row.remove(); }
|
||||
else { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); }
|
||||
});
|
||||
});
|
||||
|
||||
$('#maintenanceForm').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!$('#mtTitle').val().trim()) {
|
||||
Swal.fire({ icon: 'warning', title: 'Il titolo è obbligatorio' });
|
||||
return;
|
||||
}
|
||||
if ($('#mtExecution').val() === 'external' && !$('#mtSupplier').val()) {
|
||||
Swal.fire({ icon: 'warning', title: 'Seleziona il fornitore esterno' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Disabled inputs are not serialised — send the unit explicitly
|
||||
const payload = $(this).serializeArray();
|
||||
payload.push({ name: 'frequency_unit', value: $('#mtFrequencyUnit').val() });
|
||||
|
||||
$.post('manutenzioni/ajax/save_maintenance.php', $.param(payload))
|
||||
.done(res => {
|
||||
if (!res.success) {
|
||||
Swal.fire({ icon: 'error', title: 'Errore', text: res.message });
|
||||
return;
|
||||
}
|
||||
|
||||
const input = document.getElementById('mtFilesInput');
|
||||
if (!input.files.length) { location.reload(); return; }
|
||||
|
||||
const form = new FormData();
|
||||
form.append('scope', 'maintenance');
|
||||
form.append('owner_id', res.id);
|
||||
Array.from(input.files).forEach(f => form.append('files[]', f));
|
||||
|
||||
$.ajax({
|
||||
url: 'manutenzioni/ajax/upload_file.php',
|
||||
method: 'POST', data: form, processData: false, contentType: false, dataType: 'json'
|
||||
}).always(() => location.reload());
|
||||
})
|
||||
.fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Shared <head> for the Manutenzioni pages.
|
||||
*
|
||||
* Usage (before including):
|
||||
* $MNT_TITLE = 'Registro attrezzature';
|
||||
* $MNT_PLUGINS = ['fullcalendar']; // opzionale
|
||||
* include __DIR__ . '/include/page_head.php';
|
||||
*
|
||||
* Works from any depth under /userarea/ — the base href is derived from
|
||||
* SCRIPT_NAME, so all asset paths stay relative to /userarea/.
|
||||
*/
|
||||
|
||||
$MNT_TITLE = $MNT_TITLE ?? 'Manutenzioni';
|
||||
$MNT_PLUGINS = $MNT_PLUGINS ?? [];
|
||||
|
||||
$mntScriptName = $_SERVER['SCRIPT_NAME'] ?? '';
|
||||
$mntMarker = '/userarea/';
|
||||
$mntPos = strpos($mntScriptName, $mntMarker);
|
||||
$MNT_BASE = $mntPos !== false
|
||||
? substr($mntScriptName, 0, $mntPos + strlen($mntMarker))
|
||||
: '/userarea/';
|
||||
?>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<base href="<?= mnt_h($MNT_BASE) ?>">
|
||||
<?php include(__DIR__ . '/../../cssinclude.php'); ?>
|
||||
<script src="assets/js/jquery.min.js"></script>
|
||||
<title><?= mnt_h($MNT_TITLE) ?> — Manutenzioni</title>
|
||||
<?php if (in_array('fullcalendar', $MNT_PLUGINS, true)): ?>
|
||||
<script src="manutenzioni/assets/fullcalendar.min.js"></script>
|
||||
<script src="manutenzioni/assets/fullcalendar-it.js"></script>
|
||||
<?php endif; ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (typeof window.Swal === 'undefined') {
|
||||
var local = document.createElement('script');
|
||||
local.src = 'manutenzioni/assets/sweetalert2.min.js';
|
||||
document.head.appendChild(local);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
:root {
|
||||
--mnt-primary: #2f7d8f;
|
||||
--mnt-primary-hover: #256575;
|
||||
--mnt-heading: #1e3a44;
|
||||
--mnt-card-bg: linear-gradient(135deg, #eef7f9 0%, #e4f0f3 100%);
|
||||
--mnt-card-border: #d6e5e9;
|
||||
}
|
||||
|
||||
.mnt-card { border: none; border-radius: 0.75rem; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); overflow: hidden; }
|
||||
.mnt-card .card-header { background: var(--mnt-card-bg); border-bottom: 1px solid var(--mnt-card-border); padding: 1rem 1.25rem; }
|
||||
.mnt-card .card-header h5 { font-weight: 700; color: var(--mnt-heading); margin: 0; font-size: 1.05rem; letter-spacing: -0.01em; }
|
||||
.mnt-card .card-body { padding: 1.25rem; }
|
||||
|
||||
.btn-mnt-primary { background: var(--mnt-primary); border: none; color: #fff; font-weight: 600; font-size: 0.85rem; padding: 0.5rem 1rem; border-radius: 0.5rem; transition: all 0.2s; }
|
||||
.btn-mnt-primary:hover { background: var(--mnt-primary-hover); color: #fff; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(47, 125, 143, 0.3); }
|
||||
.btn-mnt-outline { background: transparent; border: 1.5px solid var(--mnt-primary); color: var(--mnt-primary); font-weight: 600; font-size: 0.85rem; padding: 0.45rem 1rem; border-radius: 0.5rem; transition: all 0.2s; }
|
||||
.btn-mnt-outline:hover { background: var(--mnt-primary); color: #fff; transform: translateY(-1px); }
|
||||
|
||||
.btn-action { width: 32px; height: 32px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border: none; border-radius: 0.4rem; font-size: 0.85rem; transition: all 0.15s; text-decoration: none; }
|
||||
.btn-action-view { background: rgba(47, 125, 143, 0.12); color: var(--mnt-primary); }
|
||||
.btn-action-view:hover { background: var(--mnt-primary); color: #fff; }
|
||||
.btn-action-edit { background: rgba(13, 110, 253, 0.12); color: #0d6efd; }
|
||||
.btn-action-edit:hover { background: #0d6efd; color: #fff; }
|
||||
.btn-action-delete { background: rgba(220, 53, 69, 0.12); color: #dc3545; }
|
||||
.btn-action-delete:hover { background: #dc3545; color: #fff; }
|
||||
.btn-action-done { background: rgba(25, 135, 84, 0.12); color: #198754; }
|
||||
.btn-action-done:hover { background: #198754; color: #fff; }
|
||||
|
||||
/* Schedule state badges */
|
||||
.mnt-badge { display: inline-block; padding: 0.22rem 0.6rem; border-radius: 999px; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.01em; white-space: nowrap; }
|
||||
.mnt-badge-overdue { background: rgba(220, 53, 69, 0.14); color: #b02a37; }
|
||||
.mnt-badge-soon { background: rgba(255, 193, 7, 0.2); color: #9a7000; }
|
||||
.mnt-badge-ok { background: rgba(25, 135, 84, 0.14); color: #146c43; }
|
||||
.mnt-badge-none { background: rgba(108, 117, 125, 0.14); color: #565e64; }
|
||||
.mnt-badge-critical { background: rgba(220, 53, 69, 0.9); color: #fff; }
|
||||
.mnt-badge-soft { background: rgba(47, 125, 143, 0.12); color: var(--mnt-primary); }
|
||||
.mnt-badge-status-active { background: rgba(25, 135, 84, 0.14); color: #146c43; }
|
||||
.mnt-badge-status-out_of_service { background: rgba(255, 193, 7, 0.2); color: #9a7000; }
|
||||
.mnt-badge-status-decommissioned { background: rgba(108, 117, 125, 0.16); color: #495057; }
|
||||
|
||||
.mnt-cat-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
|
||||
|
||||
.modal-content > form { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
|
||||
.modal-content > form > .modal-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
||||
|
||||
.mnt-title-cell { max-width: 320px; }
|
||||
.mnt-equipment-cell { max-width: 240px; }
|
||||
.mnt-freq-cell { max-width: 190px; }
|
||||
.mnt-person-cell { max-width: 150px; }
|
||||
.mnt-name-cell { max-width: 300px; }
|
||||
.mnt-name-cell .mnt-title-clamp {
|
||||
display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.mnt-title-clamp { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; word-break: break-word; min-width: 0; }
|
||||
.mnt-equipment-cell .mnt-title-clamp { flex: 1 1 auto; }
|
||||
|
||||
/* Mobile-first cards; the table appears from xl (1200px) upwards */
|
||||
.mnt-item-card { background: #fff; border: 1px solid var(--mnt-card-border); border-left: 5px solid var(--row-color, #e9ecef); border-radius: 0.6rem; padding: 0.85rem 0.95rem; margin-bottom: 0.6rem; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); }
|
||||
.mnt-item-card .ic-title { font-weight: 700; color: var(--mnt-heading); font-size: 0.95rem; word-break: break-word; }
|
||||
.mnt-item-card .ic-meta { font-size: 0.8rem; color: #6c757d; margin-top: 0.3rem; }
|
||||
.mnt-item-card .ic-meta strong { color: var(--mnt-heading); font-weight: 600; }
|
||||
.mnt-item-card .ic-actions { display: flex; gap: 0.4rem; justify-content: flex-end; margin-top: 0.6rem; }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.d-xl-none > .mnt-item-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: 1rem; }
|
||||
.d-xl-none > .mnt-item-card > :not(.ic-actions) { grid-column: 1; }
|
||||
.d-xl-none > .mnt-item-card > .ic-actions { grid-column: 2; grid-row: 1 / -1; margin-top: 0; }
|
||||
}
|
||||
|
||||
.mnt-pager { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem 1.25rem; padding: 0.85rem 0.25rem 0.25rem; font-size: 0.85rem; }
|
||||
.mnt-pager-info { color: #6c757d; }
|
||||
.mnt-pager-info strong { color: var(--mnt-heading); }
|
||||
.mnt-pager-per { display: flex; align-items: center; gap: 0.35rem; }
|
||||
.mnt-pager-pages { display: flex; align-items: center; gap: 0.25rem; margin-left: auto; }
|
||||
.mnt-pager-link { display: inline-flex; align-items: center; justify-content: center; min-width: 2rem; height: 2rem; padding: 0 0.5rem; border: 1px solid var(--mnt-card-border); border-radius: 0.45rem; background: #fff; color: var(--mnt-heading); text-decoration: none; }
|
||||
.mnt-pager-link:hover { border-color: #2f7d8f; color: #2f7d8f; }
|
||||
.mnt-pager-link.is-current { background: #2f7d8f; border-color: #2f7d8f; color: #fff; font-weight: 700; }
|
||||
.mnt-pager-link.is-disabled { opacity: 0.4; pointer-events: none; }
|
||||
.mnt-pager-gap { padding: 0 0.15rem; color: #adb5bd; }
|
||||
@media (max-width: 575.98px) {
|
||||
.mnt-pager-pages { margin-left: 0; width: 100%; justify-content: center; }
|
||||
}
|
||||
|
||||
.mnt-thumb { width: 46px; height: 46px; border-radius: 0.45rem; object-fit: cover; border: 1px solid var(--mnt-card-border); background: #f6f9fa; }
|
||||
.mnt-thumb-placeholder { display: inline-flex; align-items: center; justify-content: center; color: #adb5bd; font-size: 1.1rem; }
|
||||
|
||||
.empty-state { text-align: center; padding: 3rem 1rem; color: #6c757d; }
|
||||
.empty-state i { font-size: 3rem; opacity: 0.3; margin-bottom: 1rem; display: block; }
|
||||
|
||||
.mnt-filter-bar { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-bottom: 1rem; }
|
||||
.mnt-filter-bar .form-select, .mnt-filter-bar .form-control { min-width: 150px; flex: 1 1 150px; max-width: 100%; }
|
||||
|
||||
.mnt-section-title { font-size: 0.78rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #8a9aa0; margin-bottom: 0.5rem; }
|
||||
.mnt-kv { display: flex; gap: 0.5rem; padding: 0.35rem 0; border-bottom: 1px dashed #eceff1; font-size: 0.88rem; }
|
||||
.mnt-kv:last-child { border-bottom: none; }
|
||||
.mnt-kv dt { flex: 0 0 42%; color: #6c757d; font-weight: 500; margin: 0; }
|
||||
.mnt-kv dd { flex: 1; margin: 0; color: var(--mnt-heading); font-weight: 600; word-break: break-word; }
|
||||
|
||||
.mnt-photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 0.6rem; }
|
||||
.mnt-photo-item { position: relative; border-radius: 0.5rem; overflow: hidden; border: 1px solid var(--mnt-card-border); aspect-ratio: 1; }
|
||||
.mnt-photo-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.mnt-photo-item .photo-tools { position: absolute; inset: auto 0 0 0; display: flex; gap: 0.25rem; justify-content: center; padding: 0.25rem; background: rgba(0, 0, 0, 0.45); opacity: 0; transition: opacity 0.15s; }
|
||||
.mnt-photo-item:hover .photo-tools { opacity: 1; }
|
||||
.mnt-photo-item .photo-tools button { border: none; background: transparent; color: #fff; font-size: 0.8rem; padding: 0.1rem 0.35rem; }
|
||||
.mnt-photo-item.is-cover { border-color: var(--mnt-primary); box-shadow: 0 0 0 2px rgba(47, 125, 143, 0.25); }
|
||||
|
||||
.mnt-file-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.5rem 0; border-bottom: 1px solid #f1f4f5; font-size: 0.88rem; }
|
||||
.mnt-file-row:last-child { border-bottom: none; }
|
||||
.mnt-file-row .file-name { flex: 1; word-break: break-all; }
|
||||
|
||||
/* Signature pad (tablet) */
|
||||
.mnt-signature-wrap { border: 1px dashed var(--mnt-card-border); border-radius: 0.5rem; background: #fcfdfd; position: relative; }
|
||||
.mnt-signature-wrap canvas { width: 100%; height: 160px; display: block; touch-action: none; border-radius: 0.5rem; }
|
||||
|
||||
.mnt-timeline { position: relative; padding-left: 1.25rem; }
|
||||
.mnt-timeline::before { content: ''; position: absolute; left: 6px; top: 4px; bottom: 4px; width: 2px; background: #e6eef0; }
|
||||
.mnt-timeline-item { position: relative; padding-bottom: 0.9rem; }
|
||||
.mnt-timeline-item::before { content: ''; position: absolute; left: -1.25rem; top: 0.35rem; width: 10px; height: 10px; border-radius: 50%; background: var(--mnt-primary); box-shadow: 0 0 0 3px #fff; }
|
||||
.mnt-timeline-item .tl-date { font-size: 0.75rem; color: #8a9aa0; font-weight: 600; }
|
||||
.mnt-timeline-item .tl-body { font-size: 0.88rem; color: var(--mnt-heading); }
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.mnt-card .card-header { flex-direction: column; gap: 0.75rem; align-items: flex-start !important; }
|
||||
.header-actions { width: 100%; }
|
||||
.header-actions .btn { width: 100%; justify-content: center; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Pager shared by the registry and the maintenance list.
|
||||
*
|
||||
* Both lists used to print every row at once — 101 pieces of equipment and 126
|
||||
* maintenance tasks, each rendered twice (cards + table), which on a tablet
|
||||
* came out as a page some 20 000px tall.
|
||||
*
|
||||
* Usage (before including):
|
||||
* $mntPagerUrl = 'manutenzioni/index.php'; // path, relative to <base href>
|
||||
* $mntPagerPage = 3; // current page, 1-based
|
||||
* $mntPagerPages = 7; // number of pages
|
||||
* $mntPagerTotal = 163; // rows matching the current filters
|
||||
* $mntPagerPer = 25; // rows per page
|
||||
*/
|
||||
|
||||
$mntPagerPages = max(1, (int)($mntPagerPages ?? 1));
|
||||
$mntPagerPage = min(max(1, (int)($mntPagerPage ?? 1)), $mntPagerPages);
|
||||
$mntPagerTotal = (int)($mntPagerTotal ?? 0);
|
||||
$mntPagerPer = max(1, (int)($mntPagerPer ?? 25));
|
||||
|
||||
/** Keeps the active filters, drops page when it would be redundant. */
|
||||
$mntPagerLink = static function (int $page, ?int $per = null) use ($mntPagerUrl) {
|
||||
$query = $_GET;
|
||||
unset($query['page'], $query['per_page']);
|
||||
if ($per !== null) {
|
||||
$query['per_page'] = $per;
|
||||
} elseif (isset($_GET['per_page'])) {
|
||||
$query['per_page'] = $_GET['per_page'];
|
||||
}
|
||||
if ($page > 1) {
|
||||
$query['page'] = $page;
|
||||
}
|
||||
|
||||
return $mntPagerUrl . ($query ? '?' . http_build_query($query) : '');
|
||||
};
|
||||
|
||||
$mntFirstRow = $mntPagerTotal ? (($mntPagerPage - 1) * $mntPagerPer) + 1 : 0;
|
||||
$mntLastRow = min($mntPagerPage * $mntPagerPer, $mntPagerTotal);
|
||||
|
||||
// A window around the current page: with 40 pages, 40 links are noise.
|
||||
$mntWindowFrom = max(1, $mntPagerPage - 2);
|
||||
$mntWindowTo = min($mntPagerPages, $mntPagerPage + 2);
|
||||
?>
|
||||
<div class="mnt-pager" data-page="<?= $mntPagerPage ?>" data-pages="<?= $mntPagerPages ?>" data-total="<?= $mntPagerTotal ?>">
|
||||
<div class="mnt-pager-info">
|
||||
<?= $mntFirstRow ?>–<?= $mntLastRow ?> di <strong><?= $mntPagerTotal ?></strong>
|
||||
</div>
|
||||
|
||||
<div class="mnt-pager-per">
|
||||
<span class="text-muted">Per pagina:</span>
|
||||
<?php foreach ([25, 50, 100] as $option): ?>
|
||||
<?php if ($option === $mntPagerPer): ?>
|
||||
<span class="mnt-pager-link is-current"><?= $option ?></span>
|
||||
<?php else: ?>
|
||||
<a class="mnt-pager-link" href="<?= mnt_h($mntPagerLink(1, $option)) ?>"><?= $option ?></a>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($mntPagerPages > 1): ?>
|
||||
<nav class="mnt-pager-pages" aria-label="Paginazione">
|
||||
<?php if ($mntPagerPage > 1): ?>
|
||||
<a class="mnt-pager-link btn-page-prev" rel="prev" href="<?= mnt_h($mntPagerLink($mntPagerPage - 1)) ?>" aria-label="Precedente">‹</a>
|
||||
<?php else: ?>
|
||||
<span class="mnt-pager-link is-disabled">‹</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($mntWindowFrom > 1): ?>
|
||||
<a class="mnt-pager-link" href="<?= mnt_h($mntPagerLink(1)) ?>">1</a>
|
||||
<?php if ($mntWindowFrom > 2): ?><span class="mnt-pager-gap">…</span><?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php for ($p = $mntWindowFrom; $p <= $mntWindowTo; $p++): ?>
|
||||
<?php if ($p === $mntPagerPage): ?>
|
||||
<span class="mnt-pager-link is-current" aria-current="page"><?= $p ?></span>
|
||||
<?php else: ?>
|
||||
<a class="mnt-pager-link" href="<?= mnt_h($mntPagerLink($p)) ?>"><?= $p ?></a>
|
||||
<?php endif; ?>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php if ($mntWindowTo < $mntPagerPages): ?>
|
||||
<?php if ($mntWindowTo < $mntPagerPages - 1): ?><span class="mnt-pager-gap">…</span><?php endif; ?>
|
||||
<a class="mnt-pager-link" href="<?= mnt_h($mntPagerLink($mntPagerPages)) ?>"><?= $mntPagerPages ?></a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($mntPagerPage < $mntPagerPages): ?>
|
||||
<a class="mnt-pager-link btn-page-next" rel="next" href="<?= mnt_h($mntPagerLink($mntPagerPage + 1)) ?>" aria-label="Successiva">›</a>
|
||||
<?php else: ?>
|
||||
<span class="mnt-pager-link is-disabled">›</span>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<script>
|
||||
if (window.innerWidth > 1024) {
|
||||
document.getElementById('appWrapper').classList.add('toggled');
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,432 @@
|
||||
<?php include(__DIR__ . '/../include/headscript.php'); ?>
|
||||
<?php
|
||||
require_once __DIR__ . '/include/functions.php';
|
||||
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
if (!userCan('maintenance.equipment.view')) {
|
||||
http_response_code(403);
|
||||
exit('Permesso negato.');
|
||||
}
|
||||
|
||||
$canManage = userCan('maintenance.manage');
|
||||
|
||||
// ---------------------------------------------------------------- filters
|
||||
$filterSearch = trim((string)($_GET['q'] ?? ''));
|
||||
$filterCategory = isset($_GET['category']) && is_numeric($_GET['category']) ? (int)$_GET['category'] : null;
|
||||
$filterLine = isset($_GET['line']) && is_numeric($_GET['line']) ? (int)$_GET['line'] : null;
|
||||
$filterDepartment = isset($_GET['department']) && is_numeric($_GET['department']) ? (int)$_GET['department'] : null;
|
||||
$filterStatus = (string)($_GET['status'] ?? 'active');
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($filterSearch !== '') {
|
||||
$where[] = "(e.name LIKE ? OR e.serial_number LIKE ? OR e.registration_number LIKE ? OR e.manufacturer LIKE ?)";
|
||||
$like = '%' . $filterSearch . '%';
|
||||
array_push($params, $like, $like, $like, $like);
|
||||
}
|
||||
if ($filterCategory) {
|
||||
$where[] = "e.category_id = ?";
|
||||
$params[] = $filterCategory;
|
||||
}
|
||||
if ($filterLine) {
|
||||
$where[] = "e.line_id = ?";
|
||||
$params[] = $filterLine;
|
||||
}
|
||||
if ($filterDepartment) {
|
||||
$where[] = "e.department_id = ?";
|
||||
$params[] = $filterDepartment;
|
||||
}
|
||||
if ($filterStatus !== '' && $filterStatus !== 'all') {
|
||||
$where[] = "e.status = ?";
|
||||
$params[] = $filterStatus;
|
||||
}
|
||||
|
||||
$sql = "
|
||||
SELECT e.*,
|
||||
c.name AS category_name,
|
||||
c.color AS category_color,
|
||||
pl.name AS line_name,
|
||||
pl.line_number,
|
||||
d.name AS department_name,
|
||||
cf.stored_name AS cover_stored,
|
||||
(SELECT COUNT(*) FROM maint_maintenances m
|
||||
WHERE m.equipment_id = e.id AND m.is_active = 1) AS maintenance_count,
|
||||
(SELECT MIN(m.next_due_date) FROM maint_maintenances m
|
||||
WHERE m.equipment_id = e.id AND m.is_active = 1 AND m.next_due_date IS NOT NULL) AS next_due_date
|
||||
FROM inv_equipment e
|
||||
LEFT JOIN inv_categories c ON c.id = e.category_id
|
||||
LEFT JOIN production_lines pl ON pl.id = e.line_id
|
||||
LEFT JOIN departments d ON d.id = e.department_id
|
||||
LEFT JOIN inv_equipment_files cf ON cf.id = e.cover_file_id
|
||||
";
|
||||
|
||||
$whereSql = $where ? ' WHERE ' . implode(' AND ', $where) : '';
|
||||
$sql .= $whereSql;
|
||||
$sql .= ' ORDER BY c.sort_order ASC, e.name ASC';
|
||||
|
||||
// The filters only touch columns of inv_equipment, so counting needs no joins.
|
||||
$countStmt = $pdo->prepare('SELECT COUNT(*) FROM inv_equipment e' . $whereSql);
|
||||
$countStmt->execute($params);
|
||||
$totalEquipment = (int)$countStmt->fetchColumn();
|
||||
|
||||
[$page, $perPage] = mnt_page_params();
|
||||
$totalPages = max(1, (int)ceil($totalEquipment / $perPage));
|
||||
$page = min($page, $totalPages);
|
||||
|
||||
// Cast to int above, so interpolating here cannot carry anything but digits;
|
||||
// LIMIT/OFFSET placeholders would need PDO::ATTR_EMULATE_PREPARES off.
|
||||
$sql .= sprintf(' LIMIT %d OFFSET %d', $perPage, ($page - 1) * $perPage);
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$equipment = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$formData = mnt_form_data($pdo);
|
||||
$statuses = mnt_equipment_statuses();
|
||||
|
||||
$MNT_TITLE = 'Registro attrezzature';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<?php include __DIR__ . '/include/page_head.php'; ?>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<?php include __DIR__ . '/include/wrapper_open.php'; ?>
|
||||
<?php include(__DIR__ . '/../include/navbar.php'); ?>
|
||||
<?php include(__DIR__ . '/../include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<div class="card mnt-card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h5><i class="fa-solid fa-boxes-stacked me-2"></i>Registro attrezzature</h5>
|
||||
<div class="header-actions d-flex gap-2 flex-wrap">
|
||||
<a href="manutenzioni/maintenances.php" class="btn btn-mnt-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-screwdriver-wrench"></i><span>Manutenzioni</span>
|
||||
</a>
|
||||
<?php if ($canManage): ?>
|
||||
<button class="btn btn-mnt-primary d-inline-flex align-items-center gap-2" id="btnAddEquipment">
|
||||
<i class="fa-solid fa-plus"></i><span>Nuova attrezzatura</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form class="mnt-filter-bar" method="get" id="filterForm">
|
||||
<input type="text" class="form-control" name="q" placeholder="Cerca nome, matricola, seriale…"
|
||||
value="<?= mnt_h($filterSearch) ?>">
|
||||
|
||||
<select class="form-select" name="category">
|
||||
<option value="">Tutte le categorie</option>
|
||||
<?php foreach ($formData['categories'] as $category): ?>
|
||||
<option value="<?= (int)$category['id'] ?>" <?= $filterCategory === (int)$category['id'] ? 'selected' : '' ?>>
|
||||
<?= mnt_h($category['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<select class="form-select" name="line">
|
||||
<option value="">Tutte le linee</option>
|
||||
<?php foreach ($formData['lines'] as $line): ?>
|
||||
<option value="<?= (int)$line['id'] ?>" <?= $filterLine === (int)$line['id'] ? 'selected' : '' ?>>
|
||||
Linea <?= (int)$line['line_number'] ?> — <?= mnt_h($line['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<select class="form-select" name="department">
|
||||
<option value="">Tutti i reparti</option>
|
||||
<?php foreach ($formData['departments'] as $department): ?>
|
||||
<option value="<?= (int)$department['id'] ?>" <?= $filterDepartment === (int)$department['id'] ? 'selected' : '' ?>>
|
||||
<?= mnt_h($department['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<select class="form-select" name="status">
|
||||
<option value="all" <?= $filterStatus === 'all' ? 'selected' : '' ?>>Tutti gli stati</option>
|
||||
<?php foreach ($statuses as $value => $label): ?>
|
||||
<option value="<?= mnt_h($value) ?>" <?= $filterStatus === $value ? 'selected' : '' ?>>
|
||||
<?= mnt_h($label) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<?php if ($perPage !== 25): ?>
|
||||
<input type="hidden" name="per_page" value="<?= (int)$perPage ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Every control applies itself, so there is no «Filtra» button to
|
||||
press. This one stays only so that Enter in the search box
|
||||
submits, and so the bar still works without JavaScript. -->
|
||||
<button type="submit" class="visually-hidden">Filtra</button>
|
||||
|
||||
<a href="manutenzioni/index.php" class="btn btn-mnt-outline btn-reset-filters d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-xmark"></i><span>Azzera filtri</span>
|
||||
</a>
|
||||
</form>
|
||||
|
||||
<?php if (!$equipment): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fa-solid fa-boxes-stacked"></i>
|
||||
<p>Nessuna attrezzatura trovata con questi filtri.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div id="equipmentList">
|
||||
<!-- CARD -->
|
||||
<div class="d-xl-none">
|
||||
<?php foreach ($equipment as $item): ?>
|
||||
<?php
|
||||
$state = mnt_due_state($item['next_due_date']);
|
||||
// Qui la riga aggrega piu' manutenzioni: «nessuna data» non
|
||||
// significa «al bisogno», quindi l'etichetta resta neutra.
|
||||
$badge = $state === 'none'
|
||||
? ['label' => 'Senza scadenza', 'class' => 'mnt-badge-none']
|
||||
: mnt_due_badge($state);
|
||||
?>
|
||||
<div class="mnt-item-card" data-id="<?= (int)$item['id'] ?>"
|
||||
style="--row-color: <?= mnt_h($item['category_color'] ?? '#e9ecef') ?>">
|
||||
<div class="d-flex gap-2 align-items-start">
|
||||
<?php if (!empty($item['cover_stored'])): ?>
|
||||
<img class="mnt-thumb" src="manutenzioni/ajax/download_file.php?scope=equipment&id=<?= (int)$item['cover_file_id'] ?>" alt="">
|
||||
<?php else: ?>
|
||||
<span class="mnt-thumb mnt-thumb-placeholder"><i class="fa-solid fa-gear"></i></span>
|
||||
<?php endif; ?>
|
||||
<div class="flex-grow-1">
|
||||
<div class="ic-title">
|
||||
<?= mnt_h($item['name']) ?>
|
||||
<?php if ($item['status'] !== 'active'): ?>
|
||||
<span class="mnt-badge mnt-badge-status-<?= mnt_h($item['status']) ?>">
|
||||
<?= mnt_h($statuses[$item['status']] ?? $item['status']) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="ic-meta">
|
||||
<?= mnt_h($item['category_name'] ?? 'Senza categoria') ?>
|
||||
<?php if ($item['line_name']): ?>
|
||||
· Linea <?= (int)$item['line_number'] ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="ic-meta">
|
||||
Manutenzioni: <strong><?= (int)$item["maintenance_count"] ?></strong> attive
|
||||
<?php if ($item['maintenance_count'] > 0): ?>
|
||||
· <span class="mnt-badge <?= $badge['class'] ?>"><?= $badge['label'] ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ic-actions">
|
||||
<a class="btn-action btn-action-view" title="Apri scheda"
|
||||
href="manutenzioni/equipment.php?id=<?= (int)$item['id'] ?>">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</a>
|
||||
<?php if ($canManage): ?>
|
||||
<button class="btn-action btn-action-edit btn-edit" title="Modifica"><i class="fa-solid fa-pen"></i></button>
|
||||
<button class="btn-action btn-action-delete btn-delete"
|
||||
title="Elimina"
|
||||
data-name="<?= mnt_h($item['name']) ?>"
|
||||
data-maintenances="<?= (int)$item['maintenance_count'] ?>">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- DESKTOP -->
|
||||
<!-- Table only from 1200px up: below that the 8 columns force
|
||||
horizontal scrolling, which is unusable on a tablet. -->
|
||||
<div class="d-none d-xl-block table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:60px"></th>
|
||||
<th class="mnt-name-cell">Nome</th>
|
||||
<th>Categoria</th>
|
||||
<th>Matricola / Seriale</th>
|
||||
<th>Ubicazione</th>
|
||||
<th class="text-center">Stato</th>
|
||||
<th class="text-center">Manutenzioni</th>
|
||||
<th class="text-center" style="width:140px">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($equipment as $item): ?>
|
||||
<?php
|
||||
$state = mnt_due_state($item['next_due_date']);
|
||||
// Qui la riga aggrega piu' manutenzioni: «nessuna data» non
|
||||
// significa «al bisogno», quindi l'etichetta resta neutra.
|
||||
$badge = $state === 'none'
|
||||
? ['label' => 'Senza scadenza', 'class' => 'mnt-badge-none']
|
||||
: mnt_due_badge($state);
|
||||
$statusKey = (string)$item['status'];
|
||||
?>
|
||||
<tr data-id="<?= (int)$item['id'] ?>">
|
||||
<td>
|
||||
<?php if (!empty($item['cover_stored'])): ?>
|
||||
<img class="mnt-thumb" src="manutenzioni/ajax/download_file.php?scope=equipment&id=<?= (int)$item['cover_file_id'] ?>" alt="">
|
||||
<?php else: ?>
|
||||
<span class="mnt-thumb mnt-thumb-placeholder"><i class="fa-solid fa-gear"></i></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="mnt-name-cell">
|
||||
<div class="fw-semibold mnt-title-clamp" style="color:var(--mnt-heading)"
|
||||
title="<?= mnt_h($item['name']) ?>"><?= mnt_h($item['name']) ?></div>
|
||||
<?php if ($item['manufacturer']): ?>
|
||||
<div class="small text-muted mnt-title-clamp"
|
||||
title="<?= mnt_h($item['manufacturer']) ?>"><?= mnt_h($item['manufacturer']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="d-inline-flex align-items-center gap-2">
|
||||
<span class="mnt-cat-dot" style="background: <?= mnt_h($item['category_color'] ?? '#adb5bd') ?>"></span>
|
||||
<span><?= mnt_h($item['category_name'] ?? '—') ?></span>
|
||||
</span>
|
||||
<?php if ($item['line_name']): ?>
|
||||
<div class="small text-muted">Linea <?= (int)$item['line_number'] ?> — <?= mnt_h($item['line_name']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="small">
|
||||
<?= mnt_h($item['registration_number'] ?: '—') ?>
|
||||
<?php if ($item['serial_number']): ?>
|
||||
<div class="text-muted"><?= mnt_h($item['serial_number']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="small">
|
||||
<?= mnt_h($item['department_name'] ?: '—') ?>
|
||||
<?php if ($item['location']): ?>
|
||||
<div class="text-muted"><?= mnt_h($item['location']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="mnt-badge mnt-badge-status-<?= mnt_h($statusKey) ?>">
|
||||
<?= mnt_h($statuses[$statusKey] ?? $statusKey) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<?php if ((int)$item['maintenance_count'] === 0): ?>
|
||||
<span class="text-muted small">—</span>
|
||||
<?php else: ?>
|
||||
<span class="mnt-badge <?= $badge['class'] ?>"><?= $badge['label'] ?></span>
|
||||
<div class="small text-muted mt-1"><?= (int)$item['maintenance_count'] ?> attive</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-inline-flex gap-1">
|
||||
<a class="btn-action btn-action-view" title="Apri scheda"
|
||||
href="manutenzioni/equipment.php?id=<?= (int)$item['id'] ?>">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</a>
|
||||
<?php if ($canManage): ?>
|
||||
<button class="btn-action btn-action-edit btn-edit" title="Modifica"><i class="fa-solid fa-pen"></i></button>
|
||||
<button class="btn-action btn-action-delete btn-delete"
|
||||
title="Elimina"
|
||||
data-name="<?= mnt_h($item['name']) ?>"
|
||||
data-maintenances="<?= (int)$item['maintenance_count'] ?>">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$mntPagerUrl = 'manutenzioni/index.php';
|
||||
$mntPagerPage = $page;
|
||||
$mntPagerPages = $totalPages;
|
||||
$mntPagerTotal = $totalEquipment;
|
||||
$mntPagerPer = $perPage;
|
||||
include __DIR__ . '/include/pagination.php';
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php include(__DIR__ . '/../include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php if ($canManage): ?>
|
||||
<?php include __DIR__ . '/include/equipment_modal.php'; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php include(__DIR__ . '/../jsinclude.php'); ?>
|
||||
<script>
|
||||
$(function () {
|
||||
$('#filterForm select').on('change', function () { $('#filterForm').submit(); });
|
||||
|
||||
// The search box applies itself too, so the whole bar behaves the
|
||||
// same way; the pause keeps it from reloading on every keystroke.
|
||||
let mntSearchTimer = null;
|
||||
$('#filterForm input[name=q]').on('input', function () {
|
||||
clearTimeout(mntSearchTimer);
|
||||
mntSearchTimer = setTimeout(function () { $('#filterForm').submit(); }, 500);
|
||||
});
|
||||
|
||||
// The reload lands with the box empty of focus, mid-word: put the
|
||||
// caret back where it was so typing can simply continue.
|
||||
const mntSearch = document.querySelector('#filterForm input[name=q]');
|
||||
if (mntSearch && mntSearch.value) {
|
||||
mntSearch.focus();
|
||||
mntSearch.setSelectionRange(mntSearch.value.length, mntSearch.value.length);
|
||||
}
|
||||
|
||||
<?php if ($canManage): ?>
|
||||
$('#btnAddEquipment').on('click', function () { mntOpenEquipmentModal(null); });
|
||||
|
||||
$('#equipmentList').on('click', '.btn-edit', function () {
|
||||
const id = $(this).closest('[data-id]').data('id');
|
||||
$.getJSON('manutenzioni/ajax/get_equipment.php', { id: id })
|
||||
.done(res => {
|
||||
if (res.success) { mntOpenEquipmentModal(res.equipment); }
|
||||
else { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); }
|
||||
})
|
||||
.fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
|
||||
});
|
||||
|
||||
$('#equipmentList').on('click', '.btn-delete', function () {
|
||||
const $btn = $(this);
|
||||
const id = $btn.closest('[data-id]').data('id');
|
||||
const name = $btn.data('name');
|
||||
const maintenances = parseInt($btn.data('maintenances') || 0, 10);
|
||||
|
||||
Swal.fire({
|
||||
title: `Eliminare "${name}"?`,
|
||||
html: maintenances > 0
|
||||
? `<p>Verranno eliminate anche <strong>${maintenances}</strong> manutenzioni e tutto lo storico interventi.</p>`
|
||||
: '<p>L\'operazione non è reversibile.</p>',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Elimina',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonColor: '#dc3545'
|
||||
}).then(r => {
|
||||
if (!r.isConfirmed) return;
|
||||
$.post('manutenzioni/ajax/delete_equipment.php', { id: id })
|
||||
.done(res => {
|
||||
if (res.success) { location.reload(); }
|
||||
else { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); }
|
||||
})
|
||||
.fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
|
||||
});
|
||||
});
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,370 @@
|
||||
<?php include(__DIR__ . '/../include/headscript.php'); ?>
|
||||
<?php
|
||||
require_once __DIR__ . '/include/functions.php';
|
||||
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
if (!userCan('maintenance.maintenances.view')) {
|
||||
http_response_code(403);
|
||||
exit('Permesso negato.');
|
||||
}
|
||||
|
||||
$canManage = userCan('maintenance.manage');
|
||||
|
||||
// ---------------------------------------------------------------- filters
|
||||
$filterType = (string)($_GET['type'] ?? '');
|
||||
$filterState = (string)($_GET['state'] ?? '');
|
||||
$filterCategory = isset($_GET['category']) && is_numeric($_GET['category']) ? (int)$_GET['category'] : null;
|
||||
$filterCritical = !empty($_GET['critical']);
|
||||
$filterMine = !empty($_GET['mine']);
|
||||
|
||||
$where = ["m.is_active = 1", "e.status = 'active'"];
|
||||
$params = [];
|
||||
|
||||
if (array_key_exists($filterType, mnt_intervention_types())) {
|
||||
$where[] = "m.intervention_type = ?";
|
||||
$params[] = $filterType;
|
||||
}
|
||||
if ($filterCategory) {
|
||||
$where[] = "e.category_id = ?";
|
||||
$params[] = $filterCategory;
|
||||
}
|
||||
if ($filterCritical) {
|
||||
$where[] = "m.is_critical = 1";
|
||||
}
|
||||
|
||||
// "Assigned to me" resolves the logged-in user to their employee record
|
||||
if ($filterMine) {
|
||||
$stmt = $pdo->prepare("SELECT id FROM employees WHERE auth_user_id = ? LIMIT 1");
|
||||
$stmt->execute([(int)$iduserlogin]);
|
||||
$myEmployeeId = (int)$stmt->fetchColumn();
|
||||
|
||||
if ($myEmployeeId) {
|
||||
$where[] = "(m.assignee_employee_id = ? OR m.supervisor_employee_id = ?)";
|
||||
$params[] = $myEmployeeId;
|
||||
$params[] = $myEmployeeId;
|
||||
} else {
|
||||
$filterMine = false;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT m.*,
|
||||
e.id AS equipment_id, e.name AS equipment_name,
|
||||
c.name AS category_name, c.color AS category_color,
|
||||
CONCAT(a.first_name, ' ', a.last_name) AS assignee_name,
|
||||
CONCAT(s.first_name, ' ', s.last_name) AS supervisor_name,
|
||||
sup.supplier_name
|
||||
FROM maint_maintenances m
|
||||
INNER JOIN inv_equipment e ON e.id = m.equipment_id
|
||||
LEFT JOIN inv_categories c ON c.id = e.category_id
|
||||
LEFT JOIN employees a ON a.id = m.assignee_employee_id
|
||||
LEFT JOIN employees s ON s.id = m.supervisor_employee_id
|
||||
LEFT JOIN suppliers sup ON sup.idsupplier = m.supplier_id
|
||||
WHERE " . implode(' AND ', $where) . "
|
||||
ORDER BY (m.next_due_date IS NULL) ASC, m.next_due_date ASC, e.name ASC
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// The schedule state depends on each maintenance's own lead time, so it is
|
||||
// computed in PHP rather than in SQL.
|
||||
$maintenances = [];
|
||||
$counters = ['overdue' => 0, 'due_soon' => 0, 'ok' => 0, 'none' => 0];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$state = mnt_due_state($row['next_due_date'], (int)$row['alert_days']);
|
||||
$counters[$state]++;
|
||||
|
||||
if ($filterState !== '' && $filterState !== $state) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$row['state'] = $state;
|
||||
$maintenances[] = $row;
|
||||
}
|
||||
|
||||
// Paging happens here and not in SQL: the state of each task and the four
|
||||
// counter tiles are computed in PHP over the whole result set, so a LIMIT in
|
||||
// the query would leave the tiles counting one page instead of everything.
|
||||
[$page, $perPage] = mnt_page_params();
|
||||
$totalMaintenances = count($maintenances);
|
||||
$totalPages = max(1, (int)ceil($totalMaintenances / $perPage));
|
||||
$page = min($page, $totalPages);
|
||||
$maintenances = array_slice($maintenances, ($page - 1) * $perPage, $perPage);
|
||||
|
||||
$formData = mnt_form_data($pdo);
|
||||
$typeLabels = mnt_intervention_types();
|
||||
$executionLabels = mnt_execution_types();
|
||||
|
||||
$MNT_TITLE = 'Manutenzioni';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<?php include __DIR__ . '/include/page_head.php'; ?>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<?php include __DIR__ . '/include/wrapper_open.php'; ?>
|
||||
<?php include(__DIR__ . '/../include/navbar.php'); ?>
|
||||
<?php include(__DIR__ . '/../include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<div class="card mnt-card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h5><i class="fa-solid fa-screwdriver-wrench me-2"></i>Manutenzioni</h5>
|
||||
<div class="header-actions d-flex gap-2 flex-wrap">
|
||||
<a href="manutenzioni/calendar.php" class="btn btn-mnt-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-calendar-days"></i><span>Calendario</span>
|
||||
</a>
|
||||
<a href="manutenzioni/index.php" class="btn btn-mnt-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-boxes-stacked"></i><span>Registro</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row g-2 mb-3">
|
||||
<?php
|
||||
$tiles = [
|
||||
['key' => 'overdue', 'label' => 'Scadute', 'class' => 'mnt-badge-overdue'],
|
||||
['key' => 'due_soon', 'label' => 'In scadenza', 'class' => 'mnt-badge-soon'],
|
||||
['key' => 'ok', 'label' => 'In regola', 'class' => 'mnt-badge-ok'],
|
||||
['key' => 'none', 'label' => 'Senza scadenza', 'class' => 'mnt-badge-none'],
|
||||
];
|
||||
?>
|
||||
<?php foreach ($tiles as $tile): ?>
|
||||
<div class="col-6 col-md-3">
|
||||
<?php
|
||||
// Percorso completo, non "?...": con <base href="/userarea/">
|
||||
// un href di sola query porterebbe alla dashboard del template.
|
||||
// page is dropped: changing the filter changes the result
|
||||
// set, and staying on page 3 of the previous one is nonsense.
|
||||
$tileQuery = $_GET;
|
||||
unset($tileQuery['page']);
|
||||
$tileUrl = 'manutenzioni/maintenances.php?' . http_build_query(
|
||||
array_merge($tileQuery, ['state' => $filterState === $tile['key'] ? '' : $tile['key']])
|
||||
);
|
||||
?>
|
||||
<a class="d-block text-decoration-none" href="<?= mnt_h($tileUrl) ?>">
|
||||
<div class="mnt-item-card mb-0 <?= $filterState === $tile['key'] ? 'border-2' : '' ?>"
|
||||
style="--row-color: <?= $tile['key'] === 'overdue' ? '#dc3545' : ($tile['key'] === 'due_soon' ? '#f0a202' : ($tile['key'] === 'ok' ? '#198754' : '#adb5bd')) ?>">
|
||||
<div class="ic-title" style="font-size:1.4rem"><?= (int)$counters[$tile['key']] ?></div>
|
||||
<div class="ic-meta"><?= $tile['label'] ?></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<form class="mnt-filter-bar" method="get" id="filterForm">
|
||||
<input type="hidden" name="state" value="<?= mnt_h($filterState) ?>">
|
||||
|
||||
<select class="form-select" name="type">
|
||||
<option value="">Programmate e straordinarie</option>
|
||||
<?php foreach ($typeLabels as $value => $label): ?>
|
||||
<option value="<?= mnt_h($value) ?>" <?= $filterType === $value ? 'selected' : '' ?>>
|
||||
Solo <?= mnt_h(mb_strtolower($label)) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<select class="form-select" name="category">
|
||||
<option value="">Tutte le categorie</option>
|
||||
<?php foreach ($formData['categories'] as $category): ?>
|
||||
<option value="<?= (int)$category['id'] ?>" <?= $filterCategory === (int)$category['id'] ? 'selected' : '' ?>>
|
||||
<?= mnt_h($category['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<div class="form-check d-flex align-items-center gap-2 ms-1">
|
||||
<input class="form-check-input mt-0" type="checkbox" name="critical" value="1" id="fCritical" <?= $filterCritical ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="fCritical">Solo critiche</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check d-flex align-items-center gap-2 ms-1">
|
||||
<input class="form-check-input mt-0" type="checkbox" name="mine" value="1" id="fMine" <?= $filterMine ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="fMine">Assegnate a me</label>
|
||||
</div>
|
||||
|
||||
<?php if ($perPage !== 25): ?>
|
||||
<input type="hidden" name="per_page" value="<?= (int)$perPage ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- No «Filtra» button: selects and checkboxes all submit on change,
|
||||
so it never had anything left to do. Kept hidden only as the
|
||||
no-JavaScript fallback. -->
|
||||
<button type="submit" class="visually-hidden">Filtra</button>
|
||||
|
||||
<a href="manutenzioni/maintenances.php" class="btn btn-mnt-outline btn-reset-filters d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-xmark"></i><span>Azzera filtri</span>
|
||||
</a>
|
||||
</form>
|
||||
|
||||
<?php if (!$maintenances): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fa-solid fa-screwdriver-wrench"></i>
|
||||
<p>Nessuna manutenzione con questi filtri.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div id="maintenanceList">
|
||||
<!-- CARD -->
|
||||
<div class="d-xl-none">
|
||||
<?php foreach ($maintenances as $maintenance): ?>
|
||||
<?php $badge = mnt_due_badge($maintenance['state'], $maintenance['frequency_unit']); ?>
|
||||
<div class="mnt-item-card" data-id="<?= (int)$maintenance['id'] ?>"
|
||||
style="--row-color: <?= $maintenance['is_critical'] ? '#dc3545' : mnt_h($maintenance['category_color'] ?? '#e9ecef') ?>">
|
||||
<div class="ic-title"><?= mnt_h($maintenance['equipment_name']) ?></div>
|
||||
<div class="ic-meta">
|
||||
<?php if ($maintenance['code']): ?><strong><?= mnt_h($maintenance['code']) ?></strong> · <?php endif; ?>
|
||||
<?= mnt_h($maintenance['title']) ?>
|
||||
</div>
|
||||
<div class="ic-meta">
|
||||
Prossimo: <strong><?= mnt_format_date($maintenance['next_due_date']) ?></strong>
|
||||
<span class="mnt-badge <?= $badge['class'] ?> ms-1"><?= $badge['label'] ?></span>
|
||||
</div>
|
||||
<div class="ic-actions">
|
||||
<a class="btn-action btn-action-view" href="manutenzioni/equipment.php?id=<?= (int)$maintenance['equipment_id'] ?>">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</a>
|
||||
<?php if ($canManage): ?>
|
||||
<button class="btn-action btn-action-done btn-register" title="Registra intervento">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- DESKTOP -->
|
||||
<!-- Table only from 1200px up: measured, the 6 columns need
|
||||
~1280px to fit, so on a tablet it only scrolls sideways. -->
|
||||
<div class="d-none d-xl-block table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="mnt-equipment-cell">Attrezzatura</th>
|
||||
<th>Manutenzione</th>
|
||||
<th class="mnt-freq-cell">Frequenza</th>
|
||||
<th class="mnt-person-cell">Incaricato</th>
|
||||
<th class="text-center">Prossima</th>
|
||||
<th class="text-center" style="width:130px">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($maintenances as $maintenance): ?>
|
||||
<?php $badge = mnt_due_badge($maintenance['state'], $maintenance['frequency_unit']); ?>
|
||||
<tr data-id="<?= (int)$maintenance['id'] ?>">
|
||||
<td class="mnt-equipment-cell">
|
||||
<span class="d-flex align-items-start gap-2">
|
||||
<span class="mnt-cat-dot mt-1" style="background: <?= mnt_h($maintenance['category_color'] ?? '#adb5bd') ?>"></span>
|
||||
<span class="fw-semibold mnt-title-clamp" style="color:var(--mnt-heading)"
|
||||
title="<?= mnt_h($maintenance['equipment_name']) ?>"><?= mnt_h($maintenance['equipment_name']) ?></span>
|
||||
</span>
|
||||
<div class="small text-muted"><?= mnt_h($maintenance['category_name'] ?? '') ?></div>
|
||||
</td>
|
||||
<td class="mnt-title-cell">
|
||||
<?php if ($maintenance['code']): ?>
|
||||
<span class="mnt-badge mnt-badge-soft me-1"><?= mnt_h($maintenance['code']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($maintenance['is_critical']): ?>
|
||||
<span class="mnt-badge mnt-badge-critical ms-1">Critica</span>
|
||||
<?php endif; ?>
|
||||
<div class="mnt-title-clamp" title="<?= mnt_h($maintenance['title']) ?>">
|
||||
<?= mnt_h($maintenance['title']) ?>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
<?= mnt_h($typeLabels[$maintenance['intervention_type']] ?? '') ?>
|
||||
· <?= mnt_h($executionLabels[$maintenance['execution_type']] ?? '') ?>
|
||||
<?php if ($maintenance['supplier_name']): ?>
|
||||
— <?= mnt_h($maintenance['supplier_name']) ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
<td class="small mnt-freq-cell"><div class="mnt-title-clamp"><?= mnt_h(mnt_format_frequency(
|
||||
$maintenance['frequency_value'] !== null ? (int)$maintenance['frequency_value'] : null,
|
||||
$maintenance['frequency_unit'],
|
||||
$maintenance['frequency_note']
|
||||
)) ?></div></td>
|
||||
<td class="small mnt-person-cell">
|
||||
<?= mnt_h($maintenance['assignee_name'] ?: '—') ?>
|
||||
<?php if ($maintenance['supervisor_name']): ?>
|
||||
<div class="text-muted"><?= mnt_h($maintenance['supervisor_name']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div><?= mnt_format_date($maintenance['next_due_date']) ?></div>
|
||||
<span class="mnt-badge <?= $badge['class'] ?>"><?= $badge['label'] ?></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-inline-flex gap-1">
|
||||
<a class="btn-action btn-action-view" title="Apri scheda"
|
||||
href="manutenzioni/equipment.php?id=<?= (int)$maintenance['equipment_id'] ?>">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</a>
|
||||
<?php if ($canManage): ?>
|
||||
<button class="btn-action btn-action-done btn-register" title="Registra intervento">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$mntPagerUrl = 'manutenzioni/maintenances.php';
|
||||
$mntPagerPage = $page;
|
||||
$mntPagerPages = $totalPages;
|
||||
$mntPagerTotal = $totalMaintenances;
|
||||
$mntPagerPer = $perPage;
|
||||
include __DIR__ . '/include/pagination.php';
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php include(__DIR__ . '/../include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php if ($canManage): ?>
|
||||
<?php include __DIR__ . '/include/intervention_modal.php'; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php include(__DIR__ . '/../jsinclude.php'); ?>
|
||||
<script>
|
||||
$(function () {
|
||||
$('#filterForm select, #filterForm input[type=checkbox]').on('change', function () { $('#filterForm').submit(); });
|
||||
|
||||
<?php if ($canManage): ?>
|
||||
$('#maintenanceList').on('click', '.btn-register', function () {
|
||||
const id = $(this).closest('[data-id]').data('id');
|
||||
$.getJSON('manutenzioni/ajax/get_maintenance.php', { id: id })
|
||||
.done(res => {
|
||||
if (!res.success) { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); return; }
|
||||
mntOpenInterventionModal(null, {
|
||||
id: res.maintenance.id,
|
||||
title: res.maintenance.title,
|
||||
equipment_name: res.maintenance.equipment_name,
|
||||
supplier_id: res.maintenance.supplier_id
|
||||
});
|
||||
});
|
||||
});
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php include(__DIR__ . '/../include/headscript.php'); ?>
|
||||
<?php
|
||||
require_once __DIR__ . '/include/functions.php';
|
||||
|
||||
/**
|
||||
* Printable maintenance sheet, laid out like the client's paper form
|
||||
* "mod. 6.3-1": machine header, list of interventions with letter codes and
|
||||
* frequency, then the log of performed interventions with operator + signature.
|
||||
* Print to PDF from the browser.
|
||||
*/
|
||||
|
||||
$pdo = mnt_pdo();
|
||||
|
||||
if (!userCan('maintenance.equipment.view')) {
|
||||
http_response_code(403);
|
||||
exit('Permesso negato.');
|
||||
}
|
||||
|
||||
$equipmentId = isset($_GET['id']) && is_numeric($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT e.*, c.name AS category_name, pl.name AS line_name, pl.line_number, d.name AS department_name
|
||||
FROM inv_equipment e
|
||||
LEFT JOIN inv_categories c ON c.id = e.category_id
|
||||
LEFT JOIN production_lines pl ON pl.id = e.line_id
|
||||
LEFT JOIN departments d ON d.id = e.department_id
|
||||
WHERE e.id = ?
|
||||
");
|
||||
$stmt->execute([$equipmentId]);
|
||||
$equipment = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$equipment) {
|
||||
http_response_code(404);
|
||||
exit('Attrezzatura non trovata.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT m.*, sup.supplier_name,
|
||||
CONCAT(a.first_name, ' ', a.last_name) AS assignee_name
|
||||
FROM maint_maintenances m
|
||||
LEFT JOIN suppliers sup ON sup.idsupplier = m.supplier_id
|
||||
LEFT JOIN employees a ON a.id = m.assignee_employee_id
|
||||
WHERE m.equipment_id = ? AND m.is_active = 1
|
||||
ORDER BY m.code ASC, m.title ASC
|
||||
");
|
||||
$stmt->execute([$equipmentId]);
|
||||
$maintenances = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT i.*, m.code AS maintenance_code, m.title AS maintenance_title,
|
||||
CONCAT(e.first_name, ' ', e.last_name) AS operator_full_name,
|
||||
sup.supplier_name
|
||||
FROM maint_interventions i
|
||||
INNER JOIN maint_maintenances m ON m.id = i.maintenance_id
|
||||
LEFT JOIN employees e ON e.id = i.operator_employee_id
|
||||
LEFT JOIN suppliers sup ON sup.idsupplier = i.supplier_id
|
||||
WHERE i.equipment_id = ?
|
||||
ORDER BY i.performed_at DESC, i.id DESC
|
||||
");
|
||||
$stmt->execute([$equipmentId]);
|
||||
$interventions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$statusLabels = mnt_lookup($pdo, 'maint_status', ['planned' => 'Pianificato', 'in_progress' => 'In corso', 'completed' => 'Completato']);
|
||||
$resultLabels = mnt_lookup($pdo, 'maint_result', []);
|
||||
$executionLabels = mnt_execution_types();
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<?php
|
||||
// Must sit in <head>, before any relative URL on the page
|
||||
$mntScriptName = $_SERVER['SCRIPT_NAME'] ?? '';
|
||||
$mntPos = strpos($mntScriptName, '/userarea/');
|
||||
$printBase = $mntPos !== false ? substr($mntScriptName, 0, $mntPos + strlen('/userarea/')) : '/userarea/';
|
||||
?>
|
||||
<base href="<?= mnt_h($printBase) ?>">
|
||||
<title>Scheda manutenzione — <?= mnt_h($equipment['name']) ?></title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: Arial, Helvetica, sans-serif; font-size: 11px; color: #000; margin: 0; padding: 18px; background: #fff; }
|
||||
.sheet { max-width: 1000px; margin: 0 auto; }
|
||||
|
||||
.sheet-header { display: flex; justify-content: space-between; align-items: flex-start; border: 1px solid #000; padding: 8px 10px; }
|
||||
.sheet-header h1 { font-size: 15px; margin: 0; text-transform: uppercase; letter-spacing: .02em; }
|
||||
.sheet-header .form-ref { text-align: right; font-size: 10px; line-height: 1.5; }
|
||||
|
||||
.machine-row { border: 1px solid #000; border-top: none; padding: 8px 10px; }
|
||||
.machine-row .label { font-weight: bold; text-transform: uppercase; }
|
||||
.machine-meta { display: flex; flex-wrap: wrap; gap: 4px 22px; margin-top: 6px; font-size: 10.5px; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 14px; }
|
||||
caption { text-align: left; font-weight: bold; text-transform: uppercase; font-size: 11.5px; padding: 6px 0; }
|
||||
th, td { border: 1px solid #000; padding: 5px 6px; vertical-align: top; }
|
||||
th { background: #ececec; text-transform: uppercase; font-size: 10px; text-align: left; }
|
||||
.col-code { width: 60px; text-align: center; }
|
||||
.col-freq { width: 190px; }
|
||||
.col-date { width: 90px; }
|
||||
.col-operator { width: 150px; }
|
||||
.col-sign { width: 130px; }
|
||||
.signature-img { max-height: 42px; max-width: 120px; display: block; }
|
||||
.empty-row td { text-align: center; color: #666; font-style: italic; }
|
||||
|
||||
.toolbar { max-width: 1000px; margin: 0 auto 14px; display: flex; gap: 8px; justify-content: flex-end; }
|
||||
.toolbar button, .toolbar a { font-family: inherit; font-size: 12px; padding: 7px 14px; border-radius: 5px; border: 1px solid #2f7d8f; background: #2f7d8f; color: #fff; cursor: pointer; text-decoration: none; }
|
||||
.toolbar a { background: #fff; color: #2f7d8f; }
|
||||
|
||||
.footnote { margin-top: 14px; font-size: 9.5px; color: #444; }
|
||||
|
||||
@media print {
|
||||
body { padding: 0; }
|
||||
.toolbar { display: none; }
|
||||
table { page-break-inside: auto; }
|
||||
tr { page-break-inside: avoid; page-break-after: auto; }
|
||||
thead { display: table-header-group; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="toolbar">
|
||||
<a href="manutenzioni/equipment.php?id=<?= (int)$equipment['id'] ?>">← Torna alla scheda</a>
|
||||
<button type="button" onclick="window.print()">Stampa / Salva PDF</button>
|
||||
</div>
|
||||
|
||||
<div class="sheet">
|
||||
<div class="sheet-header">
|
||||
<h1>Scheda manutenzione</h1>
|
||||
<div class="form-ref">
|
||||
ZIBOGOMMA<br>
|
||||
mod. 6.3-1<br>
|
||||
Stampato il <?= date('d/m/Y') ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="machine-row">
|
||||
<span class="label">Macchina:</span>
|
||||
<strong><?= mnt_h($equipment['name']) ?></strong>
|
||||
<?php if ($equipment['manufacturer']): ?> — <?= mnt_h($equipment['manufacturer']) ?><?php endif; ?>
|
||||
<?php if ($equipment['registration_number']): ?> — matricola <?= mnt_h($equipment['registration_number']) ?><?php endif; ?>
|
||||
|
||||
<div class="machine-meta">
|
||||
<span><strong>Categoria:</strong> <?= mnt_h($equipment['category_name'] ?: '—') ?></span>
|
||||
<?php if ($equipment['line_name']): ?>
|
||||
<span><strong>Linea:</strong> <?= (int)$equipment['line_number'] ?> — <?= mnt_h($equipment['line_name']) ?></span>
|
||||
<?php endif; ?>
|
||||
<span><strong>Numero di serie:</strong> <?= mnt_h($equipment['serial_number'] ?: '—') ?></span>
|
||||
<span><strong>Lotto:</strong> <?= mnt_h($equipment['batch_lot'] ?: '—') ?></span>
|
||||
<span><strong>Messa in servizio:</strong> <?= mnt_format_date($equipment['commissioning_date']) ?></span>
|
||||
<span><strong>Reparto:</strong> <?= mnt_h($equipment['department_name'] ?: '—') ?></span>
|
||||
<span><strong>Ubicazione:</strong> <?= mnt_h($equipment['location'] ?: '—') ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<caption>Interventi previsti</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-code">Tipo</th>
|
||||
<th>Intervento</th>
|
||||
<th class="col-freq">Frequenza</th>
|
||||
<th class="col-operator">Esecuzione</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!$maintenances): ?>
|
||||
<tr class="empty-row">
|
||||
<td colspan="4">Nessuna manutenzione attiva definita.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($maintenances as $maintenance): ?>
|
||||
<tr>
|
||||
<td class="col-code"><strong><?= mnt_h($maintenance['code'] ?: '—') ?></strong></td>
|
||||
<td>
|
||||
<strong><?= mnt_h($maintenance['title']) ?></strong>
|
||||
<?php if ($maintenance['is_critical']): ?> (critica)<?php endif; ?>
|
||||
<?php if ($maintenance['description']): ?>
|
||||
<div><?= nl2br(mnt_h($maintenance['description'])) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= mnt_h(mnt_format_frequency(
|
||||
$maintenance['frequency_value'] !== null ? (int)$maintenance['frequency_value'] : null,
|
||||
$maintenance['frequency_unit'],
|
||||
$maintenance['frequency_note']
|
||||
)) ?></td>
|
||||
<td>
|
||||
<?= mnt_h($executionLabels[$maintenance['execution_type']] ?? '') ?>
|
||||
<?php if ($maintenance['supplier_name']): ?>
|
||||
<div><?= mnt_h($maintenance['supplier_name']) ?></div>
|
||||
<?php elseif ($maintenance['assignee_name']): ?>
|
||||
<div><?= mnt_h($maintenance['assignee_name']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<caption>Registro interventi eseguiti</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-date">Data</th>
|
||||
<th class="col-code">Tipo</th>
|
||||
<th>Intervento / note</th>
|
||||
<th class="col-operator">Operatore</th>
|
||||
<th class="col-sign">Firma</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!$interventions): ?>
|
||||
<tr class="empty-row">
|
||||
<td colspan="5">Nessun intervento registrato.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($interventions as $intervention): ?>
|
||||
<tr>
|
||||
<td><?= mnt_format_date($intervention['performed_at']) ?></td>
|
||||
<td class="col-code"><?= mnt_h($intervention['maintenance_code'] ?: '—') ?></td>
|
||||
<td>
|
||||
<strong><?= mnt_h($intervention['maintenance_title']) ?></strong>
|
||||
<div>
|
||||
<?= mnt_h($statusLabels[$intervention['status']] ?? $intervention['status']) ?>
|
||||
<?php if ($intervention['result']): ?>
|
||||
— <?= mnt_h($resultLabels[$intervention['result']] ?? $intervention['result']) ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($intervention['notes']): ?>
|
||||
<div><?= nl2br(mnt_h($intervention['notes'])) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($intervention['materials']): ?>
|
||||
<div>Materiali: <?= mnt_h($intervention['materials']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?= mnt_h($intervention['operator_full_name'] ?: ($intervention['operator_name'] ?: '—')) ?>
|
||||
<?php if ($intervention['supplier_name']): ?>
|
||||
<div><?= mnt_h($intervention['supplier_name']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($intervention['signature_path']): ?>
|
||||
<img class="signature-img" alt="Firma"
|
||||
src="manutenzioni/ajax/download_file.php?scope=signature&id=<?= (int)$intervention['id'] ?>">
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="footnote">
|
||||
Documento generato automaticamente dal portale ZIBO — registro attrezzature e manutenzioni.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,59 @@
|
||||
DELIMITER $$
|
||||
|
||||
DROP PROCEDURE IF EXISTS manutenzioni_migrate_step1 $$
|
||||
CREATE PROCEDURE manutenzioni_migrate_step1()
|
||||
BEGIN
|
||||
DECLARE v_db VARCHAR(64);
|
||||
SET v_db = DATABASE();
|
||||
|
||||
-- 1. Rename production_tools -> inv_equipment
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = v_db AND table_name = 'production_tools')
|
||||
AND NOT EXISTS (SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = v_db AND table_name = 'inv_equipment') THEN
|
||||
RENAME TABLE production_tools TO inv_equipment;
|
||||
END IF;
|
||||
|
||||
-- 2. Normalise charset/collation (was utf8mb4_general_ci; the rest of
|
||||
-- the new modules are utf8mb4_unicode_ci — mixing them breaks joins
|
||||
-- on string columns with "Illegal mix of collations")
|
||||
ALTER TABLE inv_equipment CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 3. Registry columns (all nullable: existing rows stay valid)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = v_db AND table_name = 'inv_equipment'
|
||||
AND column_name = 'category_id') THEN
|
||||
ALTER TABLE inv_equipment
|
||||
ADD COLUMN category_id INT UNSIGNED NULL AFTER name,
|
||||
ADD COLUMN line_id INT NULL AFTER category_id,
|
||||
ADD COLUMN batch_lot VARCHAR(100) NULL AFTER serial_number,
|
||||
ADD COLUMN purchase_date DATE NULL AFTER batch_lot,
|
||||
ADD COLUMN commissioning_date DATE NULL AFTER purchase_date,
|
||||
ADD COLUMN department_id INT UNSIGNED NULL AFTER manufacturer,
|
||||
ADD COLUMN location VARCHAR(255) NULL AFTER department_id,
|
||||
ADD COLUMN notes TEXT NULL AFTER description,
|
||||
ADD COLUMN cover_file_id INT UNSIGNED NULL AFTER notes,
|
||||
ADD COLUMN created_by INT UNSIGNED NULL AFTER cover_file_id,
|
||||
ADD KEY idx_inv_equipment_category (category_id),
|
||||
ADD KEY idx_inv_equipment_line (line_id),
|
||||
ADD KEY idx_inv_equipment_department (department_id);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = v_db AND table_name = 'inv_equipment'
|
||||
AND column_name = 'status') THEN
|
||||
ALTER TABLE inv_equipment
|
||||
ADD COLUMN status ENUM('active','decommissioned','out_of_service') NULL AFTER location;
|
||||
UPDATE inv_equipment SET status = IF(is_active = 1, 'active', 'decommissioned');
|
||||
ALTER TABLE inv_equipment
|
||||
MODIFY COLUMN status ENUM('active','decommissioned','out_of_service')
|
||||
NOT NULL DEFAULT 'active';
|
||||
ALTER TABLE inv_equipment DROP COLUMN is_active;
|
||||
ALTER TABLE inv_equipment ADD KEY idx_inv_equipment_status (status);
|
||||
END IF;
|
||||
END $$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
CALL manutenzioni_migrate_step1();
|
||||
DROP PROCEDURE manutenzioni_migrate_step1;
|
||||
@@ -0,0 +1,171 @@
|
||||
CREATE TABLE IF NOT EXISTS inv_categories (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(150) NOT NULL,
|
||||
code VARCHAR(50) NULL,
|
||||
description TEXT NULL,
|
||||
color VARCHAR(20) NOT NULL DEFAULT '#6c757d',
|
||||
-- when 1, the equipment form requires a production line
|
||||
requires_line TINYINT(1) NOT NULL DEFAULT 0,
|
||||
sort_order INT UNSIGNED NOT NULL DEFAULT 999,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uniq_inv_categories_name (name),
|
||||
KEY idx_inv_categories_active (is_active, sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inv_equipment_files (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
equipment_id INT NOT NULL,
|
||||
kind VARCHAR(10) NOT NULL DEFAULT 'file',
|
||||
original_name VARCHAR(500) NOT NULL,
|
||||
stored_name VARCHAR(500) NOT NULL,
|
||||
mime_type VARCHAR(100) NULL,
|
||||
size INT UNSIGNED NULL,
|
||||
uploaded_by INT UNSIGNED NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_inv_files_equipment (equipment_id, kind),
|
||||
CONSTRAINT fk_inv_files_equipment FOREIGN KEY (equipment_id)
|
||||
REFERENCES inv_equipment (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS maint_maintenances (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
equipment_id INT NOT NULL,
|
||||
-- letter code as used on the paper form (A, B, C, ...)
|
||||
code VARCHAR(10) NULL,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
description TEXT NULL,
|
||||
intervention_type VARCHAR(20) NOT NULL DEFAULT 'scheduled',
|
||||
execution_type VARCHAR(20) NOT NULL DEFAULT 'internal',
|
||||
is_critical TINYINT(1) NOT NULL DEFAULT 0,
|
||||
-- frequency: NULL value + unit 'on_demand' means "no automatic due date"
|
||||
frequency_value SMALLINT UNSIGNED NULL,
|
||||
frequency_unit VARCHAR(20) NOT NULL DEFAULT 'month',
|
||||
-- free text for the irregular cases in the client's sheets
|
||||
-- ("prima dell'accensione / d'estate settimanale / dopo lo spegnimento")
|
||||
frequency_note VARCHAR(500) NULL,
|
||||
alert_days SMALLINT UNSIGNED NOT NULL DEFAULT 7,
|
||||
assignee_employee_id INT UNSIGNED NULL,
|
||||
supervisor_employee_id INT UNSIGNED NULL,
|
||||
supplier_id INT NULL,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
-- denormalised, recomputed on every intervention change
|
||||
last_done_date DATE NULL,
|
||||
next_due_date DATE NULL,
|
||||
notes TEXT NULL,
|
||||
created_by INT UNSIGNED NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
KEY idx_maint_equipment (equipment_id),
|
||||
KEY idx_maint_due (next_due_date),
|
||||
KEY idx_maint_type (intervention_type, is_active),
|
||||
CONSTRAINT fk_maint_equipment FOREIGN KEY (equipment_id)
|
||||
REFERENCES inv_equipment (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_maint_assignee FOREIGN KEY (assignee_employee_id)
|
||||
REFERENCES employees (id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_maint_supervisor FOREIGN KEY (supervisor_employee_id)
|
||||
REFERENCES employees (id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_maint_supplier FOREIGN KEY (supplier_id)
|
||||
REFERENCES suppliers (idsupplier) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS maint_maintenance_files (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
maintenance_id INT UNSIGNED NOT NULL,
|
||||
original_name VARCHAR(500) NOT NULL,
|
||||
stored_name VARCHAR(500) NOT NULL,
|
||||
mime_type VARCHAR(100) NULL,
|
||||
size INT UNSIGNED NULL,
|
||||
uploaded_by INT UNSIGNED NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_mmf_maintenance (maintenance_id),
|
||||
CONSTRAINT fk_mmf_maintenance FOREIGN KEY (maintenance_id)
|
||||
REFERENCES maint_maintenances (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS maint_interventions (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
maintenance_id INT UNSIGNED NOT NULL,
|
||||
-- denormalised so the equipment history is a single-table scan
|
||||
equipment_id INT NOT NULL,
|
||||
performed_at DATE NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'completed',
|
||||
result VARCHAR(20) NULL,
|
||||
notes TEXT NULL,
|
||||
materials TEXT NULL,
|
||||
operator_employee_id INT UNSIGNED NULL,
|
||||
-- free text operator for external technicians not present in employees
|
||||
operator_name VARCHAR(191) NULL,
|
||||
signature_path VARCHAR(255) NULL,
|
||||
supplier_id INT NULL,
|
||||
created_by INT UNSIGNED NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
KEY idx_int_maintenance (maintenance_id, performed_at),
|
||||
KEY idx_int_equipment (equipment_id, performed_at),
|
||||
KEY idx_int_status (status),
|
||||
CONSTRAINT fk_int_maintenance FOREIGN KEY (maintenance_id)
|
||||
REFERENCES maint_maintenances (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_int_equipment FOREIGN KEY (equipment_id)
|
||||
REFERENCES inv_equipment (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_int_operator FOREIGN KEY (operator_employee_id)
|
||||
REFERENCES employees (id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_int_supplier FOREIGN KEY (supplier_id)
|
||||
REFERENCES suppliers (idsupplier) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS maint_intervention_files (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
intervention_id INT UNSIGNED NOT NULL,
|
||||
original_name VARCHAR(500) NOT NULL,
|
||||
stored_name VARCHAR(500) NOT NULL,
|
||||
mime_type VARCHAR(100) NULL,
|
||||
size INT UNSIGNED NULL,
|
||||
uploaded_by INT UNSIGNED NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_mif_intervention (intervention_id),
|
||||
CONSTRAINT fk_mif_intervention FOREIGN KEY (intervention_id)
|
||||
REFERENCES maint_interventions (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS maint_histories (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
equipment_id INT NULL,
|
||||
maintenance_id INT UNSIGNED NULL,
|
||||
intervention_id INT UNSIGNED NULL,
|
||||
user_id INT UNSIGNED NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
changes TEXT NULL,
|
||||
notes TEXT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_hist_equipment (equipment_id, created_at),
|
||||
KEY idx_hist_maintenance (maintenance_id, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS maint_notifications (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
maintenance_id INT UNSIGNED NOT NULL,
|
||||
employee_id INT UNSIGNED NULL,
|
||||
email VARCHAR(191) NOT NULL,
|
||||
type VARCHAR(20) NOT NULL,
|
||||
sent_date DATE NOT NULL,
|
||||
sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uniq_maint_notif (maintenance_id, email, type, sent_date),
|
||||
CONSTRAINT fk_notif_maintenance FOREIGN KEY (maintenance_id)
|
||||
REFERENCES maint_maintenances (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO ws_lookup_options (category, value, label, sort_order, is_default, is_active) VALUES
|
||||
('maint_status', 'planned', 'Pianificato', 10, 0, 1),
|
||||
('maint_status', 'in_progress', 'In corso', 20, 0, 1),
|
||||
('maint_status', 'completed', 'Completato', 30, 1, 1),
|
||||
|
||||
('maint_result', 'compliant', 'Conforme', 10, 1, 1),
|
||||
('maint_result', 'not_compliant','Non conforme', 20, 0, 1),
|
||||
('maint_result', 'blocked', 'Bloccato', 30, 0, 1),
|
||||
('maint_result', 'partial', 'Parziale', 40, 0, 1),
|
||||
|
||||
('equipment_status', 'active', 'Attivo', 10, 1, 1),
|
||||
('equipment_status', 'out_of_service', 'Fuori servizio', 20, 0, 1),
|
||||
('equipment_status', 'decommissioned', 'Dismesso', 30, 0, 1);
|
||||
@@ -0,0 +1,68 @@
|
||||
INSERT IGNORE INTO inv_categories (name, code, color, requires_line, sort_order) VALUES
|
||||
('Linea di produzione', 'line', '#dc2626', 1, 10),
|
||||
('Attrezzature', 'equipment', '#0d6efd', 0, 20),
|
||||
('Accessori', 'accessories', '#20c997', 0, 30),
|
||||
('Qualità', 'quality', '#6f42c1', 0, 40);
|
||||
|
||||
UPDATE inv_equipment
|
||||
SET category_id = (SELECT id FROM inv_categories WHERE code = 'equipment')
|
||||
WHERE category_id IS NULL AND line_id IS NULL;
|
||||
|
||||
INSERT INTO inv_equipment (name, category_id, line_id, status, description, created_at, updated_at)
|
||||
SELECT
|
||||
pl.name,
|
||||
(SELECT id FROM inv_categories WHERE code = 'line'),
|
||||
pl.id,
|
||||
IF(pl.status = 'active', 'active', 'out_of_service'),
|
||||
CONCAT_WS(' ', NULLIF(pl.brand, ''), NULLIF(pl.model, '')),
|
||||
NOW(), NOW()
|
||||
FROM production_lines pl
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM inv_equipment e WHERE e.line_id = pl.id
|
||||
);
|
||||
|
||||
DELIMITER $$
|
||||
|
||||
DROP PROCEDURE IF EXISTS manutenzioni_migrate_step3 $$
|
||||
CREATE PROCEDURE manutenzioni_migrate_step3()
|
||||
BEGIN
|
||||
DECLARE v_db VARCHAR(64);
|
||||
SET v_db = DATABASE();
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_schema = v_db AND table_name = 'inv_equipment'
|
||||
AND constraint_name = 'fk_inv_equipment_category') THEN
|
||||
ALTER TABLE inv_equipment
|
||||
ADD CONSTRAINT fk_inv_equipment_category FOREIGN KEY (category_id)
|
||||
REFERENCES inv_categories (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_schema = v_db AND table_name = 'inv_equipment'
|
||||
AND constraint_name = 'fk_inv_equipment_line') THEN
|
||||
ALTER TABLE inv_equipment
|
||||
ADD CONSTRAINT fk_inv_equipment_line FOREIGN KEY (line_id)
|
||||
REFERENCES production_lines (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_schema = v_db AND table_name = 'inv_equipment'
|
||||
AND constraint_name = 'fk_inv_equipment_department') THEN
|
||||
ALTER TABLE inv_equipment
|
||||
ADD CONSTRAINT fk_inv_equipment_department FOREIGN KEY (department_id)
|
||||
REFERENCES departments (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_schema = v_db AND table_name = 'inv_equipment'
|
||||
AND constraint_name = 'fk_inv_equipment_cover') THEN
|
||||
ALTER TABLE inv_equipment
|
||||
ADD CONSTRAINT fk_inv_equipment_cover FOREIGN KEY (cover_file_id)
|
||||
REFERENCES inv_equipment_files (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
CALL manutenzioni_migrate_step3();
|
||||
DROP PROCEDURE manutenzioni_migrate_step3;
|
||||
@@ -0,0 +1,19 @@
|
||||
INSERT IGNORE INTO auth_permissions
|
||||
(name, display_name, description, removable, created_at, updated_at)
|
||||
VALUES
|
||||
('maintenance.equipment.view', 'View Equipment Registry', 'Can view the equipment registry.', 1, NOW(), NOW()),
|
||||
('maintenance.maintenances.view','View Maintenances', 'Can view maintenances and the intervention log.', 1, NOW(), NOW()),
|
||||
('maintenance.calendar.view', 'View Maintenance Calendar','Can view the maintenance calendar.', 1, NOW(), NOW()),
|
||||
('maintenance.categories.view', 'View Equipment Categories','Can view equipment categories.', 1, NOW(), NOW()),
|
||||
('maintenance.manage', 'Manage Maintenance', 'Can create/edit equipment, maintenances and interventions.', 1, NOW(), NOW());
|
||||
|
||||
INSERT IGNORE INTO auth_permission_role (permission_id, role_id)
|
||||
SELECT p.id, 1
|
||||
FROM auth_permissions p
|
||||
WHERE p.name IN (
|
||||
'maintenance.equipment.view',
|
||||
'maintenance.maintenances.view',
|
||||
'maintenance.calendar.view',
|
||||
'maintenance.categories.view',
|
||||
'maintenance.manage'
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
+856
-61
File diff suppressed because it is too large
Load Diff
@@ -272,6 +272,13 @@
|
||||
<td><span class='qty-badge'>{$qtyTot}</span></td>
|
||||
<td>{$badge}</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'
|
||||
data-id='{$r['id']}'
|
||||
data-name='" . htmlspecialchars($r['item_name'], ENT_QUOTES) . "'>
|
||||
@@ -342,7 +349,47 @@
|
||||
</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 -->
|
||||
<div class="modal fade" id="stockModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
@@ -505,7 +552,48 @@
|
||||
|
||||
$('#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
|
||||
$("#addItemForm").on("submit", function(e) {
|
||||
e.preventDefault();
|
||||
@@ -558,11 +646,16 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Flag: true se lo stock è stato modificato mentre il modale era aperto
|
||||
let stockChanged = false;
|
||||
|
||||
// Open stock modal
|
||||
$(document).on("click", ".manage-stock", function() {
|
||||
const id = $(this).data("id");
|
||||
const name = $(this).data("name");
|
||||
|
||||
stockChanged = false; // reset ad ogni apertura
|
||||
|
||||
$("#stockItemId").val(id);
|
||||
$("#stockItemName").text(name);
|
||||
|
||||
@@ -571,6 +664,13 @@
|
||||
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
|
||||
$(document).on("click", ".stock-edit", function() {
|
||||
$("#stockEditId").val($(this).data("id"));
|
||||
@@ -629,11 +729,9 @@
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
stockChanged = true;
|
||||
stockResetForm();
|
||||
stockLoadRows(itemId);
|
||||
// Refresh totals by reloading page (simple and safe)
|
||||
// If you want live update without reload, we can do it.
|
||||
// location.reload();
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: "error",
|
||||
@@ -669,8 +767,8 @@
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
stockChanged = true;
|
||||
stockLoadRows(itemId);
|
||||
// location.reload();
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: "error",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -150,17 +150,24 @@ $dashboardSections = [
|
||||
[
|
||||
'id' => 'secPersonale',
|
||||
'title' => 'Personale',
|
||||
'subtitle' => 'Dipendenti, skill',
|
||||
'subtitle' => 'Dipendenti, formazione, skill',
|
||||
'icon' => '👥',
|
||||
'open' => false,
|
||||
'buttons' => [
|
||||
[
|
||||
'label' => 'Employees',
|
||||
'label' => 'Dipendenti',
|
||||
'icon' => '👥',
|
||||
'class' => 'btn-employees',
|
||||
'url' => 'employees.php',
|
||||
'permission' => 'hr.employees.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Mansioni',
|
||||
'icon' => '🧩',
|
||||
'class' => 'btn-setup',
|
||||
'url' => 'job-roles.php',
|
||||
'permission' => 'hr.employees.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Departments',
|
||||
'icon' => '🏢',
|
||||
@@ -168,6 +175,20 @@ $dashboardSections = [
|
||||
'url' => 'departments.php',
|
||||
'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',
|
||||
'icon' => '🧠',
|
||||
@@ -494,45 +515,97 @@ $dashboardSections = [
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
.my-deadlines-widgets:empty { display: none; }
|
||||
|
||||
.my-deadlines-widgets:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Each widget wraps itself in .my-deadlines-widgets; collapse the nested
|
||||
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;
|
||||
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);
|
||||
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); }
|
||||
.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 {
|
||||
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: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;
|
||||
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-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;
|
||||
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;
|
||||
}
|
||||
.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'); ?>
|
||||
|
||||
@@ -288,7 +288,7 @@ if (!empty($_GET['ajax'])) {
|
||||
(
|
||||
SELECT GROUP_CONCAT(t.name ORDER BY t.name SEPARATOR ' | ')
|
||||
FROM productiondata_tools pt
|
||||
JOIN production_tools t ON t.id = pt.tool_id
|
||||
JOIN inv_equipment t ON t.id = pt.tool_id
|
||||
WHERE pt.productiondata_id = p.id
|
||||
) AS tools_list,
|
||||
(
|
||||
@@ -350,7 +350,7 @@ if (!empty($_GET['ajax'])) {
|
||||
(
|
||||
SELECT GROUP_CONCAT(t.name ORDER BY t.name SEPARATOR ' | ')
|
||||
FROM productiondata_tools pt
|
||||
JOIN production_tools t ON t.id = pt.tool_id
|
||||
JOIN inv_equipment t ON t.id = pt.tool_id
|
||||
WHERE pt.productiondata_id = p.id
|
||||
) AS tools_list,
|
||||
(
|
||||
|
||||
@@ -127,7 +127,15 @@
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$stmt = $pdo->query("SELECT * FROM production_tools ORDER BY id ASC");
|
||||
// production_tools is now the unified registry inv_equipment;
|
||||
// this page keeps showing the "Attrezzature" slice of it.
|
||||
$stmt = $pdo->query("
|
||||
SELECT e.*
|
||||
FROM inv_equipment e
|
||||
LEFT JOIN inv_categories c ON c.id = e.category_id
|
||||
WHERE c.code = 'equipment' OR e.category_id IS NULL
|
||||
ORDER BY e.id ASC
|
||||
");
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)):
|
||||
?>
|
||||
<tr>
|
||||
@@ -139,10 +147,17 @@
|
||||
<td><?= nl2br(htmlspecialchars($row['description'] ?? '')) ?></td>
|
||||
|
||||
<td>
|
||||
<?php if ((int)$row['is_active'] === 1): ?>
|
||||
<?php
|
||||
$statusLabels = [
|
||||
'active' => 'Attivo',
|
||||
'out_of_service' => 'Fuori servizio',
|
||||
'decommissioned' => 'Dismesso',
|
||||
];
|
||||
?>
|
||||
<?php if ($row['status'] === 'active'): ?>
|
||||
<span class="badge-active">Attivo</span>
|
||||
<?php else: ?>
|
||||
<span class="badge-inactive">Non attivo</span>
|
||||
<span class="badge-inactive"><?= htmlspecialchars($statusLabels[$row['status']] ?? $row['status']) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
@@ -156,7 +171,7 @@
|
||||
data-type="<?= htmlspecialchars($row['tool_type'] ?? '', ENT_QUOTES) ?>"
|
||||
data-manufacturer="<?= htmlspecialchars($row['manufacturer'] ?? '', ENT_QUOTES) ?>"
|
||||
data-desc="<?= htmlspecialchars($row['description'] ?? '', ENT_QUOTES) ?>"
|
||||
data-active="<?= (int)$row['is_active'] ?>">
|
||||
data-status="<?= htmlspecialchars($row['status'], ENT_QUOTES) ?>">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
|
||||
@@ -214,9 +229,10 @@
|
||||
<textarea name="description" class="form-control mb-3"></textarea>
|
||||
|
||||
<label class="fw-semibold">Stato</label>
|
||||
<select name="is_active" class="form-control">
|
||||
<option value="1">Attivo</option>
|
||||
<option value="0">Non attivo</option>
|
||||
<select name="status" class="form-control">
|
||||
<option value="active">Attivo</option>
|
||||
<option value="out_of_service">Fuori servizio</option>
|
||||
<option value="decommissioned">Dismesso</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
@@ -265,9 +281,10 @@
|
||||
<textarea id="edit_description" name="description" class="form-control mb-3"></textarea>
|
||||
|
||||
<label class="fw-semibold">Stato</label>
|
||||
<select id="edit_is_active" name="is_active" class="form-control">
|
||||
<option value="1">Attivo</option>
|
||||
<option value="0">Non attivo</option>
|
||||
<select id="edit_status" name="status" class="form-control">
|
||||
<option value="active">Attivo</option>
|
||||
<option value="out_of_service">Fuori servizio</option>
|
||||
<option value="decommissioned">Dismesso</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
@@ -327,7 +344,7 @@
|
||||
$("#edit_tool_type").val($(this).data("type"));
|
||||
$("#edit_manufacturer").val($(this).data("manufacturer"));
|
||||
$("#edit_description").val($(this).data("desc"));
|
||||
$("#edit_is_active").val($(this).data("active"));
|
||||
$("#edit_status").val($(this).data("status"));
|
||||
|
||||
$("#editToolModal").modal("show");
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user