` OBLIGATOIRE : * action=reassign ticket_id, staff_id, [assist_ids=CSV] [notify=1] [address] → assign_to + participant(assistants) + followed_by + ticket_msg * notify=1 → courriel d'assignation au tech (résumé + lien Répondre + Adresse), via PHPMailer6 OAuth2 (ops_secret.php) * 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'); // ─── secrets : fichier serveur HORS REPO `ops_secret.php` (à côté de ce fichier) qui définit : // $OPS_TOKEN, $DB_USER, $DB_PASS + creds OAuth Gmail $oauthEmail,$clientId,$clientSecret,$refreshToken // (réutilise les mêmes creds que ticket_view.php). JAMAIS committé. require __DIR__ . '/ops_secret.php'; $DB_HOST = '10.100.80.100'; $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'; // Auteur réel = staff legacy du répartiteur Ops (mappé depuis son email Authentik côté hub) ; repli = Tech Targo. $actor = (int)($_POST['actor_staff_id'] ?? 0); // ─── 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'); $now = time(); $logStaff = $actor > 0 ? $actor : $OPS_STAFF; function ops_log($db, $ticket, $staff, $msg) { $m = $db->real_escape_string('

CHANGE LOG (Ops):

' . $msg . '

'); $db->query("INSERT INTO ticket_msg (ticket_id, staff_id, msg, date_orig, public) VALUES ($ticket, $staff, '$m', " . time() . ", 0)"); } // ─── FERMETURE EN LOT : 1 connexion, N tickets (efficace — évite N allers-retours HTTPS + mysqli) ─── if ($action === 'batch_close') { $ids = array_values(array_unique(array_filter(array_map('intval', explode(',', (string)($_POST['ticket_ids'] ?? '')))))); $closed = 0; $skipped = 0; $lockedN = 0; $results = []; foreach ($ids as $tid) { $r = $db->query("SELECT status, lock_name FROM ticket WHERE id = $tid LIMIT 1"); $row = $r ? $r->fetch_assoc() : null; if (!$row || $row['status'] === 'closed') { $skipped++; $results[] = ['t' => $tid, 'skip' => true]; continue; } if (trim((string)$row['lock_name']) !== '') { $lockedN++; $results[] = ['t' => $tid, 'locked' => $row['lock_name']]; continue; } $db->query("UPDATE ticket SET status='closed', date_closed=$now, closed_by=$logStaff, last_update=$now WHERE id=$tid AND status<>'closed'"); $db->query("UPDATE ticket SET status='open', last_update=$now WHERE waiting_for=$tid AND status<>'closed'"); ops_log($db, $tid, $logStaff, 'Ticket fermé via Ops (lot).'); $closed++; $results[] = ['t' => $tid, 'ok' => true]; } out(['ok' => true, 'action' => 'batch_close', 'closed' => $closed, 'skipped' => $skipped, 'locked' => $lockedN, 'results' => $results]); } // ─── actions sur 1 ticket ─── $ticket = (int)($_POST['ticket_id'] ?? 0); if ($ticket <= 0) out(['ok' => false, 'error' => 'ticket_id requis'], 400); $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']]); 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, email 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); // ─── avis courriel au nouveau tech (notify=1) — MÊME contenu que ticket_view.php (résumé + lien Répondre), // + ligne « Adresse » fournie par Ops. Envoi via PHPMailer6 OAuth2 (creds dans ops_oauth_boot.php, hors repo). ─── $notified = false; $notifyErr = null; $notifyTo = ''; if (!empty($_POST['notify'])) { $staffEmail = trim((string)($s['email'] ?? '')); $notifyTo = $staffEmail; if ($staffEmail === '') { $notifyErr = 'staff sans courriel'; } else { $opsAddress = trim((string)($_POST['address'] ?? '')); // adresse de service propre (Ops) $tq = $db->query("SELECT subject, dept_id, account_id, status, due_date, due_time, bon_id, wizard_fibre FROM ticket WHERE id=$ticket LIMIT 1"); $tr = $tq ? $tq->fetch_assoc() : []; $subject = (string)($tr['subject'] ?? ''); $deptId = (int)($tr['dept_id'] ?? 0); $accId = (int)($tr['account_id'] ?? 0); $tstatus = (string)($tr['status'] ?? ''); $dueEpoch = (int)($tr['due_date'] ?? 0); $dueStr = $dueEpoch > 0 ? date('d-m-Y', $dueEpoch) : 'sans date'; $dueTime = trim((string)($tr['due_time'] ?? '')); $dueTimeStr = ($dueTime !== '' && $dueEpoch > 0) ? " - $dueTime" : ''; $bonId = trim((string)($tr['bon_id'] ?? '')); $wizFibre = trim((string)($tr['wizard_fibre'] ?? '')); $dn = $db->query("SELECT name FROM ticket_dept WHERE id=$deptId LIMIT 1"); $dnr = $dn ? $dn->fetch_assoc() : null; $deptName = (string)($dnr['name'] ?? ''); $cn = $db->query("SELECT first_name, last_name, company, customer_id FROM account WHERE id=$accId LIMIT 1"); $cnr = $cn ? $cn->fetch_assoc() : null; $clientName = $cnr ? trim(($cnr['first_name'] ?? '') . ' ' . ($cnr['last_name'] ?? '') . ' ' . ($cnr['company'] ?? '') . ' - ' . ($cnr['customer_id'] ?? '')) : ''; // liens ONU (fibre) / bon de travail — fidèle à ticket_view.php $onu = ''; if ($wizFibre !== '') { $wi = explode('|', $wizFibre); $onu = "Connecter ONU

"; } if ($deptId === 26) $onu = "Remplacer ONU

"; $bonLink = $bonId !== '' ? "Ouvrir Bon de Travail: https://facturation.targo.ca/facturation/accueil.php?menu=bon_travail&bon_id=$bonId

" : ''; $addrLine = $opsAddress !== '' ? "Adresse: " . htmlspecialchars($opsAddress, ENT_QUOTES) . "
" : ''; $replyLink = "https://store.targo.ca/targo/reply_ticket.php?ticket=$ticket&staff=$staff"; $subjectLine = html_entity_decode("[Ticket #$ticket] $subject [$deptName]", ENT_QUOTES); $body = "Ce ticket vous a été assigné.

" . "Répondre: $replyLink

" . $onu . $bonLink . "Ticket ID: $ticket
" . "Client: $clientName

" . "Sujet: $subject
" . "Status: $tstatus
" . "Due date: $dueStr $dueTimeStr
" . $addrLine . "Département: $deptName

"; try { // L'app legacy (vendor + PHPMailer6) est dans ./facturation quand ce script est au docroot /var/www ; sinon ici. $APP_DIR = is_dir(__DIR__ . '/facturation/PHPMailer6') ? __DIR__ . '/facturation' : __DIR__; require_once $APP_DIR . '/vendor/autoload.php'; // League\OAuth2 (Google provider) — déjà dans l'app require_once $APP_DIR . '/PHPMailer6/Exception.php'; require_once $APP_DIR . '/PHPMailer6/PHPMailer.php'; require_once $APP_DIR . '/PHPMailer6/SMTP.php'; require_once $APP_DIR . '/PHPMailer6/OAuth.php'; $provider = new \League\OAuth2\Client\Provider\Google(['clientId' => $clientId, 'clientSecret' => $clientSecret]); // creds depuis ops_secret.php $mail = new \PHPMailer\PHPMailer\PHPMailer(true); $mail->isSMTP(); $mail->Host = gethostbyname('smtp.gmail.com'); $mail->SMTPOptions = ['ssl' => ['verify_peer_name' => false]]; $mail->Port = 587; $mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS; $mail->SMTPAuth = true; $mail->AuthType = 'XOAUTH2'; $mail->setOAuth(new \PHPMailer\PHPMailer\OAuth(['provider' => $provider, 'clientId' => $clientId, 'clientSecret' => $clientSecret, 'refreshToken' => $refreshToken, 'userName' => $oauthEmail])); $mail->setFrom('ticket@targointernet.com', 'Ticket'); $mail->addAddress($staffEmail); $mail->Subject = $subjectLine; $mail->CharSet = \PHPMailer\PHPMailer\PHPMailer::CHARSET_UTF8; $mail->isHTML(true); $mail->Body = $body; $mail->send(); $notified = true; } catch (\Throwable $e) { $notifyErr = $e->getMessage(); } } } out(['ok' => true, 'action' => 'reassign', 'affected' => $aff, 'staff' => $staff, 'assistants' => $assistIds, 'notified' => $notified, 'notify_to' => $notifyTo, 'notify_error' => $notifyErr]);