Pont d'écriture Ops → legacy osTicket via endpoint PHP token-gated (hérite des droits write de l'app facturation.targo.ca → AUCUN grant DB). Fidèle à ticket_view.php : réassignation (assign_to + participant[assistants] + followed_by + ticket_msg + lock respecté) + fermeture (status=closed + date_closed + closed_by + réouverture des enfants). Auteur du log = utilisateur Authentik réel (email → staff legacy par email/nom). Hub (legacy-dispatch-sync.js): - adresse de SERVICE importée (champ address) + reimportAddresses + fillMissingCoords - garde TERRITOIRE (rejette les homonymes hors-zone Gaspésie/Outaouais/Estrie) + centroïde CP/ville - purgeStaleOrphans (anciens imports TT- périmés), # ticket legacy dans titre + description - legacyWrite (POST form + X-Ops-Token lu d'un fichier), pushAssignments (reassign), closeTicketLegacy roster.js: occupancy + unassigned-jobs exposent address/latitude/longitude. Ops (PlanificationPage.vue): - carte des jobs à assigner (pins couleur=compétence + lettre repère liste↔carte), chips filtre par type, panneau ouvert par défaut - clics cellule: bande quart/garde + blocs jobs → tournée ; fond → menu horaire (fini le clic droit) - éditeur de tournée: itinéraire routier réel + adresse + fil ticket (expand), refresh occupation à l'assignation - bouton « Publier au legacy » (aperçu + ✕ désassigner) + « Fermer le ticket » - fix dates (lundi calculé en local — plus de décalage UTC) + nav ±1 jour services/legacy-bridge/ops_reassign.php: endpoint (placeholders; secrets injectés au déploiement, hors repo). scripts/migration: backfill_dispatch_address.sql + diagnostics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
92 lines
5.3 KiB
PHP
92 lines
5.3 KiB
PHP
<?php
|
|
/**
|
|
* ops_reassign.php — Pont d'écriture Ops → legacy osTicket (gestionclient).
|
|
* Déployé SUR facturation.targo.ca (hôte de l'app) → la connexion DB hérite des droits d'ÉCRITURE
|
|
* de l'app (aucun GRANT à modifier ; le user du hub @10.100.5.61 reste SELECT-only).
|
|
*
|
|
* Actions (POST, x-www-form-urlencoded ou JSON), header `X-Ops-Token: <token>` OBLIGATOIRE :
|
|
* action=reassign ticket_id, staff_id, [assist_ids=CSV] → assign_to + participant(assistants) + followed_by + ticket_msg
|
|
* action=close ticket_id → status=closed + date_closed + closed_by + réouverture enfants + ticket_msg
|
|
*
|
|
* FIDÈLE à ticket_view.php : format participant `-id-;-id-`, followers JSON, log CHANGE LOG, respect du lock_name,
|
|
* dates en epoch (time()), réouverture des tickets enfants `waiting_for` à la fermeture.
|
|
*
|
|
* ⚠️ Secrets (TOKEN + creds DB) : remplis UNIQUEMENT dans la copie serveur, jamais committés. Ici = placeholders.
|
|
*/
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
// ─── à remplir sur le serveur (hors repo) ───
|
|
$OPS_TOKEN = '__OPS_TOKEN__';
|
|
$DB_HOST = '10.100.80.100';
|
|
$DB_USER = '__DB_USER__';
|
|
$DB_PASS = '__DB_PASS__';
|
|
$DB_NAME = 'gestionclient';
|
|
$OPS_STAFF = 3301; // auteur des entrées ticket_msg (Tech Targo)
|
|
|
|
function out($a, $code = 200) { http_response_code($code); echo json_encode($a, JSON_UNESCAPED_UNICODE); exit; }
|
|
|
|
// ─── auth ───
|
|
$tok = $_SERVER['HTTP_X_OPS_TOKEN'] ?? ($_POST['token'] ?? '');
|
|
if (!is_string($tok) || !hash_equals($OPS_TOKEN, $tok)) out(['ok' => false, 'error' => 'forbidden'], 403);
|
|
|
|
$action = $_POST['action'] ?? 'reassign';
|
|
$ticket = (int)($_POST['ticket_id'] ?? 0);
|
|
if ($ticket <= 0) out(['ok' => false, 'error' => 'ticket_id requis'], 400);
|
|
// Auteur réel de l'action = staff legacy du répartiteur Ops (mappé depuis son email Authentik côté hub) ; repli = Tech Targo.
|
|
$actor = (int)($_POST['actor_staff_id'] ?? 0);
|
|
$logStaff = $actor > 0 ? $actor : $OPS_STAFF;
|
|
|
|
// ─── connexion (droits write hérités de l'hôte app) ───
|
|
$db = @new mysqli($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME);
|
|
if ($db->connect_errno) out(['ok' => false, 'error' => 'db_connect: ' . $db->connect_error], 500);
|
|
$db->set_charset('utf8');
|
|
|
|
// ─── ticket courant ───
|
|
$res = $db->query("SELECT id, status, assign_to, lock_name, followed_by FROM ticket WHERE id = $ticket LIMIT 1");
|
|
$t = $res ? $res->fetch_assoc() : null;
|
|
if (!$t) out(['ok' => false, 'error' => 'ticket introuvable'], 404);
|
|
if ($t['status'] === 'closed') out(['ok' => false, 'error' => 'ticket déjà fermé', 'status' => 'closed']);
|
|
// LOCK : un staff édite le ticket dans le legacy → on NE clobbe PAS (le répartiteur réessaie)
|
|
if (trim((string)$t['lock_name']) !== '') out(['ok' => false, 'error' => 'verrouillé', 'locked_by' => $t['lock_name']]);
|
|
|
|
function ops_log($db, $ticket, $staff, $msg) {
|
|
$m = $db->real_escape_string('<hr><p>CHANGE LOG (Ops):</p><p>' . $msg . '</p>');
|
|
$db->query("INSERT INTO ticket_msg (ticket_id, staff_id, msg, date_orig, public) VALUES ($ticket, $staff, '$m', " . time() . ", 0)");
|
|
}
|
|
|
|
$now = time();
|
|
|
|
if ($action === 'close') {
|
|
$db->query("UPDATE ticket SET status='closed', date_closed=$now, closed_by=$logStaff, last_update=$now WHERE id=$ticket AND status<>'closed'");
|
|
$aff = $db->affected_rows;
|
|
// réouvre les tickets enfants en attente de celui-ci (fidèle au legacy)
|
|
$db->query("UPDATE ticket SET status='open', last_update=$now WHERE waiting_for=$ticket AND status<>'closed'");
|
|
ops_log($db, $ticket, $logStaff, 'Ticket fermé via Ops.');
|
|
out(['ok' => true, 'action' => 'close', 'affected' => $aff]);
|
|
}
|
|
|
|
// ─── reassign (+ assistants) ───
|
|
$staff = (int)($_POST['staff_id'] ?? 0);
|
|
if ($staff <= 0) out(['ok' => false, 'error' => 'staff_id requis'], 400);
|
|
$chk = $db->query("SELECT id, first_name, last_name FROM staff WHERE id=$staff AND status=1");
|
|
$s = $chk ? $chk->fetch_assoc() : null;
|
|
if (!$s) out(['ok' => false, 'error' => 'staff inactif ou introuvable', 'staff_id' => $staff]);
|
|
|
|
// assistants : CSV d'ids → participant `-id-;-id-` + ajout aux followers (followed_by JSON) — FIDÈLE à ticket_view.php
|
|
$assistCsv = trim((string)($_POST['assist_ids'] ?? ''));
|
|
$assistIds = $assistCsv === '' ? [] : array_values(array_unique(array_filter(array_map('intval', explode(',', $assistCsv)))));
|
|
$participant = '';
|
|
foreach ($assistIds as $a) { $participant .= ($participant === '' ? '' : ';') . "-$a-"; }
|
|
$aFollow = json_decode($t['followed_by'] !== '' && $t['followed_by'] !== null ? $t['followed_by'] : '{}', true);
|
|
if (!is_array($aFollow)) $aFollow = [];
|
|
foreach ($assistIds as $a) { if (!array_key_exists((string)$a, $aFollow)) $aFollow[(string)$a] = ['child' => 0]; }
|
|
$jsonFollow = $db->real_escape_string(json_encode((object)$aFollow));
|
|
$participantEsc = $db->real_escape_string($participant);
|
|
|
|
$db->query("UPDATE ticket SET assign_to=$staff, participant='$participantEsc', followed_by='$jsonFollow', last_update=$now WHERE id=$ticket AND status<>'closed'");
|
|
$aff = $db->affected_rows;
|
|
$who = trim(($s['first_name'] ?? '') . ' ' . ($s['last_name'] ?? ''));
|
|
$log = "Réassigné à #$staff ($who) via Ops." . ($assistIds ? ' Assistants : ' . implode(', ', $assistIds) . '.' : '');
|
|
ops_log($db, $ticket, $logStaff, $log);
|
|
out(['ok' => true, 'action' => 'reassign', 'affected' => $aff, 'staff' => $staff, 'assistants' => $assistIds]);
|