PDO::ERRMODE_EXCEPTION, ]); } catch (PDOException $e) { log_msg('ERROR: Cannot connect to PrintStream: ' . $e->getMessage()); exit(1); } log_msg('Connected to PrintStream (' . $psCfg['host'] . '/' . $psCfg['database'] . ')'); // ── PrintStream queries ─────────────────────────────────────────────────────── // pdo_odbc uses SQLPrepare/SQLExecute for every call — SQLPrepare maps to // sp_prepare in SQL Server which REJECTS CTEs (error 8180). ATTR_EMULATE_PREPARES // is ignored by pdo_odbc. Solution: flatten to plain JOINs (no CTEs) and embed // the token value directly after escaping single quotes (admin-controlled data). // Main job query — flat JOINs, no CTEs. // Token appended as: WHERE e2.[DETAILS1] LIKE '%escaped_token%' $psSqlBase = " SELECT DISTINCT s.[JOB NUMBER] AS JobNumber, d.[NAMES] AS CustomerName, e2.[DETAILS1] AS JobName, e2.[FINAL DELIVERY] AS FinalDelivery, e2.[ORIG_Qty1] AS Quantity, CONCAT(ISNULL(e2.[DETAILS2],''),ISNULL(e2.[DETAILS3],''),ISNULL(e2.[DETAILS4],''), ISNULL(e2.[DETAILS5],''),ISNULL(e2.[DETAILS6],''),ISNULL(e2.[DETAILS7],''), ISNULL(e2.[DETAILS8],''),ISNULL(e2.[DETAILS9],''),ISNULL(e2.[DETAILS10],'')) AS Notes, e2.[ORIG_QuoteNo] AS QuoteNo FROM dbo.SCHEDFIL AS s JOIN dbo.ESTIMATE AS e ON e.[DATAFLEX RECNUM ONE] = s.[ESTIMATE RECNUM] JOIN dbo.CostCenters AS cc ON cc.[Code] = s.[COST CENTRE] LEFT JOIN dbo.DEBTOR AS d ON e.[DEBTOR] = d.[AC NO] LEFT JOIN dbo.ESTIMATE AS e2 ON s.[JOB NUMBER] = e2.[JOB NUMBER] WHERE ISNULL(s.[COMPLETED],'') <> 'Y' AND ISNULL(s.[CLOSED OUT],'') <> 'Y' AND ISNULL(e.[CLOSED OUT],'') <> 'Y' AND e.[DELIVERY DATE] > DATEADD(DAY, -365, CAST(GETDATE() AS date)) AND s.[FILETYPE] IN ('D','Q','X','I','W','R','T','P') AND (s.[STATUS] IN ('*','6','7','0','1','2','3','4','5',' ') OR s.[STATUS] IS NULL) AND e2.[DETAILS1] LIKE "; // Notes query — fetches all MDP notes for a list of QuoteNo values in one round-trip. // PHP concatenates lines per QuoteNo. Token placeholder: __QUOTENOS__ $psNotesSqlBase = " SELECT n.[RELATED TO] AS QuoteNo, ISNULL(n.[NOTE],'') AS NoteLine FROM dbo.NOTES AS n WHERE n.[MODULE] = 'MDP' AND n.[RELATED TO] IN (__QUOTENOS__) ORDER BY n.[RELATED TO], TRY_CAST(n.[LINE NO] AS INT) "; // ── Process boards ──────────────────────────────────────────────────────────── $boards = $kanban->query( 'SELECT id, printstream_job_name FROM boards WHERE import_from_printstream = 1' ); if (empty($boards)) { log_msg('No boards with import_from_printstream enabled. Nothing to do.'); exit(0); } $totalBoards = 0; $totalCreated = 0; $totalUpdated = 0; foreach ($boards as $boardRow) { $boardId = (int) $boardRow['id']; $filterMemo = safe_str($boardRow['printstream_job_name']); $totalBoards++; log_msg("Board ID {$boardId}:"); try { $firstCol = $kanban->first( 'SELECT id FROM board_columns WHERE board_id = :b ORDER BY position ASC LIMIT 1', ['b' => $boardId] ); $firstLane = $kanban->first( 'SELECT id FROM swim_lanes WHERE board_id = :b ORDER BY position ASC LIMIT 1', ['b' => $boardId] ); } catch (\Throwable $e) { log_msg(' ERROR querying kanban DB: ' . $e->getMessage()); continue; } if ($firstCol === null || $firstLane === null) { log_msg(' No columns or swim lanes — skipping.'); continue; } $firstColId = (int) $firstCol['id']; $firstLaneId = (int) $firstLane['id']; // Parse filter tokens (one per line, case-insensitive, deduplicated) $tokens = []; foreach (preg_split('/\r?\n/', $filterMemo) as $line) { $tok = trim($line); if ($tok !== '') { $tokens[strtolower($tok)] = $tok; } } if (empty($tokens)) { log_msg(' No job name filter configured — skipping.'); continue; } // Query PrintStream for each token, collect unique jobs $jobs = []; // keyed by strtolower(jobNumber) foreach ($tokens as $tok) { try { $escaped = str_replace("'", "''", $tok); $sql = $psSqlBase . "'%" . $escaped . "%'"; $stmt = $ps->query($sql); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); } catch (\Throwable $e) { log_msg(" WARNING: PrintStream query failed for '{$tok}': " . get_class($e) . ': ' . $e->getMessage()); continue; } foreach ($rows as $row) { $jn = safe_str($row['JobNumber'] ?? ''); if ($jn === '') { continue; } $key = strtolower($jn); if (!isset($jobs[$key])) { $rawDate = $row['FinalDelivery'] ?? null; $deliveryDate = null; if ($rawDate !== null && $rawDate !== '') { $parsed = date_create((string) $rawDate); $deliveryDate = $parsed ? $parsed->format('Y-m-d') : null; } $jobs[$key] = [ 'job_number' => $jn, 'customer_name' => safe_str($row['CustomerName'] ?? ''), 'job_name' => safe_str($row['JobName'] ?? ''), 'delivery_date' => $deliveryDate, 'quantity' => safe_str($row['Quantity'] ?? ''), 'notes' => safe_str($row['Notes'] ?? ''), 'full_note' => '', '_quote_no' => safe_str($row['QuoteNo'] ?? ''), ]; } } } log_msg(' Found ' . count($jobs) . ' unique open job(s) matching filters.'); // Fetch FullNote for all jobs in one batch query, concatenate lines in PHP if (!empty($jobs)) { $quoteNos = array_filter(array_column(array_values($jobs), '_quote_no')); if (!empty($quoteNos)) { try { $inList = implode(',', array_map(fn($q) => "'" . str_replace("'", "''", $q) . "'", $quoteNos)); $notesSql = str_replace('__QUOTENOS__', $inList, $psNotesSqlBase); $notesStmt = $ps->query($notesSql); $noteRows = $notesStmt->fetchAll(PDO::FETCH_ASSOC); // Group note lines by QuoteNo and concatenate $notesByQuote = []; foreach ($noteRows as $nr) { $qn = (string) ($nr['QuoteNo'] ?? ''); $notesByQuote[$qn] = ($notesByQuote[$qn] ?? '') . (string) ($nr['NoteLine'] ?? ''); } // Map FullNote back to jobs via _quote_no foreach ($jobs as &$job) { $qn = $job['_quote_no']; if ($qn !== '' && isset($notesByQuote[$qn])) { $job['full_note'] = $notesByQuote[$qn]; } } unset($job); } catch (\Throwable $e) { log_msg(' WARNING: FullNote query failed (notes will be empty): ' . $e->getMessage()); } } } $now = date('Y-m-d H:i:s'); foreach ($jobs as $job) { unset($job['_quote_no']); $existing = $kanban->first( 'SELECT COUNT(*) AS cnt FROM cards WHERE board_id = :b AND job_number = :jn', ['b' => $boardId, 'jn' => $job['job_number']] ); $alreadyExists = (int) ($existing['cnt'] ?? 0) > 0; if (!$alreadyExists) { // Determine next position in target cell $maxPos = $kanban->first( 'SELECT MAX(position) AS m FROM cards WHERE column_id = :c AND swim_lane_id = :l', ['c' => $firstColId, 'l' => $firstLaneId] ); $nextPos = ((int) ($maxPos['m'] ?? -1)) + 1; $kanban->execute( 'INSERT INTO cards (board_id, column_id, swim_lane_id, job_number, job_name, customer_name, delivery_date, quantity, notes, full_note, position, created_at, created_by, updated_at, updated_by) VALUES (:board_id, :column_id, :swim_lane_id, :job_number, :job_name, :customer_name, :delivery_date, :quantity, :notes, :full_note, :position, :created_at, :created_by, :updated_at, :updated_by)', [ 'board_id' => $boardId, 'column_id' => $firstColId, 'swim_lane_id' => $firstLaneId, 'job_number' => $job['job_number'], 'job_name' => $job['job_name'], 'customer_name' => $job['customer_name'], 'delivery_date' => $job['delivery_date'], 'quantity' => $job['quantity'] !== '' ? $job['quantity'] : null, 'notes' => $job['notes'], 'full_note' => $job['full_note'], 'position' => $nextPos, 'created_at' => $now, 'created_by' => 'printstream-import', 'updated_at' => $now, 'updated_by' => 'printstream-import', ] ); log_msg(' + ' . $job['job_number'] . ' — ' . $job['job_name']); $totalCreated++; } else { // Refresh PrintStream fields without touching column/lane/position $kanban->execute( 'UPDATE cards SET job_name = :job_name, customer_name = :customer_name, delivery_date = :delivery_date, quantity = :quantity, notes = :notes, full_note = :full_note, updated_at = :updated_at, updated_by = :updated_by WHERE board_id = :board_id AND job_number = :job_number', [ 'job_name' => $job['job_name'], 'customer_name' => $job['customer_name'], 'delivery_date' => $job['delivery_date'], 'quantity' => $job['quantity'] !== '' ? $job['quantity'] : null, 'notes' => $job['notes'], 'full_note' => $job['full_note'], 'updated_at' => $now, 'updated_by' => 'printstream-import', 'board_id' => $boardId, 'job_number' => $job['job_number'], ] ); $totalUpdated++; } } } $ps = null; log_msg(''); log_msg('Import complete.'); log_msg(" Boards processed : {$totalBoards}"); log_msg(" Cards created : {$totalCreated}"); log_msg(" Cards updated : {$totalUpdated}");