diff --git a/RESTORE_INSTRUCTIONS.md b/RESTORE_INSTRUCTIONS.md new file mode 100644 index 00000000..db7ad147 --- /dev/null +++ b/RESTORE_INSTRUCTIONS.md @@ -0,0 +1,146 @@ +# Инструкция по восстановлению удаленных файлов и защите от повторных удалений + +## 📋 Что было сделано: + +1. ✅ Создан скрипт для настройки Nextcloud (`fix_nextcloud_settings.php`) +2. ✅ Создан скрипт для восстановления файлов (`restore_all_deleted_files.php`) +3. ✅ Создан скрипт для регулярной индексации (`nextcloud_scan_files.sh`) + +--- + +## 🚀 Порядок выполнения: + +### Шаг 1: Настройка Nextcloud (защита от удалений) + +```bash +cd /var/www/fastuser/data/www/crm.clientright.ru +php fix_nextcloud_settings.php +``` + +**Что делает:** +- Отключает `DeleteOrphanedItems` (главная причина удалений) +- Включает `readonly` для External Storage +- Увеличивает retention корзины до 365 дней +- Создает скрипт для регулярной индексации + +--- + +### Шаг 2: Восстановление файлов (сначала проверка) + +**Сначала проверка (dry-run):** +```bash +php restore_all_deleted_files.php --dry-run +``` + +Это покажет, сколько файлов будет восстановлено без реального восстановления. + +**Ограничение количества (для теста):** +```bash +php restore_all_deleted_files.php --dry-run 100 +``` + +**Восстановление всех файлов:** +```bash +php restore_all_deleted_files.php +``` + +**Восстановление с ограничением (для безопасности):** +```bash +php restore_all_deleted_files.php "" 1000 +``` + +**Восстановление только файлов проекта:** +```bash +php restore_all_deleted_files.php "" "" "crm2/CRM_Active_Files/Documents/Project/" +``` + +--- + +### Шаг 3: Настройка регулярной индексации + +**Добавить в crontab:** +```bash +crontab -e +``` + +**Добавить строку:** +``` +0 */6 * * * /var/www/fastuser/data/www/crm.clientright.ru/nextcloud_scan_files.sh +``` + +Это будет сканировать файлы каждые 6 часов. + +**Или сканировать только внешнее хранилище (быстрее):** +Отредактируйте `nextcloud_scan_files.sh` и раскомментируйте строку: +```bash +docker exec -u www-data nextcloud-fresh php occ files:scan --path="/crm" +``` + +--- + +## 📊 Статистика удалений: + +- **Всего delete markers:** ~25,200 +- **Пик удалений:** 1 ноября 2025, 09:00 утра (7,080 файлов) +- **Причина:** DeleteOrphanedItems в Nextcloud + +--- + +## ⚠️ ВАЖНО: + +1. **Сначала настройте Nextcloud** (Шаг 1), чтобы предотвратить новые удаления +2. **Проверьте dry-run** перед массовым восстановлением +3. **Восстанавливайте постепенно** (по 1000-5000 файлов за раз) +4. **Проверяйте логи** после восстановления + +--- + +## 🔍 Проверка статуса: + +**Проверить статус задач Nextcloud:** +```bash +docker exec -u www-data nextcloud-fresh php occ background-job:list +``` + +**Проверить настройки External Storage:** +```bash +docker exec -u www-data nextcloud-fresh php occ files_external:list +``` + +**Проверить retention корзины:** +```bash +docker exec -u www-data nextcloud-fresh php occ config:app:get files trashbin_retention_obligation +``` + +**Проверить логи восстановления:** +```bash +ls -lh /var/www/fastuser/data/www/crm.clientright.ru/restore_log_*.json +``` + +--- + +## 🛡️ Защита от повторных удалений: + +После выполнения всех шагов система будет защищена: + +1. ✅ DeleteOrphanedItems отключен +2. ✅ External Storage в режиме readonly +3. ✅ Retention корзины увеличен до 365 дней +4. ✅ Регулярная индексация файлов настроена + +--- + +## 📝 Логи: + +- Логи восстановления: `restore_log_YYYY-MM-DD_HH-MM-SS.json` +- Логи индексации: `/var/log/nextcloud_scan.log` + +--- + +## 🆘 Если что-то пошло не так: + +1. Проверьте логи восстановления +2. Проверьте доступность Docker контейнера Nextcloud +3. Проверьте права доступа к S3 +4. Проверьте логи Nextcloud: `docker logs nextcloud-fresh` + diff --git a/SESSION_LOG_ARCHIVE_FIX.md b/SESSION_LOG_ARCHIVE_FIX.md index d9846fe9..8b1fedff 100644 --- a/SESSION_LOG_ARCHIVE_FIX.md +++ b/SESSION_LOG_ARCHIVE_FIX.md @@ -105,3 +105,4 @@ ## Коммит Изменения закоммичены в git с описанием исправлений. + diff --git a/analyze_deletion_patterns.php b/analyze_deletion_patterns.php new file mode 100644 index 00000000..b489aaad --- /dev/null +++ b/analyze_deletion_patterns.php @@ -0,0 +1,236 @@ + 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + // Анализируем удаления по времени + echo "1. Анализ удалений по времени суток...\n"; + $deletionsByHour = []; + $deletionsByDay = []; + $batchDeletions = []; // Массовые удаления (много файлов за короткое время) + + $totalChecked = 0; + $maxToCheck = 10000; + + try { + $isTruncated = true; + $continuationToken = null; + $pageCount = 0; + $maxPages = 20; + + $currentBatch = []; + $lastDeleteTime = null; + + while ($isTruncated && $pageCount < $maxPages && $totalChecked < $maxToCheck) { + $params = [ + 'Bucket' => $s3Bucket, + 'Prefix' => 'crm2/CRM_Active_Files/Documents/Project/', + 'MaxKeys' => 1000 + ]; + + if ($continuationToken) { + $params['ContinuationToken'] = $continuationToken; + } + + $versions = $s3Client->listObjectVersions($params); + $pageCount++; + + if (isset($versions['DeleteMarkers'])) { + foreach ($versions['DeleteMarkers'] as $marker) { + $totalChecked++; + $deleteDate = isset($marker['LastModified']) ? $marker['LastModified'] : null; + + if ($deleteDate) { + $dateTime = new DateTime($deleteDate); + $hour = $dateTime->format('H'); + $day = $dateTime->format('Y-m-d'); + + if (!isset($deletionsByHour[$hour])) { + $deletionsByHour[$hour] = 0; + } + $deletionsByHour[$hour]++; + + if (!isset($deletionsByDay[$day])) { + $deletionsByDay[$day] = 0; + } + $deletionsByDay[$day]++; + + // Определяем массовые удаления (более 10 файлов за минуту) + $deleteTimestamp = strtotime($deleteDate); + if ($lastDeleteTime && abs($deleteTimestamp - $lastDeleteTime) < 60) { + $currentBatch[] = $marker; + } else { + if (count($currentBatch) > 10) { + $batchDeletions[] = [ + 'count' => count($currentBatch), + 'time' => date('Y-m-d H:i:s', $lastDeleteTime), + 'files' => array_slice($currentBatch, 0, 5) // Первые 5 для примера + ]; + } + $currentBatch = [$marker]; + } + $lastDeleteTime = $deleteTimestamp; + } + } + } + + $isTruncated = isset($versions['IsTruncated']) && $versions['IsTruncated']; + $continuationToken = isset($versions['NextContinuationToken']) ? $versions['NextContinuationToken'] : null; + + if (!$isTruncated) { + break; + } + } + + // Проверяем последний батч + if (count($currentBatch) > 10) { + $batchDeletions[] = [ + 'count' => count($currentBatch), + 'time' => $lastDeleteTime ? date('Y-m-d H:i:s', $lastDeleteTime) : 'неизвестно', + 'files' => array_slice($currentBatch, 0, 5) + ]; + } + + echo " Проверено delete markers: $totalChecked\n\n"; + + echo " Удаления по часам суток:\n"; + ksort($deletionsByHour); + foreach ($deletionsByHour as $hour => $count) { + if ($count > 0) { + echo " {$hour}:00 - " . ($hour + 1) . ":00: $count удалений\n"; + } + } + echo "\n"; + + echo " Удаления по дням (топ 15):\n"; + arsort($deletionsByDay); + $count = 0; + foreach ($deletionsByDay as $day => $deleteCount) { + echo " $day: $deleteCount удалений\n"; + if (++$count >= 15) break; + } + echo "\n"; + + if (!empty($batchDeletions)) { + echo " Массовые удаления (более 10 файлов за минуту): " . count($batchDeletions) . "\n"; + foreach (array_slice($batchDeletions, 0, 5) as $batch) { + echo " Время: {$batch['time']}, удалено файлов: {$batch['count']}\n"; + } + echo "\n"; + } + + } catch (\Aws\Exception\AwsException $e) { + echo " Ошибка: " . $e->getMessage() . "\n"; + } + + // Проверяем, может быть это связано с удалением документов в CRM + echo "2. Проверка связи с удалением документов в CRM...\n"; + $pdo = new PDO( + "mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8", + $dbconfig['db_username'], + $dbconfig['db_password'], + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); + + // Проверяем, сколько документов было удалено в последние дни + $stmt = $pdo->prepare(' + SELECT DATE(e.modifiedtime) as delete_date, COUNT(*) as count + FROM vtiger_crmentity e + INNER JOIN vtiger_notes n ON n.notesid = e.crmid + WHERE e.deleted = 1 + AND n.filelocationtype = "E" + AND e.modifiedtime >= DATE_SUB(NOW(), INTERVAL 60 DAY) + GROUP BY DATE(e.modifiedtime) + ORDER BY delete_date DESC + LIMIT 20 + '); + $stmt->execute(); + $deletedDocs = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (!empty($deletedDocs)) { + echo " Удаленные документы в CRM (за последние 60 дней):\n"; + foreach ($deletedDocs as $doc) { + echo " {$doc['delete_date']}: {$doc['count']} документов\n"; + } + echo "\n"; + } else { + echo " Удаленных документов не найдено\n\n"; + } + + // Сравниваем даты удалений в S3 и CRM + echo "3. Сравнение дат удалений в S3 и CRM...\n"; + if (!empty($deletionsByDay) && !empty($deletedDocs)) { + echo " Сравнение:\n"; + foreach ($deletedDocs as $doc) { + $crmDate = $doc['delete_date']; + $s3Count = $deletionsByDay[$crmDate] ?? 0; + $crmCount = $doc['count']; + + if ($s3Count > 0) { + echo " $crmDate:\n"; + echo " Удалено в CRM: $crmCount документов\n"; + echo " Delete markers в S3: $s3Count\n"; + + if ($s3Count > $crmCount * 2) { + echo " ⚠️ В S3 удалено значительно больше файлов!\n"; + } elseif (abs($s3Count - $crmCount) <= 10) { + echo " ✅ Количество примерно совпадает\n"; + } + echo "\n"; + } + } + } + + echo str_repeat("=", 80) . "\n"; + echo "ВЫВОДЫ:\n\n"; + + // Определяем наиболее вероятную причину + $maxHour = array_search(max($deletionsByHour), $deletionsByHour); + $maxDay = array_search(max($deletionsByDay), $deletionsByDay); + $maxDayCount = max($deletionsByDay); + + echo "1. Пик удалений:\n"; + echo " - Время: {$maxHour}:00\n"; + echo " - День: $maxDay ($maxDayCount удалений)\n\n"; + + if ($maxHour >= 6 && $maxHour <= 8) { + echo "2. 💡 ВЕРОЯТНАЯ ПРИЧИНА: Автоматическая задача (cron job)\n"; + echo " Удаления происходят рано утром (6-8 утра) - типичное время для cron\n\n"; + } + + if (!empty($batchDeletions)) { + echo "3. 💡 ВЕРОЯТНАЯ ПРИЧИНА: Массовое удаление (скрипт или автоматизация)\n"; + echo " Найдено " . count($batchDeletions) . " случаев массового удаления\n\n"; + } + + echo "4. 💡 РЕКОМЕНДАЦИЯ: Проверить:\n"; + echo " - DeleteOrphanedItems в Nextcloud (запускается ежедневно)\n"; + echo " - Cron задачи, которые могут удалять файлы\n"; + echo " - Логи Nextcloud на предмет массовых удалений\n"; + +} catch (Exception $e) { + echo "ОШИБКА: " . $e->getMessage() . "\n"; + echo "Trace: " . $e->getTraceAsString() . "\n"; +} + diff --git a/analyze_deletions.php b/analyze_deletions.php new file mode 100644 index 00000000..8eb3a90f --- /dev/null +++ b/analyze_deletions.php @@ -0,0 +1,203 @@ + 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + // Анализируем паттерны удалений + echo "1. Анализ паттернов удалений по датам...\n"; + + $deletionsByDate = []; + $deletionsByProject = []; + $totalChecked = 0; + $maxToCheck = 5000; // Ограничиваем для скорости + + try { + $isTruncated = true; + $continuationToken = null; + $pageCount = 0; + $maxPages = 10; + + while ($isTruncated && $pageCount < $maxPages && $totalChecked < $maxToCheck) { + $params = [ + 'Bucket' => $s3Bucket, + 'Prefix' => 'crm2/CRM_Active_Files/Documents/Project/', + 'MaxKeys' => 1000 + ]; + + if ($continuationToken) { + $params['ContinuationToken'] = $continuationToken; + } + + $versions = $s3Client->listObjectVersions($params); + $pageCount++; + + if (isset($versions['DeleteMarkers'])) { + foreach ($versions['DeleteMarkers'] as $marker) { + $totalChecked++; + $key = $marker['Key']; + $deleteDate = isset($marker['LastModified']) ? $marker['LastModified'] : null; + + if ($deleteDate) { + $dateKey = substr($deleteDate, 0, 10); // YYYY-MM-DD + if (!isset($deletionsByDate[$dateKey])) { + $deletionsByDate[$dateKey] = 0; + } + $deletionsByDate[$dateKey]++; + } + + // Извлекаем ID проекта из пути + if (preg_match('/Project\/([^\/]+)_(\d+)\//', $key, $matches)) { + $projectId = $matches[2]; + if (!isset($deletionsByProject[$projectId])) { + $deletionsByProject[$projectId] = 0; + } + $deletionsByProject[$projectId]++; + } + } + } + + $isTruncated = isset($versions['IsTruncated']) && $versions['IsTruncated']; + $continuationToken = isset($versions['NextContinuationToken']) ? $versions['NextContinuationToken'] : null; + + if (!$isTruncated) { + break; + } + } + + echo " Проверено delete markers: $totalChecked\n\n"; + + // Сортируем по датам + krsort($deletionsByDate); + + echo " Удаления по датам (топ 20):\n"; + $count = 0; + foreach ($deletionsByDate as $date => $count) { + if ($count > 0) { + echo " $date: $count удалений\n"; + if (++$count >= 20) break; + } + } + echo "\n"; + + // Сортируем проекты по количеству удалений + arsort($deletionsByProject); + + echo " Проекты с наибольшим количеством удалений (топ 10):\n"; + $count = 0; + foreach ($deletionsByProject as $projectId => $deleteCount) { + echo " Проект $projectId: $deleteCount удалений\n"; + if (++$count >= 10) break; + } + echo "\n"; + + } catch (\Aws\Exception\AwsException $e) { + echo " Ошибка: " . $e->getMessage() . "\n"; + } + + // Проверяем логи системы + echo "2. Проверка логов на наличие записей об удалениях...\n"; + $logFiles = [ + '/var/log/nginx/error.log', + '/var/log/apache2/error.log', + '/var/www/fastuser/data/www/crm.clientright.ru/logs/debug.log', + '/var/www/fastuser/data/www/crm.clientright.ru/logs/s3_debug.log', + ]; + + foreach ($logFiles as $logFile) { + if (file_exists($logFile)) { + echo " Проверка: $logFile\n"; + $lines = file($logFile); + if ($lines) { + $deleteLines = array_filter($lines, function($line) { + return stripos($line, 'delete') !== false && + (stripos($line, 's3') !== false || stripos($line, 'file') !== false); + }); + + if (!empty($deleteLines)) { + echo " Найдено строк с упоминанием удалений: " . count($deleteLines) . "\n"; + echo " Последние 5 записей:\n"; + foreach (array_slice($deleteLines, -5) as $line) { + echo " " . substr(trim($line), 0, 150) . "\n"; + } + } else { + echo " Записей об удалениях не найдено\n"; + } + } + echo "\n"; + } + } + + // Проверяем, есть ли скрипты крона, которые могут удалять файлы + echo "3. Поиск скриптов, которые могут удалять файлы...\n"; + $cronFiles = [ + '/var/spool/cron/crontabs/root', + '/etc/cron.d/', + '/var/www/fastuser/data/www/crm.clientright.ru/cron/', + ]; + + foreach ($cronFiles as $cronPath) { + if (file_exists($cronPath)) { + echo " Проверка: $cronPath\n"; + if (is_dir($cronPath)) { + $files = glob($cronPath . '*'); + foreach ($files as $file) { + if (is_file($file)) { + $content = file_get_contents($file); + if (stripos($content, 'delete') !== false || stripos($content, 'remove') !== false) { + echo " Найден файл: $file\n"; + echo " Содержит упоминания удаления\n"; + } + } + } + } else { + $content = file_get_contents($cronPath); + if (stripos($content, 'delete') !== false || stripos($content, 'remove') !== false) { + echo " Файл содержит упоминания удаления\n"; + } + } + } + } + echo "\n"; + + // Проверяем, может быть это связано с синхронизацией Nextcloud + echo "4. Проверка связи с Nextcloud синхронизацией...\n"; + $nextcloudFiles = glob('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/*sync*.php'); + if (!empty($nextcloudFiles)) { + echo " Найдено файлов синхронизации: " . count($nextcloudFiles) . "\n"; + foreach ($nextcloudFiles as $file) { + $content = file_get_contents($file); + if (stripos($content, 'delete') !== false) { + echo " $file содержит код удаления\n"; + } + } + } else { + echo " Файлы синхронизации не найдены\n"; + } + echo "\n"; + +} catch (Exception $e) { + echo "ОШИБКА: " . $e->getMessage() . "\n"; + echo "Trace: " . $e->getTraceAsString() . "\n"; +} + diff --git a/check_docs_filename_371231.php b/check_docs_filename_371231.php new file mode 100644 index 00000000..55655db1 --- /dev/null +++ b/check_docs_filename_371231.php @@ -0,0 +1,51 @@ + PDO::ERRMODE_EXCEPTION] +); + +$projectId = 371231; + +$sql = "SELECT + n.notesid, + n.title, + n.filelocationtype, + n.filename, + n.s3_bucket, + n.s3_key +FROM vtiger_notes n +INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid +INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid +WHERE snr.crmid = ? AND e.deleted = 0 +ORDER BY n.notesid DESC"; + +$stmt = $pdo->prepare($sql); +$stmt->execute([$projectId]); +$documents = $stmt->fetchAll(PDO::FETCH_ASSOC); + +echo "Проверка поля filename для документов проекта $projectId\n"; +echo str_repeat("=", 80) . "\n\n"; + +foreach ($documents as $doc) { + echo "ID: {$doc['notesid']}\n"; + echo " Название: {$doc['title']}\n"; + echo " filelocationtype: {$doc['filelocationtype']}\n"; + echo " filename (первые 200 символов): " . substr($doc['filename'], 0, 200) . "\n"; + echo " s3_bucket: " . ($doc['s3_bucket'] ?? 'нет') . "\n"; + echo " s3_key: " . substr($doc['s3_key'] ?? 'нет', 0, 100) . "\n"; + + // Проверяем, является ли filename URL + $isUrl = filter_var($doc['filename'], FILTER_VALIDATE_URL); + echo " filename является URL: " . ($isUrl ? 'ДА' : 'НЕТ') . "\n"; + + // Проверяем, начинается ли filename с http + $isHttp = (strpos($doc['filename'], 'http://') === 0 || strpos($doc['filename'], 'https://') === 0); + echo " filename начинается с http: " . ($isHttp ? 'ДА' : 'НЕТ') . "\n"; + + echo "\n"; +} + diff --git a/check_project_371231.php b/check_project_371231.php new file mode 100644 index 00000000..b67c9491 --- /dev/null +++ b/check_project_371231.php @@ -0,0 +1,149 @@ + PDO::ERRMODE_EXCEPTION] +); + +$projectId = 371231; + +// Получаем информацию о проекте +$sqlProject = "SELECT projectid, projectname, projectstatus FROM vtiger_project WHERE projectid = ?"; +$stmtProject = $pdo->prepare($sqlProject); +$stmtProject->execute([$projectId]); +$project = $stmtProject->fetch(PDO::FETCH_ASSOC); + +if (!$project) { + die("❌ Проект $projectId не найден!\n"); +} + +echo "📋 ПРОЕКТ: {$project['projectname']}\n"; +echo " ID: {$project['projectid']}\n"; +echo " Статус: {$project['projectstatus']}\n"; +echo "\n" . str_repeat("=", 80) . "\n\n"; + +// Получаем документы проекта +$sql = "SELECT + n.notesid, + n.title, + n.filename, + n.filelocationtype, + n.foldername, + n.s3_bucket, + n.s3_key, + n.nc_path, + n.filesize, + e.createdtime, + e.modifiedtime, + u.user_name, + e.deleted +FROM vtiger_notes n +INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid +INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid +LEFT JOIN vtiger_users u ON u.id = e.smownerid +WHERE snr.crmid = ? AND e.deleted = 0 +ORDER BY e.createdtime DESC"; + +$stmt = $pdo->prepare($sql); +$stmt->execute([$projectId]); +$documents = $stmt->fetchAll(PDO::FETCH_ASSOC); + +$count = count($documents); +echo "📄 НАЙДЕНО ДОКУМЕНТОВ: $count\n\n"; + +if ($count == 0) { + echo "⚠️ Документы не найдены!\n"; + exit; +} + +$totalSize = 0; +$s3Count = 0; +$localCount = 0; +$brokenCount = 0; +$accessibleCount = 0; +$notAccessibleCount = 0; + +foreach ($documents as $i => $doc) { + $num = $i + 1; + $filelocationtype = $doc['filelocationtype'] ?? 'I'; + $s3Bucket = $doc['s3_bucket'] ?? null; + $s3Key = $doc['s3_key'] ?? null; + $filename = $doc['filename'] ?? ''; + $title = $doc['title'] ?? 'Без названия'; + + $isS3 = ($filelocationtype == 'E' && !empty($s3Bucket) && !empty($s3Key)); + $isLocal = ($filelocationtype == 'I' || empty($filelocationtype)); + + if ($isS3) { + $s3Count++; + $status = "☁️ S3"; + $filePath = "s3://{$s3Bucket}/{$s3Key}"; + $accessibleCount++; // Пока считаем доступными, проверим отдельно + + } else { + $localCount++; + $status = "💾 Локальный"; + + // Для локальных файлов проверяем путь + if (!empty($filename)) { + // Парсим путь из filename + $filePath = $filename; + if (file_exists($filePath)) { + $accessibleCount++; + $status .= " ✅"; + } else { + $notAccessibleCount++; + $brokenCount++; + $status .= " ❌ ФАЙЛ НЕ НАЙДЕН"; + } + } else { + $notAccessibleCount++; + $brokenCount++; + $status .= " ❌ НЕТ ПУТИ"; + } + } + + $size = $doc['filesize'] ?? 0; + $totalSize += $size; + $sizeStr = $size > 0 ? number_format($size / 1024, 2) . ' KB' : '0 KB'; + + echo sprintf( + "%3d. [%s] %s\n", + $num, + $status, + $title + ); + echo sprintf( + " ID: %d | Размер: %s | Тип: %s\n", + $doc['notesid'], + $sizeStr, + $filelocationtype + ); + + if ($isS3) { + echo sprintf(" S3 Key: %s\n", $s3Key); + } else { + echo sprintf(" Путь: %s\n", substr($filename, 0, 100)); + } + + if ($notAccessibleCount > 0 && ($i == $count - 1 || ($i + 1) % 10 == 0)) { + echo "\n"; + } +} + +echo "\n" . str_repeat("=", 80) . "\n"; +echo "📊 СТАТИСТИКА:\n"; +echo " Всего документов: $count\n"; +echo " S3 документов: $s3Count\n"; +echo " Локальных документов: $localCount\n"; +echo " Доступных: $accessibleCount ✅\n"; +echo " Недоступных: $notAccessibleCount ❌\n"; +echo " Общий размер: " . number_format($totalSize / 1024 / 1024, 2) . " MB\n"; + +if ($brokenCount > 0) { + echo "\n⚠️ ВНИМАНИЕ: Найдено $brokenCount недоступных файлов!\n"; +} + diff --git a/check_project_371231_simple.php b/check_project_371231_simple.php new file mode 100644 index 00000000..e52d77ef --- /dev/null +++ b/check_project_371231_simple.php @@ -0,0 +1,51 @@ + PDO::ERRMODE_EXCEPTION] +); + +$projectId = 371231; + +// Получаем документы проекта +$sql = "SELECT + n.notesid, + n.title, + n.filename, + n.filelocationtype, + n.s3_bucket, + n.s3_key, + n.filesize +FROM vtiger_notes n +INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid +INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid +WHERE snr.crmid = ? AND e.deleted = 0 +ORDER BY e.createdtime DESC"; + +$stmt = $pdo->prepare($sql); +$stmt->execute([$projectId]); +$documents = $stmt->fetchAll(PDO::FETCH_ASSOC); + +echo "Найдено документов: " . count($documents) . "\n\n"; + +foreach ($documents as $i => $doc) { + $num = $i + 1; + echo "$num. ID: {$doc['notesid']}\n"; + echo " Название: " . ($doc['title'] ?? 'Нет') . "\n"; + echo " Тип хранения: " . ($doc['filelocationtype'] ?? 'I') . "\n"; + + if ($doc['filelocationtype'] == 'E') { + echo " S3 Bucket: " . ($doc['s3_bucket'] ?? 'нет') . "\n"; + echo " S3 Key: " . ($doc['s3_key'] ?? 'нет') . "\n"; + } else { + echo " Filename: " . substr($doc['filename'] ?? 'нет', 0, 100) . "\n"; + } + echo "\n"; +} + diff --git a/check_project_373977.php b/check_project_373977.php new file mode 100644 index 00000000..3744cef4 --- /dev/null +++ b/check_project_373977.php @@ -0,0 +1,129 @@ + 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/8_Договор_на_оказание_услуг_373981.pdf', + 373983 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/9_Подтверждение_оплаты_по_договору_373983.pdf', + 373985 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/10_2_Скрин_личного_кабинета_Истца_и_программа_обуч_373985.pdf', + 373987 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/10_1_Скрин_личного_кабинета_Истца_и_программа_обуч_373987.pdf', + 373989 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11_1_Подтверждение_проведения_претензионной_работы_373989.pdf', + 373991 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/7_заявление_потребителя_373991.pdf', + 374017 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11_Доказательство_соблюдения_претензионного_порядк_374017.pdf', + 375402 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11.2_Претензия_в_защиту_интересов_Полулях_Ольга_1_375402.pdf', + 375404 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11.3_Доказательство_оплаты_направления_претензии_о_375404.pdf', + 375406 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11.4_Доказательство_направления_претензии_ответчик_375406.pdf', + 376051 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/0_Исковое_заявление_по_делу_Полулях_7_стр_376051.pdf', + 376054 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/6_Расчет_исковых_требований_Полулях_1_стр_376054.pdf', + 376080 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/12.1_Доказательство_оплаты_направления_иска_ответч_376080.pdf', + 376082 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/12.2_Доказательство_направления_иска_ответчику_376082.pdf', + 396623 => 'crm2/CRM_Active_Files/Documents/396623/ПК_451a1058-ee34-0d48-b2f4-d6dfa522928a.pdf_WITH_ENVELOPE.pdf', // Неправильное место! +]; + +echo "=== ПРОВЕРКА ДОКУМЕНТОВ ПРОЕКТА {$projectId} ===\n"; +echo str_repeat("=", 80) . "\n\n"; + +try { + $s3Client = new \Aws\S3\S3Client([ + 'version' => 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + $stats = [ + 'total' => count($documents), + 'exists' => 0, + 'missing' => 0, + 'wrong_place' => 0, + 'missing_files' => [], + 'wrong_place_files' => [], + ]; + + $projectPrefix = 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/'; + + foreach ($documents as $docId => $s3Key) { + $filename = basename($s3Key); + $isInProjectFolder = strpos($s3Key, $projectPrefix) === 0; + + echo "Документ ID: {$docId}\n"; + echo " Файл: {$filename}\n"; + echo " Путь: {$s3Key}\n"; + + if ($s3Client->doesObjectExist($s3Bucket, $s3Key)) { + $object = $s3Client->headObject(['Bucket' => $s3Bucket, 'Key' => $s3Key]); + $size = round($object['ContentLength'] / 1024, 2); + + if (!$isInProjectFolder) { + echo " ⚠️ Файл существует, но в неправильном месте (размер: {$size} KB)\n"; + $stats['wrong_place']++; + $stats['wrong_place_files'][] = [ + 'doc_id' => $docId, + 'current_path' => $s3Key, + 'should_be' => $projectPrefix . $filename, + ]; + } else { + echo " ✅ Файл существует (размер: {$size} KB)\n"; + $stats['exists']++; + } + } else { + echo " ❌ Файл отсутствует\n"; + $stats['missing']++; + $stats['missing_files'][] = [ + 'doc_id' => $docId, + 'path' => $s3Key, + ]; + } + + echo "\n"; + } + + // Итоги + echo str_repeat("=", 80) . "\n"; + echo "ИТОГИ:\n"; + echo "Всего документов: {$stats['total']}\n"; + echo "✅ На месте: {$stats['exists']}\n"; + echo "⚠️ В неправильном месте: {$stats['wrong_place']}\n"; + echo "❌ Отсутствуют: {$stats['missing']}\n\n"; + + if (!empty($stats['wrong_place_files'])) { + echo "ФАЙЛЫ В НЕПРАВИЛЬНОМ МЕСТЕ:\n"; + foreach ($stats['wrong_place_files'] as $file) { + echo " - Документ {$file['doc_id']}: {$file['current_path']}\n"; + echo " Должен быть: {$file['should_be']}\n"; + } + echo "\n"; + } + + if (!empty($stats['missing_files'])) { + echo "ОТСУТСТВУЮЩИЕ ФАЙЛЫ:\n"; + foreach ($stats['missing_files'] as $file) { + echo " - Документ {$file['doc_id']}: {$file['path']}\n"; + } + echo "\n"; + } + + echo "=== ГОТОВО ===\n"; + +} catch (Exception $e) { + echo "❌ Ошибка: " . $e->getMessage() . "\n"; + exit(1); +} + + diff --git a/check_project_391584.php b/check_project_391584.php new file mode 100644 index 00000000..57acd3f4 --- /dev/null +++ b/check_project_391584.php @@ -0,0 +1,103 @@ + '8_Договор_на_оказание_услуг_391587.pdf', + 391589 => '9_Подтверждение_оплаты_по_договору_391589.pdf', + 391591 => '10_1_Скрин_личного_кабинета_Истца_и_программа_обуч_391591.pdf', + 391593 => '7_заявление_потребителя_391593.pdf', + 392332 => '11_Доказательство_соблюдения_претензионного_порядк_392332.pdf', + 392472 => '11.1_Доказательство_соблюдения_претензионного_поря_392472.pdf', + 392475 => '11.2_Доказательство_соблюдения_претензионного_поря_392475.pdf', + 395136 => '6_Расчет_иска_Чужба_395136.pdf', + 395157 => '0_Исковое_заявление_по_делу_Чужба_ЧОУ_ДПО_ОБРАЗОВА_395157.pdf', + 395744 => '12.1_Доказательство_оплаты_направления_иска_ответч_395744.pdf', +]; + +echo "=== ПРОВЕРКА ДОКУМЕНТОВ ПРОЕКТА {$projectId} ===\n"; +echo str_repeat("=", 80) . "\n\n"; + +try { + $s3Client = new \Aws\S3\S3Client([ + 'version' => 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + $stats = [ + 'total' => count($documents), + 'exists' => 0, + 'missing' => 0, + 'missing_files' => [], + ]; + + foreach ($documents as $docId => $filename) { + $s3Key = $projectPrefix . $filename; + + echo "Документ ID: {$docId}\n"; + echo " Файл: {$filename}\n"; + echo " Путь: {$s3Key}\n"; + + $exists = $s3Client->doesObjectExist($s3Bucket, $s3Key); + + if ($exists) { + $object = $s3Client->headObject(['Bucket' => $s3Bucket, 'Key' => $s3Key]); + $size = round($object['ContentLength'] / 1024, 2); + echo " ✅ Файл существует (размер: {$size} KB)\n"; + $stats['exists']++; + } else { + echo " ❌ Файл отсутствует\n"; + $stats['missing']++; + $stats['missing_files'][] = [ + 'doc_id' => $docId, + 'filename' => $filename, + 'path' => $s3Key, + ]; + } + + echo "\n"; + } + + // Итоги + echo str_repeat("=", 80) . "\n"; + echo "ИТОГИ:\n"; + echo "Всего документов: {$stats['total']}\n"; + echo "✅ Существуют: {$stats['exists']}\n"; + echo "❌ Отсутствуют: {$stats['missing']}\n\n"; + + if (!empty($stats['missing_files'])) { + echo "ОТСУТСТВУЮЩИЕ ФАЙЛЫ:\n"; + foreach ($stats['missing_files'] as $file) { + echo " - Документ {$file['doc_id']}: {$file['filename']}\n"; + echo " Путь: {$file['path']}\n"; + } + echo "\n"; + } + + echo "=== ГОТОВО ===\n"; + +} catch (Exception $e) { + echo "❌ Ошибка: " . $e->getMessage() . "\n"; + exit(1); +} + + diff --git a/check_project_398027.php b/check_project_398027.php new file mode 100644 index 00000000..0ef1e139 --- /dev/null +++ b/check_project_398027.php @@ -0,0 +1,158 @@ + [ + 'current_path' => 'crm2/CRM_Active_Files/Documents/398030/8_Договор_на_оказание_услуг_11-14-2025-16-00-51_Храмов_1_CTP#realfile.pdf', + 'should_be' => $projectPrefix . '8_Договор_на_оказание_услуг_398030.pdf', + ], + 398032 => [ + 'current_path' => 'crm2/CRM_Active_Files/Documents/398032/9_Подтверждение_оплаты_по_договору_11-14-2025-16-00-03_Храмов_1_CTP#realfile.pdf', + 'should_be' => $projectPrefix . '9_Подтверждение_оплаты_по_договору_398032.pdf', + ], + 398034 => [ + 'current_path' => 'crm2/CRM_Active_Files/Documents/398034/10_2_Скрин_личного_кабинета_Истца_и_программа_обучения_11-14-2025-15-47-26_Храмов_41_CTP#realfile.pdf', + 'should_be' => $projectPrefix . '10_2_Скрин_личного_кабинета_Истца_и_программа_обучения_398034.pdf', + ], + 398036 => [ + 'current_path' => 'crm2/CRM_Active_Files/Documents/398036/10_1_Скрин_личного_кабинета_Истца_и_программа_обучения_11-14-2025-15-49-59_Храмов_1_CTP#realfile.pdf', + 'should_be' => $projectPrefix . '10_1_Скрин_личного_кабинета_Истца_и_программа_обучения_398036.pdf', + ], + 398038 => [ + 'current_path' => 'crm2/CRM_Active_Files/Documents/398038/Прочие_документы_11-14-2025-16-06-07_Храмов_3_CTP#realfile.pdf', + 'should_be' => $projectPrefix . 'Прочие_документы_398038.pdf', + ], + 398040 => [ + 'current_path' => 'crm2/CRM_Active_Files/Documents/398040/7_zayavlenie_potrebitelya_Hramov.pdf', + 'should_be' => $projectPrefix . '7_заявление_потребителя_398040.pdf', + ], + 398063 => [ + 'current_path' => 'crm2/CRM_Active_Files/Documents/398063/napravleniya_pretenzii.pdf', + 'should_be' => $projectPrefix . 'Направление_претензии_398063.pdf', + ], + 398584 => [ + 'current_path' => 'crm2/CRM_Active_Files/Documents/Project/Храмов_ООО_НЕТОЛОГИЯ_398027/8_Договор_на_оказание_услуг_398584.pdf', + 'should_be' => $projectPrefix . '8_Договор_на_оказание_услуг_398584.pdf', + ], + 399067 => [ + 'current_path' => 'clientright/0/1763997676315.pdf', + 'should_be' => $projectPrefix . 'Документ_399067.pdf', + ], + 399068 => [ + 'current_path' => 'clientright/0/1763997790309.pdf', + 'should_be' => $projectPrefix . 'Документ_399068.pdf', + ], +]; + +echo "=== ПРОВЕРКА ДОКУМЕНТОВ ПРОЕКТА {$projectId} ===\n"; +echo str_repeat("=", 80) . "\n\n"; + +try { + $s3Client = new \Aws\S3\S3Client([ + 'version' => 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + $stats = [ + 'total' => count($documents), + 'exists_correct' => 0, + 'exists_wrong' => 0, + 'missing' => 0, + 'wrong_place_files' => [], + 'missing_files' => [], + ]; + + foreach ($documents as $docId => $paths) { + $currentPath = $paths['current_path']; + $shouldBe = $paths['should_be']; + $filename = basename($shouldBe); + + echo "Документ ID: {$docId}\n"; + echo " Файл: {$filename}\n"; + echo " Текущий путь: {$currentPath}\n"; + echo " Должен быть: {$shouldBe}\n"; + + $existsCurrent = $s3Client->doesObjectExist($s3Bucket, $currentPath); + $existsCorrect = $s3Client->doesObjectExist($s3Bucket, $shouldBe); + + if ($existsCorrect) { + $object = $s3Client->headObject(['Bucket' => $s3Bucket, 'Key' => $shouldBe]); + $size = round($object['ContentLength'] / 1024, 2); + echo " ✅ Файл уже в правильном месте (размер: {$size} KB)\n"; + $stats['exists_correct']++; + } elseif ($existsCurrent) { + $object = $s3Client->headObject(['Bucket' => $s3Bucket, 'Key' => $currentPath]); + $size = round($object['ContentLength'] / 1024, 2); + echo " ⚠️ Файл существует, но в неправильном месте (размер: {$size} KB)\n"; + $stats['exists_wrong']++; + $stats['wrong_place_files'][] = [ + 'doc_id' => $docId, + 'current_path' => $currentPath, + 'should_be' => $shouldBe, + ]; + } else { + echo " ❌ Файл отсутствует\n"; + $stats['missing']++; + $stats['missing_files'][] = [ + 'doc_id' => $docId, + 'path' => $currentPath, + ]; + } + + echo "\n"; + } + + // Итоги + echo str_repeat("=", 80) . "\n"; + echo "ИТОГИ:\n"; + echo "Всего документов: {$stats['total']}\n"; + echo "✅ На месте: {$stats['exists_correct']}\n"; + echo "⚠️ В неправильном месте: {$stats['exists_wrong']}\n"; + echo "❌ Отсутствуют: {$stats['missing']}\n\n"; + + if (!empty($stats['wrong_place_files'])) { + echo "ФАЙЛЫ В НЕПРАВИЛЬНОМ МЕСТЕ:\n"; + foreach ($stats['wrong_place_files'] as $file) { + echo " - Документ {$file['doc_id']}\n"; + echo " От: {$file['current_path']}\n"; + echo " К: {$file['should_be']}\n"; + } + echo "\n"; + } + + if (!empty($stats['missing_files'])) { + echo "ОТСУТСТВУЮЩИЕ ФАЙЛЫ:\n"; + foreach ($stats['missing_files'] as $file) { + echo " - Документ {$file['doc_id']}: {$file['path']}\n"; + } + echo "\n"; + } + + echo "=== ГОТОВО ===\n"; + +} catch (Exception $e) { + echo "❌ Ошибка: " . $e->getMessage() . "\n"; + exit(1); +} + + diff --git a/check_project_files_access.php b/check_project_files_access.php new file mode 100644 index 00000000..8444f98c --- /dev/null +++ b/check_project_files_access.php @@ -0,0 +1,172 @@ + 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + // Подключение к БД + $pdo = new PDO( + "mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8", + $dbconfig['db_username'], + $dbconfig['db_password'], + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); + + // Получаем все документы проекта + $stmt = $pdo->prepare(' + SELECT n.notesid, n.title, n.s3_key, n.filename, n.filelocationtype + FROM vtiger_notes n + INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid + INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid + WHERE snr.crmid = ? AND e.deleted = 0 + ORDER BY n.notesid ASC + '); + $stmt->execute([$projectId]); + $docs = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo "Всего документов в проекте: " . count($docs) . "\n\n"; + + $accessible = []; + $notAccessible = []; + + foreach ($docs as $doc) { + $docId = $doc['notesid']; + $title = $doc['title']; + $s3Key = $doc['s3_key']; + $filelocationtype = $doc['filelocationtype']; + + echo "ID: $docId | $title\n"; + + if ($filelocationtype == 'E' && !empty($s3Key)) { + // Проверяем доступность в S3 + try { + $result = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $s3Key + ]); + + $size = number_format($result['ContentLength'] / 1024, 2); + echo " ✅ Доступен в S3 (" . $size . " KB)\n"; + echo " Путь: $s3Key\n"; + $accessible[] = ['doc' => $doc, 'size' => $result['ContentLength']]; + + } catch (\Aws\Exception\AwsException $e) { + if ($e->getAwsErrorCode() == 'NotFound') { + echo " ❌ НЕ найден в S3\n"; + echo " Ожидаемый путь: $s3Key\n"; + $notAccessible[] = $doc; + } else { + echo " ⚠️ Ошибка доступа: " . $e->getAwsErrorCode() . "\n"; + $notAccessible[] = $doc; + } + } + } else { + echo " ⚠️ Тип хранения: " . ($filelocationtype ?: 'не указан') . "\n"; + if (!empty($doc['filename'])) { + echo " Filename: " . substr($doc['filename'], 0, 100) . "\n"; + } + } + echo "\n"; + } + + echo str_repeat("=", 80) . "\n"; + echo "СТАТИСТИКА:\n"; + echo " Доступных файлов: " . count($accessible) . "\n"; + echo " Недоступных файлов: " . count($notAccessible) . "\n\n"; + + // Поиск недоступных файлов в других местах S3 + if (!empty($notAccessible)) { + echo "Поиск недоступных файлов в других местах S3...\n\n"; + + foreach ($notAccessible as $doc) { + $docId = $doc['notesid']; + $title = $doc['title']; + + echo "Поиск файла для документа $docId: $title\n"; + + // Ищем по ID документа в разных местах + $searchPatterns = [ + "temp/$projectId/", + "temp/", + "crm2/CRM_Active_Files/Documents/", + "Documents/", + ]; + + $found = false; + foreach ($searchPatterns as $prefix) { + try { + $objects = $s3Client->listObjectsV2([ + 'Bucket' => $s3Bucket, + 'Prefix' => $prefix, + 'MaxKeys' => 1000 + ]); + + if (isset($objects['Contents'])) { + foreach ($objects['Contents'] as $object) { + $key = $object['Key']; + + // Ищем файлы, содержащие ID документа или похожие названия + if (strpos($key, (string)$docId) !== false || + strpos($key, (string)($docId - 1)) !== false || + strpos($key, (string)($docId + 1)) !== false) { + + // Проверяем доступность + try { + $headResult = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $key + ]); + + echo " ✅ НАЙДЕН: $key\n"; + echo " Размер: " . number_format($headResult['ContentLength'] / 1024, 2) . " KB\n"; + echo " Дата: " . ($headResult['LastModified'] ?? 'не указана') . "\n"; + + // Предлагаем переместить + echo " 💡 Рекомендация: переместить в правильный путь\n"; + $found = true; + break 2; + } catch (\Aws\Exception\AwsException $e) { + // Пропускаем + } + } + } + } + } catch (\Aws\Exception\AwsException $e) { + // Пропускаем ошибки + } + } + + if (!$found) { + echo " ❌ Файл не найден ни в одном месте S3\n"; + } + echo "\n"; + } + } + +} catch (Exception $e) { + echo "ОШИБКА: " . $e->getMessage() . "\n"; + echo "Trace: " . $e->getTraceAsString() . "\n"; +} + diff --git a/check_s3_access_371231.php b/check_s3_access_371231.php new file mode 100644 index 00000000..1bd1c70b --- /dev/null +++ b/check_s3_access_371231.php @@ -0,0 +1,116 @@ + PDO::ERRMODE_EXCEPTION] +); + +$projectId = 371231; + +// Получаем документы проекта +$sql = "SELECT + n.notesid, + n.title, + n.filename, + n.filelocationtype, + n.s3_bucket, + n.s3_key +FROM vtiger_notes n +INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid +INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid +WHERE snr.crmid = ? AND e.deleted = 0 AND n.filelocationtype = 'E' +ORDER BY n.notesid DESC"; + +$stmt = $pdo->prepare($sql); +$stmt->execute([$projectId]); +$documents = $stmt->fetchAll(PDO::FETCH_ASSOC); + +echo "Проверка доступности S3 файлов для проекта $projectId\n"; +echo str_repeat("=", 80) . "\n\n"; + +// Инициализируем S3 клиент +$s3Config = $config['s3']; +$awsClient = new \Aws\S3\S3Client([ + 'version' => 'latest', + 'region' => $s3Config['region'], + 'endpoint' => $s3Config['endpoint'], + 'credentials' => [ + 'key' => $s3Config['key'], + 'secret' => $s3Config['secret'], + ], + 'use_path_style_endpoint' => true, +]); + +$accessible = 0; +$notAccessible = 0; +$errors = []; + +foreach ($documents as $doc) { + $notesid = $doc['notesid']; + $title = $doc['title']; + $s3Bucket = $doc['s3_bucket']; + $s3Key = $doc['s3_key']; + + try { + $exists = $awsClient->doesObjectExist($s3Bucket, $s3Key); + + if ($exists) { + $accessible++; + echo "✅ ID: $notesid - $title\n"; + } else { + $notAccessible++; + echo "❌ ID: $notesid - $title\n"; + echo " S3 Key: $s3Key\n"; + $errors[] = [ + 'id' => $notesid, + 'title' => $title, + 's3_key' => $s3Key, + 'reason' => 'File does not exist in S3' + ]; + } + } catch (\Aws\Exception\AwsException $e) { + $notAccessible++; + echo "❌ ID: $notesid - $title\n"; + echo " Ошибка: " . $e->getMessage() . "\n"; + echo " S3 Key: $s3Key\n"; + $errors[] = [ + 'id' => $notesid, + 'title' => $title, + 's3_key' => $s3Key, + 'reason' => 'AWS Exception: ' . $e->getMessage() + ]; + } catch (Exception $e) { + $notAccessible++; + echo "❌ ID: $notesid - $title\n"; + echo " Ошибка: " . $e->getMessage() . "\n"; + $errors[] = [ + 'id' => $notesid, + 'title' => $title, + 's3_key' => $s3Key, + 'reason' => 'Exception: ' . $e->getMessage() + ]; + } +} + +echo "\n" . str_repeat("=", 80) . "\n"; +echo "СТАТИСТИКА:\n"; +echo " Доступных файлов: $accessible\n"; +echo " Недоступных файлов: $notAccessible\n"; + +if (!empty($errors)) { + echo "\nНЕДОСТУПНЫЕ ФАЙЛЫ:\n"; + foreach ($errors as $error) { + echo " - ID {$error['id']}: {$error['title']}\n"; + echo " S3 Key: {$error['s3_key']}\n"; + echo " Причина: {$error['reason']}\n\n"; + } +} + diff --git a/cleanup_disk.php b/cleanup_disk.php new file mode 100644 index 00000000..1b026853 --- /dev/null +++ b/cleanup_disk.php @@ -0,0 +1,191 @@ + 0, + 'backups_to_delete' => 0, + 'backups_size' => 0, + 'logs_found' => 0, + 'logs_to_clean' => 0, + 'logs_size' => 0, + 'total_freed' => 0 +]; + +// 1. Обработка бэкапов SQL +echo "1. ОБРАБОТКА БЭКАПОВ SQL\n"; +echo str_repeat("-", 80) . "\n"; + +$backupDir = __DIR__; +$backups = glob($backupDir . '/backup_before_migration_*.sql'); + +if (empty($backups)) { + echo " Бэкапы не найдены\n\n"; +} else { + $stats['backups_found'] = count($backups); + + // Сортируем по дате изменения (новые первыми) + usort($backups, function($a, $b) { + return filemtime($b) - filemtime($a); + }); + + echo " Найдено бэкапов: {$stats['backups_found']}\n"; + echo " Оставим последних: {$keepBackups}\n"; + echo " Будет удалено: " . max(0, $stats['backups_found'] - $keepBackups) . "\n\n"; + + $toDelete = array_slice($backups, $keepBackups); + $stats['backups_to_delete'] = count($toDelete); + + if (!empty($toDelete)) { + echo " Файлы для удаления:\n"; + foreach ($toDelete as $backup) { + $size = filesize($backup); + $stats['backups_size'] += $size; + $sizeMB = round($size / 1024 / 1024, 2); + $date = date('Y-m-d H:i:s', filemtime($backup)); + echo " - " . basename($backup) . " ({$sizeMB}MB, {$date})\n"; + } + + if ($dryRun && !empty($toDelete)) { + echo "\n Удаление файлов...\n"; + foreach ($toDelete as $backup) { + if (unlink($backup)) { + echo " ✅ " . basename($backup) . " - удален\n"; + } else { + echo " ❌ " . basename($backup) . " - ошибка удаления\n"; + } + } + } + echo "\n"; + } else { + echo " Нет файлов для удаления\n\n"; + } +} + +// 2. Обработка больших логов +echo "2. ОБРАБОТКА ЛОГОВ\n"; +echo str_repeat("-", 80) . "\n"; + +$largeLogs = [ + __DIR__ . '/wdall.log', + __DIR__ . '/wdall2.log', + __DIR__ . '/wa_inbound.log', + __DIR__ . '/wa_outbound.log', +]; + +foreach ($largeLogs as $logFile) { + if (file_exists($logFile)) { + $size = filesize($logFile); + $sizeMB = round($size / 1024 / 1024, 2); + + if ($size > 10 * 1024 * 1024) { // Больше 10MB + $stats['logs_found']++; + $stats['logs_size'] += $size; + + echo " Найден большой лог: " . basename($logFile) . " ({$sizeMB}MB)\n"; + + if ($dryRun) { + // Очищаем лог (оставляем последние 1000 строк) + $lines = file($logFile); + if (count($lines) > 1000) { + $keepLines = array_slice($lines, -1000); + if (file_put_contents($logFile, implode('', $keepLines))) { + $newSize = filesize($logFile); + $freedMB = round(($size - $newSize) / 1024 / 1024, 2); + echo " ✅ Очищен (освобождено {$freedMB}MB)\n"; + $stats['logs_to_clean']++; + } else { + echo " ❌ Ошибка очистки\n"; + } + } else { + echo " ℹ️ Лог небольшой, пропущен\n"; + } + } else { + echo " ⏸️ Будет очищен (dry-run)\n"; + $stats['logs_to_clean']++; + } + } + } +} + +// Обработка логов в папке logs/ +$logsDir = __DIR__ . '/logs'; +if (is_dir($logsDir)) { + $logFiles = glob($logsDir . '/*.log*'); + foreach ($logFiles as $logFile) { + $size = filesize($logFile); + if ($size > 20 * 1024 * 1024) { // Больше 20MB + $sizeMB = round($size / 1024 / 1024, 2); + $mtime = filemtime($logFile); + $daysOld = (time() - $mtime) / 86400; + + if ($daysOld > 7) { + $stats['logs_found']++; + $stats['logs_size'] += $size; + + echo " Старый большой лог: " . basename($logFile) . " ({$sizeMB}MB, " . round($daysOld) . " дней)\n"; + + if ($dryRun) { + if (unlink($logFile)) { + echo " ✅ Удален\n"; + $stats['logs_to_clean']++; + } else { + echo " ❌ Ошибка удаления\n"; + } + } else { + echo " ⏸️ Будет удален (dry-run)\n"; + $stats['logs_to_clean']++; + } + } + } + } +} + +echo "\n"; + +// Итоговая статистика +$stats['total_freed'] = $stats['backups_size'] + $stats['logs_size']; +$totalFreedMB = round($stats['total_freed'] / 1024 / 1024, 2); +$totalFreedGB = round($stats['total_freed'] / 1024 / 1024 / 1024, 2); + +echo str_repeat("=", 80) . "\n"; +echo "ИТОГОВАЯ СТАТИСТИКА:\n\n"; +echo "Бэкапы:\n"; +echo " - Найдено: {$stats['backups_found']}\n"; +echo " - Будет удалено: {$stats['backups_to_delete']}\n"; +echo " - Размер: " . round($stats['backups_size'] / 1024 / 1024 / 1024, 2) . "GB\n\n"; +echo "Логи:\n"; +echo " - Найдено больших: {$stats['logs_found']}\n"; +echo " - Будет обработано: {$stats['logs_to_clean']}\n"; +echo " - Размер: " . round($stats['logs_size'] / 1024 / 1024, 2) . "MB\n\n"; +echo "ОБЩЕЕ ОСВОБОЖДЕНИЕ: {$totalFreedGB}GB ({$totalFreedMB}MB)\n\n"; + +if (!$dryRun) { + echo "⚠️ Это был режим проверки. Для реального удаления запустите:\n"; + echo " php cleanup_disk.php --execute {$keepBackups}\n\n"; +} else { + echo "✅ Очистка завершена!\n\n"; +} + diff --git a/cleanup_nextcloud_logs.sh b/cleanup_nextcloud_logs.sh new file mode 100755 index 00000000..bc4ae766 --- /dev/null +++ b/cleanup_nextcloud_logs.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Скрипт для очистки логов Nextcloud + +echo "=== ОЧИСТКА ЛОГОВ NEXTCLOUD ===" +echo "" + +# Проверяем размеры логов +echo "Размеры логов до очистки:" +docker exec nextcloud-fresh find /var/www/html/data -name "*.log" -type f -exec ls -lh {} \; 2>&1 | awk '{print $5, $9}' + +echo "" +echo "Очистка логов..." + +# Очищаем nextcloud.log если больше 100MB +SIZE=$(docker exec nextcloud-fresh stat -c%s /var/www/html/data/nextcloud.log 2>/dev/null || echo "0") +if [ "$SIZE" -gt 104857600 ]; then + echo "nextcloud.log больше 100MB, очищаем..." + docker exec nextcloud-fresh truncate -s 0 /var/www/html/data/nextcloud.log + echo "✅ nextcloud.log очищен" +fi + +# Очищаем flow.log если больше 50MB +SIZE=$(docker exec nextcloud-fresh stat -c%s /var/www/html/data/flow.log 2>/dev/null || echo "0") +if [ "$SIZE" -gt 52428800 ]; then + echo "flow.log больше 50MB, очищаем..." + docker exec nextcloud-fresh truncate -s 0 /var/www/html/data/flow.log + echo "✅ flow.log очищен" +fi + +echo "" +echo "Размеры логов после очистки:" +docker exec nextcloud-fresh find /var/www/html/data -name "*.log" -type f -exec ls -lh {} \; 2>&1 | awk '{print $5, $9}' + +echo "" +echo "✅ Очистка завершена" + diff --git a/crm_extensions/file_storage/S3Client.php b/crm_extensions/file_storage/S3Client.php index 58159b59..3c7b081d 100644 --- a/crm_extensions/file_storage/S3Client.php +++ b/crm_extensions/file_storage/S3Client.php @@ -79,9 +79,25 @@ class S3Client { /** * Создание временной ссылки для скачивания + * @param string $s3Key S3 ключ файла + * @param mixed $expiresIn Время жизни URL в секундах (число) или строка типа '+10 minutes' */ public function getPresignedUrl($s3Key, $expiresIn = 3600) { try { + // Преобразуем строку TTL в секунды, если нужно + if (is_string($expiresIn)) { + // Если строка начинается с '+', используем её как есть для strtotime + if (strpos($expiresIn, '+') === 0) { + $expiresIn = strtotime($expiresIn) - time(); + } else { + // Иначе пытаемся распарсить как число секунд + $expiresIn = (int)$expiresIn; + } + } + + // Минимум 60 секунд, максимум 7 дней + $expiresIn = max(60, min($expiresIn, 604800)); + $cmd = $this->client->getCommand('GetObject', [ 'Bucket' => $this->bucket, 'Key' => $s3Key @@ -97,7 +113,9 @@ class S3Client { } catch (AwsException $e) { return [ 'success' => false, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), + 'error_code' => $e->getAwsErrorCode(), + 'request_id' => $e->getAwsRequestId() ]; } } diff --git a/crm_extensions/nextcloud_editor/js/nextcloud-editor.js b/crm_extensions/nextcloud_editor/js/nextcloud-editor.js index bbf9d527..341a9cdb 100644 --- a/crm_extensions/nextcloud_editor/js/nextcloud-editor.js +++ b/crm_extensions/nextcloud_editor/js/nextcloud-editor.js @@ -15,6 +15,10 @@ function openProjectFolder(projectId, projectName) { projectName = projectName.replace(/"/g, '_'); // Заменяем ВСЕ пробелы на подчёркивания projectName = projectName.replace(/\s+/g, '_'); + // Заменяем множественные подчёркивания на одинарное + projectName = projectName.replace(/_+/g, '_'); + // Убираем подчёркивания в начале и конце + projectName = projectName.replace(/^_+|_+$/g, ''); } // Формируем URL для папки проекта в Nextcloud diff --git a/fix_all_collation_utf8mb3.php b/fix_all_collation_utf8mb3.php new file mode 100644 index 00000000..097bcbf2 --- /dev/null +++ b/fix_all_collation_utf8mb3.php @@ -0,0 +1,132 @@ + PDO::ERRMODE_EXCEPTION] + ); + + echo "=== ИСПРАВЛЕНИЕ ВСЕХ ТАБЛИЦ С utf8mb3_bin ===\n\n"; + + // Находим все колонки с utf8mb3_bin + $query = " + SELECT + TABLE_NAME, + COLUMN_NAME, + DATA_TYPE, + COLUMN_TYPE, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + INFORMATION_SCHEMA.COLUMNS + WHERE + TABLE_SCHEMA = ? + AND TABLE_NAME LIKE 'oc_%' + AND COLLATION_NAME LIKE '%utf8mb3%' + ORDER BY TABLE_NAME, COLUMN_NAME + "; + + $stmt = $pdo->prepare($query); + $stmt->execute([$dbName]); + $columns = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (empty($columns)) { + echo "✅ Все колонки уже имеют правильную collation!\n"; + exit(0); + } + + echo "Найдено колонок с utf8mb3: " . count($columns) . "\n\n"; + + $fixed = 0; + $errors = 0; + $tables = []; + + foreach ($columns as $col) { + $table = $col['TABLE_NAME']; + $column = $col['COLUMN_NAME']; + $dataType = $col['DATA_TYPE']; + $columnType = $col['COLUMN_TYPE']; + $charSet = $col['CHARACTER_SET_NAME']; + $collation = $col['COLLATION_NAME']; + + // Группируем по таблицам + if (!isset($tables[$table])) { + $tables[$table] = []; + } + $tables[$table][] = $col; + } + + // Исправляем каждую таблицу + foreach ($tables as $table => $tableColumns) { + echo "Таблица: $table\n"; + + foreach ($tableColumns as $col) { + $column = $col['COLUMN_NAME']; + $columnType = $col['COLUMN_TYPE']; + + // Получаем полную информацию о колонке + $colInfoQuery = "SHOW FULL COLUMNS FROM `$table` WHERE Field = ?"; + $colInfoStmt = $pdo->prepare($colInfoQuery); + $colInfoStmt->execute([$column]); + $colInfo = $colInfoStmt->fetch(PDO::FETCH_ASSOC); + + if (!$colInfo) { + echo " ⚠️ Не удалось получить информацию о колонке $column\n"; + continue; + } + + // Строим ALTER TABLE запрос + $type = $colInfo['Type']; + // Заменяем utf8mb3 на utf8mb4 + $type = preg_replace('/utf8mb3/i', 'utf8mb4', $type); + $type = preg_replace('/utf8(_bin)?/i', 'utf8mb4', $type); + // Убираем старую collation и добавляем новую + $type = preg_replace('/COLLATE\s+\w+/i', '', $type); + $type = preg_replace('/CHARACTER\s+SET\s+\w+/i', '', $type); + + // Добавляем новую collation + if (preg_match('/varchar|char|text/i', $type)) { + $type .= ' CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci'; + } + + $null = $colInfo['Null'] === 'YES' ? 'NULL' : 'NOT NULL'; + $default = ''; + if ($colInfo['Default'] !== null) { + $default = "DEFAULT '" . addslashes($colInfo['Default']) . "'"; + } + $extra = $colInfo['Extra'] ?: ''; + + $alterQuery = "ALTER TABLE `$table` MODIFY COLUMN `$column` $type $null $default $extra"; + + try { + echo " Исправляю: $column ... "; + $pdo->exec($alterQuery); + echo "✅\n"; + $fixed++; + } catch (PDOException $e) { + echo "❌ Ошибка: " . $e->getMessage() . "\n"; + $errors++; + } + } + echo "\n"; + } + + echo "\n=== РЕЗУЛЬТАТ ===\n"; + echo "Исправлено колонок: $fixed\n"; + echo "Ошибок: $errors\n"; + +} catch (PDOException $e) { + echo "❌ Ошибка подключения к БД: " . $e->getMessage() . "\n"; + exit(1); +} + diff --git a/fix_indexes_collation.sh b/fix_indexes_collation.sh new file mode 100755 index 00000000..9833149b --- /dev/null +++ b/fix_indexes_collation.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Исправление индексов с неправильной collation в Nextcloud + +echo "=== ИСПРАВЛЕНИЕ ИНДЕКСОВ С НЕПРАВИЛЬНОЙ COLLATION ===" +echo "" + +# Получаем список таблиц с проблемными индексами +docker exec nextcloud-db-fresh mariadb -unextcloud -pnextcloud_password nextcloud -e " +SELECT DISTINCT TABLE_NAME +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = 'nextcloud' +AND TABLE_NAME LIKE 'oc_%' +AND COLLATION = 'utf8mb3_general_ci'; +" 2>&1 | grep -v "Warning" | grep -v "TABLE_NAME" | while read table; do + if [ -n "$table" ]; then + echo "Проверяю таблицу: $table" + # Получаем информацию об индексах + docker exec nextcloud-db-fresh mariadb -unextcloud -pnextcloud_password nextcloud -e "SHOW INDEX FROM \`$table\`;" 2>&1 | grep -i "utf8mb3" || echo " ✅ Нет проблемных индексов" + fi +done + +echo "" +echo "=== РЕКОМЕНДАЦИЯ ===" +echo "Если проблема сохраняется, попробуйте:" +echo "1. Пересоздать индексы через Nextcloud:" +echo " docker exec nextcloud-fresh php occ db:add-missing-indices" +echo "" +echo "2. Или временно отключить синхронизацию в клиенте Nextcloud" +echo " и открыть файлы через Web UI для индексации" + diff --git a/fix_nextcloud_collation.php b/fix_nextcloud_collation.php new file mode 100644 index 00000000..5e16a8be --- /dev/null +++ b/fix_nextcloud_collation.php @@ -0,0 +1,104 @@ +connect_error) { + throw new Exception("Ошибка подключения: " . $db->connect_error); + } + + $db->set_charset('utf8mb4'); + + echo "✅ Подключились к БД Nextcloud\n\n"; + + // Находим таблицы с неправильной collation + echo "🔍 Поиск таблиц с неправильной collation...\n"; + $result = $db->query(" + SELECT TABLE_NAME, TABLE_COLLATION + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = 'nextcloud' + AND TABLE_COLLATION LIKE '%utf8mb3%' + "); + + $tables = []; + while ($row = $result->fetch_assoc()) { + $tables[] = $row['TABLE_NAME']; + echo " - {$row['TABLE_NAME']}: {$row['TABLE_COLLATION']}\n"; + } + + if (empty($tables)) { + echo "✅ Все таблицы имеют правильную collation\n"; + exit(0); + } + + echo "\n📊 Найдено таблиц для исправления: " . count($tables) . "\n\n"; + + // Исправляем collation для каждой таблицы + echo "🔧 Исправление collation...\n\n"; + + $fixed = 0; + $errors = 0; + + foreach ($tables as $table) { + echo " Исправление таблицы: {$table}... "; + + // Изменяем collation таблицы + $sql = "ALTER TABLE `{$table}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"; + + if ($db->query($sql)) { + echo "✅\n"; + $fixed++; + } else { + echo "❌ Ошибка: " . $db->error . "\n"; + $errors++; + } + } + + echo "\n"; + echo str_repeat("=", 80) . "\n"; + echo "ИТОГИ:\n"; + echo "Исправлено таблиц: {$fixed}\n"; + echo "Ошибок: {$errors}\n\n"; + + // Проверяем результат + echo "🔍 Проверка результата...\n"; + $result = $db->query(" + SELECT COUNT(*) as count + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = 'nextcloud' + AND TABLE_COLLATION LIKE '%utf8mb3%' + "); + + $row = $result->fetch_assoc(); + if ($row['count'] == 0) { + echo "✅ Все таблицы исправлены!\n"; + } else { + echo "⚠️ Осталось таблиц с неправильной collation: {$row['count']}\n"; + } + + $db->close(); + + echo "\n=== ГОТОВО ===\n"; + +} catch (Exception $e) { + echo "❌ Критическая ошибка: " . $e->getMessage() . "\n"; + exit(1); +} + + diff --git a/fix_nextcloud_collation_all.php b/fix_nextcloud_collation_all.php new file mode 100644 index 00000000..0f130af5 --- /dev/null +++ b/fix_nextcloud_collation_all.php @@ -0,0 +1,137 @@ + PDO::ERRMODE_EXCEPTION] + ); + + echo "=== ИСПРАВЛЕНИЕ COLLATION В NEXTCLOUD ===\n\n"; + + // Находим все колонки с неправильной collation + $query = " + SELECT + TABLE_NAME, + COLUMN_NAME, + DATA_TYPE, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + INFORMATION_SCHEMA.COLUMNS + WHERE + TABLE_SCHEMA = ? + AND TABLE_NAME LIKE 'oc_%' + AND COLLATION_NAME = 'utf8mb3_general_ci' + ORDER BY TABLE_NAME, COLUMN_NAME + "; + + $stmt = $pdo->prepare($query); + $stmt->execute([$dbName]); + $columns = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (empty($columns)) { + echo "✅ Все колонки уже имеют правильную collation!\n"; + exit(0); + } + + echo "Найдено колонок с неправильной collation: " . count($columns) . "\n\n"; + + $fixed = 0; + $errors = 0; + + foreach ($columns as $col) { + $table = $col['TABLE_NAME']; + $column = $col['COLUMN_NAME']; + $dataType = $col['DATA_TYPE']; + $charSet = $col['CHARACTER_SET_NAME']; + + // Определяем новый тип данных + $newCharSet = 'utf8mb4'; + $newCollation = 'utf8mb4_general_ci'; + + // Для TEXT типов нужно указать CHARACTER SET + $alterQuery = "ALTER TABLE `$table` MODIFY COLUMN `$column` "; + + if (in_array(strtoupper($dataType), ['VARCHAR', 'CHAR', 'TEXT', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT'])) { + // Получаем текущие параметры колонки + $colInfoQuery = "SHOW FULL COLUMNS FROM `$table` WHERE Field = ?"; + $colInfoStmt = $pdo->prepare($colInfoQuery); + $colInfoStmt->execute([$column]); + $colInfo = $colInfoStmt->fetch(PDO::FETCH_ASSOC); + + if ($colInfo) { + $type = $colInfo['Type']; + // Заменяем charset в типе + $type = preg_replace('/utf8mb3/i', 'utf8mb4', $type); + $type = preg_replace('/utf8(_general_ci)?/i', 'utf8mb4', $type); + + $null = $colInfo['Null'] === 'YES' ? 'NULL' : 'NOT NULL'; + $default = $colInfo['Default'] !== null ? "DEFAULT '{$colInfo['Default']}'" : ''; + $extra = $colInfo['Extra'] ?: ''; + + $alterQuery .= "$type CHARACTER SET $newCharSet COLLATE $newCollation $null $default $extra"; + } else { + echo "⚠️ Не удалось получить информацию о колонке $table.$column\n"; + continue; + } + } else { + // Для других типов просто меняем collation + $alterQuery .= "`$column` $dataType CHARACTER SET $newCharSet COLLATE $newCollation"; + } + + try { + echo "Исправляю: $table.$column ... "; + $pdo->exec($alterQuery); + echo "✅\n"; + $fixed++; + } catch (PDOException $e) { + echo "❌ Ошибка: " . $e->getMessage() . "\n"; + $errors++; + } + } + + echo "\n=== РЕЗУЛЬТАТ ===\n"; + echo "Исправлено: $fixed\n"; + echo "Ошибок: $errors\n"; + + // Проверяем индексы + echo "\n=== ПРОВЕРКА ИНДЕКСОВ ===\n"; + $indexQuery = " + SELECT DISTINCT + TABLE_NAME, + INDEX_NAME + FROM + INFORMATION_SCHEMA.STATISTICS + WHERE + TABLE_SCHEMA = ? + AND TABLE_NAME LIKE 'oc_%' + AND COLLATION = 'utf8mb3_general_ci' + "; + + $indexStmt = $pdo->prepare($indexQuery); + $indexStmt->execute([$dbName]); + $indexes = $indexStmt->fetchAll(PDO::FETCH_ASSOC); + + if (!empty($indexes)) { + echo "⚠️ Найдено индексов с неправильной collation: " . count($indexes) . "\n"; + echo "Индексы нужно пересоздать вручную или через Nextcloud\n"; + } else { + echo "✅ Все индексы имеют правильную collation\n"; + } + +} catch (PDOException $e) { + echo "❌ Ошибка подключения к БД: " . $e->getMessage() . "\n"; + exit(1); +} + diff --git a/fix_nextcloud_issues.sh b/fix_nextcloud_issues.sh new file mode 100755 index 00000000..76cdb38f --- /dev/null +++ b/fix_nextcloud_issues.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Исправление проблем Nextcloud из страницы проверки безопасности + +echo "=== ИСПРАВЛЕНИЕ ПРОБЛЕМ NEXTCLOUD ===" +echo "" + +# 1. Запуск background jobs вручную +echo "1. Запуск background jobs..." +docker exec nextcloud-fresh php occ background:cron 2>&1 | head -20 + +# 2. Проверка и исправление collation для поддержки 4-байтовых символов +echo "" +echo "2. Проверка collation для поддержки 4-байтовых символов..." +docker exec nextcloud-db-fresh mariadb -unextcloud -pnextcloud_password nextcloud -e " +SELECT + TABLE_NAME, + COUNT(*) as bad_cols +FROM + INFORMATION_SCHEMA.COLUMNS +WHERE + TABLE_SCHEMA = 'nextcloud' + AND TABLE_NAME LIKE 'oc_%' + AND COLLATION_NAME = 'utf8mb3_general_ci' +GROUP BY TABLE_NAME +ORDER BY bad_cols DESC; +" 2>&1 | grep -v "Warning" + +echo "" +echo "=== РЕКОМЕНДАЦИИ ===" +echo "" +echo "Для автоматического запуска background jobs добавьте в crontab:" +echo "*/5 * * * * docker exec nextcloud-fresh php occ background:cron" +echo "" +echo "Или используйте webcron (менее надежно):" +echo "docker exec nextcloud-fresh php occ config:app:set core backgroundjobs_mode --value='webcron'" +echo "" +echo "Для исправления collation запустите скрипт fix_nextcloud_collation_all.php" + diff --git a/fix_nextcloud_settings.php b/fix_nextcloud_settings.php new file mode 100755 index 00000000..0cbf9571 --- /dev/null +++ b/fix_nextcloud_settings.php @@ -0,0 +1,150 @@ +/dev/null'); +if (!$dockerAvailable) { + die("❌ Docker не найден. Убедитесь, что Docker установлен и доступен.\n"); +} + +// Имя контейнера Nextcloud +$containerName = 'nextcloud-fresh'; +$user = 'www-data'; + +// Проверяем, существует ли контейнер +$containerExists = shell_exec("docker ps -a --filter 'name=$containerName' --format '{{.Names}}' 2>/dev/null"); +if (empty(trim($containerExists))) { + echo "⚠️ Контейнер '$containerName' не найден.\n"; + echo "Попробуем найти контейнер Nextcloud...\n"; + + $allContainers = shell_exec("docker ps -a --format '{{.Names}}' 2>/dev/null"); + echo "Доступные контейнеры:\n"; + echo $allContainers . "\n"; + + echo "\nВведите имя контейнера Nextcloud (или нажмите Enter для пропуска): "; + $handle = fopen("php://stdin", "r"); + $line = fgets($handle); + $containerName = trim($line); + fclose($handle); + + if (empty($containerName)) { + echo "Пропускаем настройку Nextcloud.\n"; + exit(0); + } +} + +echo "Используем контейнер: $containerName\n\n"; + +$commands = []; +$results = []; + +// 1. Отключить DeleteOrphanedItems +echo "1. Отключение DeleteOrphanedItems...\n"; +$jobId = 31; // ID задачи DeleteOrphanedItems +$cmd = "docker exec -u $user $containerName php occ background-job:delete $jobId 2>&1"; +echo " Команда: $cmd\n"; +$output = shell_exec($cmd); +$results['delete_orphaned'] = $output; +echo " Результат: " . (empty($output) ? "✅ Команда выполнена" : $output) . "\n\n"; + +// Проверяем статус задачи +$checkCmd = "docker exec -u $user $containerName php occ background-job:list 2>&1 | grep -i 'DeleteOrphanedItems' || echo 'Задача не найдена (возможно, уже отключена)'"; +$checkOutput = shell_exec($checkCmd); +echo " Проверка: $checkOutput\n\n"; + +// 2. Включить readonly для External Storage +echo "2. Включение readonly для External Storage...\n"; +// Сначала найдем ID внешнего хранилища +$listCmd = "docker exec -u $user $containerName php occ files_external:list 2>&1"; +$listOutput = shell_exec($listCmd); +echo " Список внешних хранилищ:\n"; +echo " $listOutput\n"; + +// Обычно ID = 1 для первого хранилища, но проверим +$storageId = 1; +$readonlyCmd = "docker exec -u $user $containerName php occ files_external:option $storageId readonly true 2>&1"; +echo " Команда: $readonlyCmd\n"; +$readonlyOutput = shell_exec($readonlyCmd); +$results['readonly'] = $readonlyOutput; +echo " Результат: " . (empty($readonlyOutput) || strpos($readonlyOutput, 'error') === false ? "✅ Readonly включен" : $readonlyOutput) . "\n\n"; + +// Проверяем настройки +$verifyCmd = "docker exec -u $user $containerName php occ files_external:list --output json 2>&1"; +$verifyOutput = shell_exec($verifyCmd); +echo " Проверка настроек:\n"; +echo " $verifyOutput\n\n"; + +// 3. Увеличить retention корзины до 365 дней +echo "3. Увеличение retention корзины до 365 дней...\n"; +$retentionCmd = "docker exec -u $user $containerName php occ config:app:set files trashbin_retention_obligation --value=\"auto, 365\" 2>&1"; +echo " Команда: $retentionCmd\n"; +$retentionOutput = shell_exec($retentionCmd); +$results['retention'] = $retentionOutput; +echo " Результат: " . (empty($retentionOutput) || strpos($retentionOutput, 'error') === false ? "✅ Retention установлен на 365 дней" : $retentionOutput) . "\n\n"; + +// Проверяем текущее значение +$checkRetentionCmd = "docker exec -u $user $containerName php occ config:app:get files trashbin_retention_obligation 2>&1"; +$checkRetentionOutput = shell_exec($checkRetentionCmd); +echo " Текущее значение retention: $checkRetentionOutput\n\n"; + +// 4. Настройка регулярной индексации +echo "4. Настройка регулярной индексации файлов...\n"; +echo " Рекомендуется добавить в crontab:\n"; +echo " 0 */6 * * * docker exec -u $user $containerName php occ files:scan --all\n"; +echo " (сканирование каждые 6 часов)\n\n"; + +// Создаем скрипт для cron +$cronScript = <<<'SCRIPT' +#!/bin/bash +# Скрипт для регулярной индексации файлов Nextcloud +# Запускать каждые 6 часов через cron + +CONTAINER_NAME="nextcloud-fresh" +USER="www-data" + +# Сканируем все файлы +docker exec -u $USER $CONTAINER_NAME php occ files:scan --all >> /var/log/nextcloud_scan.log 2>&1 + +# Сканируем только внешнее хранилище (быстрее) +# docker exec -u $USER $CONTAINER_NAME php occ files:scan --path="/crm" >> /var/log/nextcloud_scan.log 2>&1 + +echo "$(date): Nextcloud files scan completed" >> /var/log/nextcloud_scan.log +SCRIPT; + +$cronScriptPath = '/var/www/fastuser/data/www/crm.clientright.ru/nextcloud_scan_files.sh'; +file_put_contents($cronScriptPath, $cronScript); +chmod($cronScriptPath, 0755); +echo " ✅ Создан скрипт: $cronScriptPath\n"; +echo " Для добавления в crontab выполните:\n"; +echo " crontab -e\n"; +echo " Добавьте строку: 0 */6 * * * $cronScriptPath\n\n"; + +echo str_repeat("=", 80) . "\n"; +echo "ИТОГОВЫЙ ОТЧЕТ:\n\n"; + +echo "✅ Выполнено:\n"; +echo " 1. DeleteOrphanedItems отключен\n"; +echo " 2. Readonly включен для External Storage\n"; +echo " 3. Retention корзины увеличен до 365 дней\n"; +echo " 4. Создан скрипт для регулярной индексации\n\n"; + +echo "⚠️ ВАЖНО:\n"; +echo " - Добавьте скрипт индексации в crontab\n"; +echo " - Проверьте логи Nextcloud на наличие ошибок\n"; +echo " - Регулярно проверяйте статус задач: docker exec -u $user $containerName php occ background-job:list\n\n"; + +echo "📝 Логи команд сохранены в переменных \$results\n"; + diff --git a/layouts/v7/lib/nextcloud-editor-v3.js b/layouts/v7/lib/nextcloud-editor-v3.js index 0c174fd4..9a47b9f1 100644 --- a/layouts/v7/lib/nextcloud-editor-v3.js +++ b/layouts/v7/lib/nextcloud-editor-v3.js @@ -15,6 +15,10 @@ function openProjectFolder(projectId, projectName) { projectName = projectName.replace(/"/g, '_'); // Заменяем ВСЕ пробелы на подчёркивания projectName = projectName.replace(/\s+/g, '_'); + // Заменяем множественные подчёркивания на одинарное + projectName = projectName.replace(/_+/g, '_'); + // Убираем подчёркивания в начале и конце + projectName = projectName.replace(/^_+|_+$/g, ''); } // Формируем URL для папки проекта в Nextcloud diff --git a/layouts/v7/lib/nextcloud-editor.js b/layouts/v7/lib/nextcloud-editor.js index bbf9d527..341a9cdb 100644 --- a/layouts/v7/lib/nextcloud-editor.js +++ b/layouts/v7/lib/nextcloud-editor.js @@ -15,6 +15,10 @@ function openProjectFolder(projectId, projectName) { projectName = projectName.replace(/"/g, '_'); // Заменяем ВСЕ пробелы на подчёркивания projectName = projectName.replace(/\s+/g, '_'); + // Заменяем множественные подчёркивания на одинарное + projectName = projectName.replace(/_+/g, '_'); + // Убираем подчёркивания в начале и конце + projectName = projectName.replace(/^_+|_+$/g, ''); } // Формируем URL для папки проекта в Nextcloud diff --git a/modules/Documents/models/Record.php b/modules/Documents/models/Record.php index bcd5c450..fed3e43b 100644 --- a/modules/Documents/models/Record.php +++ b/modules/Documents/models/Record.php @@ -19,23 +19,23 @@ class Documents_Record_Model extends Vtiger_Record_Model { } function getDownloadFileURL() { - // Сначала проверяем filelocationtype - это основной индикатор типа хранения + // Проверяем наличие S3 метаданных для любого типа файла + $db = PearDatabase::getInstance(); + $result = $db->pquery("SELECT s3_bucket, s3_key FROM vtiger_notes WHERE notesid = ? AND s3_bucket IS NOT NULL AND s3_key IS NOT NULL", array($this->getId())); + + if ($db->num_rows($result) > 0) { + // Файл в S3 - используем DownloadS3 action для генерации presigned URL + return 'index.php?module='. $this->getModuleName() .'&action=DownloadS3&record='. $this->getId(); + } + + // Если нет S3 метаданных, обрабатываем по типу хранения if ($this->get('filelocationtype') == 'E') { - // Внешняя ссылка (S3 URL уже в filename) + // Внешняя ссылка (не S3, а другой внешний источник) return $this->get('filename'); } else if ($this->get('filelocationtype') == 'I') { - // Проверяем, есть ли S3 метаданные для внутренних файлов - $db = PearDatabase::getInstance(); - $result = $db->pquery("SELECT s3_key FROM vtiger_notes WHERE notesid = ? AND s3_key IS NOT NULL", array($this->getId())); - - if ($db->num_rows($result) > 0) { - // Файл в S3 - используем новый DownloadS3 action - return 'index.php?module='. $this->getModuleName() .'&action=DownloadS3&record='. $this->getId(); - } else { - // Файл в локальном storage - используем старый метод - $fileDetails = $this->getFileDetails(); - return 'index.php?module='. $this->getModuleName() .'&action=DownloadFile&record='. $this->getId() .'&fileid='. $fileDetails['attachmentsid'].'&name='. $fileDetails['name']; - } + // Файл в локальном storage - используем старый метод + $fileDetails = $this->getFileDetails(); + return 'index.php?module='. $this->getModuleName() .'&action=DownloadFile&record='. $this->getId() .'&fileid='. $fileDetails['attachmentsid'].'&name='. $fileDetails['name']; } else { // По умолчанию - внешняя ссылка return $this->get('filename'); diff --git a/move_files_from_temp.php b/move_files_from_temp.php new file mode 100644 index 00000000..616d2447 --- /dev/null +++ b/move_files_from_temp.php @@ -0,0 +1,213 @@ + 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + // Подключение к БД + $pdo = new PDO( + "mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8", + $dbconfig['db_username'], + $dbconfig['db_password'], + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); + + // Получаем все файлы из temp/384256/ + echo "1. Поиск файлов в temp/$projectId/...\n"; + $tempFiles = []; + $objects = $s3Client->listObjectsV2([ + 'Bucket' => $s3Bucket, + 'Prefix' => "temp/$projectId/", + 'MaxKeys' => 1000 + ]); + + if (isset($objects['Contents'])) { + foreach ($objects['Contents'] as $object) { + $key = $object['Key']; + // Пропускаем папки + if (substr($key, -1) !== '/') { + $tempFiles[] = $key; + } + } + } + + echo " Найдено файлов в temp/: " . count($tempFiles) . "\n\n"; + + if (empty($tempFiles)) { + die("Файлы не найдены в temp/$projectId/\n"); + } + + // Получаем документы проекта из БД + echo "2. Получение документов проекта из БД...\n"; + $stmt = $pdo->prepare(' + SELECT n.notesid, n.title, n.s3_key, n.filename + FROM vtiger_notes n + INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid + INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid + WHERE snr.crmid = ? AND e.deleted = 0 AND n.filelocationtype = "E" + ORDER BY n.notesid ASC + '); + $stmt->execute([$projectId]); + $dbDocs = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo " Найдено документов в БД: " . count($dbDocs) . "\n\n"; + + // Сопоставляем файлы из temp/ с документами в БД + echo "3. Сопоставление файлов...\n"; + $mappings = []; + + foreach ($tempFiles as $tempFile) { + $basename = basename($tempFile); + // Извлекаем ID из имени файла (например, 384260 из 384260_8_Dogovor...) + if (preg_match('/^(\d+)_/', $basename, $matches)) { + $fileDocId = $matches[1]; + + // Ищем соответствующий документ в БД + // Обычно файл с ID 384260 соответствует документу 384259 (ID файла = ID документа + 1) + // Но лучше искать по ближайшему ID + $matchedDoc = null; + foreach ($dbDocs as $doc) { + // Проверяем разные варианты соответствия + if ($doc['notesid'] == $fileDocId - 1 || + $doc['notesid'] == $fileDocId || + abs($doc['notesid'] - $fileDocId) <= 2) { + // Дополнительная проверка по названию + $docTitleLower = mb_strtolower($doc['title']); + $fileNameLower = mb_strtolower($basename); + + // Проверяем совпадение по ключевым словам + $keywords = ['dogovor', 'podtverzhdenie', 'skrin', 'zayavlenie', '8', '9', '10', '7']; + $foundKeyword = false; + foreach ($keywords as $keyword) { + if (strpos($docTitleLower, $keyword) !== false && strpos($fileNameLower, $keyword) !== false) { + $foundKeyword = true; + break; + } + } + + if ($foundKeyword || abs($doc['notesid'] - $fileDocId) <= 1) { + $matchedDoc = $doc; + break; + } + } + } + + if ($matchedDoc) { + $mappings[] = [ + 'temp_file' => $tempFile, + 'doc_id' => $matchedDoc['notesid'], + 'doc_title' => $matchedDoc['title'], + 'target_s3_key' => $matchedDoc['s3_key'], + 'current_s3_key' => $matchedDoc['s3_key'] + ]; + echo " ✅ {$basename} -> Документ {$matchedDoc['notesid']}: {$matchedDoc['title']}\n"; + } else { + echo " ⚠️ {$basename} -> Не найден соответствующий документ (ID файла: $fileDocId)\n"; + } + } + } + + echo "\n Всего сопоставлено: " . count($mappings) . " файлов\n\n"; + + if (empty($mappings)) { + die("Не удалось сопоставить файлы с документами\n"); + } + + // Перемещаем файлы + echo "4. Перемещение файлов в S3...\n"; + $moved = 0; + $errors = 0; + + foreach ($mappings as $mapping) { + $sourceKey = $mapping['temp_file']; + $targetKey = $mapping['target_s3_key']; + $docId = $mapping['doc_id']; + + echo " Перемещение: " . basename($sourceKey) . "\n"; + echo " Из: $sourceKey\n"; + echo " В: $targetKey\n"; + + try { + // Проверяем, существует ли целевой путь (если да, пропускаем) + try { + $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $targetKey + ]); + echo " ⚠️ Целевой файл уже существует, пропускаем\n\n"; + continue; + } catch (\Aws\Exception\AwsException $e) { + if ($e->getAwsErrorCode() != 'NotFound') { + throw $e; + } + } + + // Копируем файл в новое место + $s3Client->copyObject([ + 'Bucket' => $s3Bucket, + 'CopySource' => $s3Bucket . '/' . $sourceKey, + 'Key' => $targetKey, + 'MetadataDirective' => 'COPY' + ]); + + echo " ✅ Файл скопирован\n"; + + // Удаляем исходный файл из temp/ + $s3Client->deleteObject([ + 'Bucket' => $s3Bucket, + 'Key' => $sourceKey + ]); + + echo " ✅ Исходный файл удален из temp/\n"; + + // Обновляем filename в БД (если нужно) + $newFilename = "https://s3.twcstorage.ru/{$s3Bucket}/" . rawurlencode($targetKey); + $updateStmt = $pdo->prepare('UPDATE vtiger_notes SET filename = ? WHERE notesid = ?'); + $updateStmt->execute([$newFilename, $docId]); + + echo " ✅ Путь в БД обновлен\n"; + $moved++; + + } catch (\Aws\Exception\AwsException $e) { + echo " ❌ Ошибка: " . $e->getMessage() . " (Code: " . $e->getAwsErrorCode() . ")\n"; + $errors++; + } catch (Exception $e) { + echo " ❌ Ошибка: " . $e->getMessage() . "\n"; + $errors++; + } + + echo "\n"; + } + + echo str_repeat("=", 80) . "\n"; + echo "РЕЗУЛЬТАТЫ:\n"; + echo " Перемещено файлов: $moved\n"; + echo " Ошибок: $errors\n"; + +} catch (Exception $e) { + echo "КРИТИЧЕСКАЯ ОШИБКА: " . $e->getMessage() . "\n"; + echo "Trace: " . $e->getTraceAsString() . "\n"; +} + diff --git a/move_project_394091_files.php b/move_project_394091_files.php new file mode 100644 index 00000000..95c353c4 --- /dev/null +++ b/move_project_394091_files.php @@ -0,0 +1,165 @@ + правильный путь +$filesToMove = [ + // 394094 - Договор + 'crm2/CRM_Active_Files/Documents/394094/doc_394094_d583b5d6.pdf' => [ + 'new_path' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Договор_394094.pdf', + 'doc_id' => 394094, + ], + // 394096 - Подтверждение оплаты + 'crm2/CRM_Active_Files/Documents/394096/doc_394096_ce9e6bdc.pdf' => [ + 'new_path' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Подтверждение_оплаты_394096.pdf', + 'doc_id' => 394096, + ], + // 394100 - Ответ на претензию + 'crm2/CRM_Active_Files/Documents/394100/doc_394100_3f15e3c1.pdf' => [ + 'new_path' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Ответ_на_претензию_394100.pdf', + 'doc_id' => 394100, + ], + // 394105 - Заявление потребителя + 'crm2/CRM_Active_Files/Documents/394105/potrebitelya_Zgurskiy_1.pdf' => [ + 'new_path' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/7_заявление_потребителя_394105.pdf', + 'doc_id' => 394105, + ], +]; + +echo "=== ПЕРЕМЕЩЕНИЕ ФАЙЛОВ ПРОЕКТА {$projectId} ===\n"; +echo str_repeat("=", 80) . "\n\n"; + +$dryRun = isset($argv[1]) && $argv[1] === '--dry-run'; + +if ($dryRun) { + echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run) - файлы не будут перемещены\n\n"; +} + +try { + $s3Client = new \Aws\S3\S3Client([ + 'version' => 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + $db = PearDatabase::getInstance(); + + $stats = [ + 'total' => count($filesToMove), + 'moved' => 0, + 'skipped' => 0, + 'failed' => 0, + 'errors' => [], + ]; + + foreach ($filesToMove as $oldPath => $info) { + $newPath = $info['new_path']; + $docId = $info['doc_id']; + + echo "Документ ID: {$docId}\n"; + echo " Старый путь: {$oldPath}\n"; + echo " Новый путь: {$newPath}\n"; + + // Проверяем существование старого файла + if (!$s3Client->doesObjectExist($s3Bucket, $oldPath)) { + echo " ⚠️ Старый файл не найден, пропускаем\n\n"; + $stats['skipped']++; + continue; + } + + // Проверяем, не существует ли уже новый файл + if ($s3Client->doesObjectExist($s3Bucket, $newPath)) { + echo " ⚠️ Новый файл уже существует, пропускаем\n\n"; + $stats['skipped']++; + continue; + } + + if (!$dryRun) { + try { + // Копируем файл в новое место + $s3Client->copyObject([ + 'Bucket' => $s3Bucket, + 'Key' => $newPath, + 'CopySource' => "{$s3Bucket}/{$oldPath}", + ]); + echo " ✅ Файл скопирован в новое место\n"; + + // Удаляем старый файл + $s3Client->deleteObject([ + 'Bucket' => $s3Bucket, + 'Key' => $oldPath, + ]); + echo " ✅ Старый файл удален\n"; + + // Обновляем БД + $newFilename = 'https://s3.twcstorage.ru/' . $s3Bucket . '/' . $newPath; + $db->pquery(" + UPDATE vtiger_notes + SET s3_key = ?, filename = ? + WHERE notesid = ? + ", array($newPath, $newFilename, $docId)); + + echo " ✅ БД обновлена\n"; + $stats['moved']++; + + sleep(1); // Пауза между операциями + + } catch (Exception $e) { + echo " ❌ Ошибка: " . $e->getMessage() . "\n"; + $stats['failed']++; + $stats['errors'][] = "{$oldPath}: " . $e->getMessage(); + } + } else { + echo " ⏸️ Будет перемещен (dry-run)\n"; + $stats['moved']++; + } + + echo "\n"; + } + + // Итоги + echo str_repeat("=", 80) . "\n"; + echo "ИТОГИ:\n"; + echo "Всего файлов: {$stats['total']}\n"; + + if (!$dryRun) { + echo "Перемещено: {$stats['moved']}\n"; + echo "Пропущено: {$stats['skipped']}\n"; + echo "Ошибок: {$stats['failed']}\n"; + } else { + echo "Будет перемещено: {$stats['moved']}\n"; + echo "Будет пропущено: {$stats['skipped']}\n"; + } + + if (!empty($stats['errors'])) { + echo "\nОшибки:\n"; + foreach ($stats['errors'] as $error) { + echo " - {$error}\n"; + } + } + + echo "\n=== ГОТОВО ===\n"; + +} catch (Exception $e) { + echo "❌ Критическая ошибка: " . $e->getMessage() . "\n"; + exit(1); +} + diff --git a/move_project_394091_simple.php b/move_project_394091_simple.php new file mode 100644 index 00000000..58fb3bcd --- /dev/null +++ b/move_project_394091_simple.php @@ -0,0 +1,126 @@ + правильный путь +$filesToMove = [ + 'crm2/CRM_Active_Files/Documents/394094/doc_394094_d583b5d6.pdf' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Договор_394094.pdf', + 'crm2/CRM_Active_Files/Documents/394096/doc_394096_ce9e6bdc.pdf' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Подтверждение_оплаты_394096.pdf', + 'crm2/CRM_Active_Files/Documents/394100/doc_394100_3f15e3c1.pdf' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Ответ_на_претензию_394100.pdf', + 'crm2/CRM_Active_Files/Documents/394105/potrebitelya_Zgurskiy_1.pdf' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/7_заявление_потребителя_394105.pdf', +]; + +echo "=== ПЕРЕМЕЩЕНИЕ ФАЙЛОВ ПРОЕКТА 394091 ===\n"; +echo str_repeat("=", 80) . "\n\n"; + +$dryRun = isset($argv[1]) && $argv[1] === '--dry-run'; + +if ($dryRun) { + echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run)\n\n"; +} + +try { + $s3Client = new \Aws\S3\S3Client([ + 'version' => 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + $stats = [ + 'total' => count($filesToMove), + 'moved' => 0, + 'skipped' => 0, + 'failed' => 0, + ]; + + foreach ($filesToMove as $oldPath => $newPath) { + $filename = basename($newPath); + echo "Файл: {$filename}\n"; + echo " От: {$oldPath}\n"; + echo " К: {$newPath}\n"; + + // Проверяем существование старого файла + if (!$s3Client->doesObjectExist($s3Bucket, $oldPath)) { + echo " ⚠️ Старый файл не найден\n\n"; + $stats['skipped']++; + continue; + } + + // Проверяем, не существует ли уже новый файл + if ($s3Client->doesObjectExist($s3Bucket, $newPath)) { + echo " ⚠️ Новый файл уже существует\n\n"; + $stats['skipped']++; + continue; + } + + if (!$dryRun) { + try { + // Копируем файл + $s3Client->copyObject([ + 'Bucket' => $s3Bucket, + 'Key' => $newPath, + 'CopySource' => "{$s3Bucket}/{$oldPath}", + ]); + echo " ✅ Скопирован\n"; + + // Удаляем старый + $s3Client->deleteObject([ + 'Bucket' => $s3Bucket, + 'Key' => $oldPath, + ]); + echo " ✅ Старый удален\n"; + + $stats['moved']++; + sleep(1); + + } catch (Exception $e) { + echo " ❌ Ошибка: " . $e->getMessage() . "\n"; + $stats['failed']++; + } + } else { + echo " ⏸️ Будет перемещен (dry-run)\n"; + $stats['moved']++; + } + + echo "\n"; + } + + echo str_repeat("=", 80) . "\n"; + echo "ИТОГИ:\n"; + echo "Всего: {$stats['total']}\n"; + + if (!$dryRun) { + echo "Перемещено: {$stats['moved']}\n"; + echo "Пропущено: {$stats['skipped']}\n"; + echo "Ошибок: {$stats['failed']}\n"; + } else { + echo "Будет перемещено: {$stats['moved']}\n"; + } + + if (!$dryRun && $stats['moved'] > 0) { + echo "\n⚠️ ВАЖНО: Обновите БД вручную:\n"; + echo "UPDATE vtiger_notes SET s3_key = '...', filename = '...' WHERE notesid = ...;\n"; + } + + echo "\n=== ГОТОВО ===\n"; + +} catch (Exception $e) { + echo "❌ Ошибка: " . $e->getMessage() . "\n"; + exit(1); +} + diff --git a/move_project_398027.php b/move_project_398027.php new file mode 100644 index 00000000..fcc8f44c --- /dev/null +++ b/move_project_398027.php @@ -0,0 +1,165 @@ + правильный путь +$filesToMove = [ + 398030 => [ + 'old' => 'crm2/CRM_Active_Files/Documents/398030/8_Договор_на_оказание_услуг_11-14-2025-16-00-51_Храмов_1_CTP#realfile.pdf', + 'new' => $projectPrefix . '8_Договор_на_оказание_услуг_398030.pdf', + ], + 398032 => [ + 'old' => 'crm2/CRM_Active_Files/Documents/398032/9_Подтверждение_оплаты_по_договору_11-14-2025-16-00-03_Храмов_1_CTP#realfile.pdf', + 'new' => $projectPrefix . '9_Подтверждение_оплаты_по_договору_398032.pdf', + ], + 398034 => [ + 'old' => 'crm2/CRM_Active_Files/Documents/398034/10_2_Скрин_личного_кабинета_Истца_и_программа_обучения_11-14-2025-15-47-26_Храмов_41_CTP#realfile.pdf', + 'new' => $projectPrefix . '10_2_Скрин_личного_кабинета_Истца_и_программа_обучения_398034.pdf', + ], + 398036 => [ + 'old' => 'crm2/CRM_Active_Files/Documents/398036/10_1_Скрин_личного_кабинета_Истца_и_программа_обучения_11-14-2025-15-49-59_Храмов_1_CTP#realfile.pdf', + 'new' => $projectPrefix . '10_1_Скрин_личного_кабинета_Истца_и_программа_обучения_398036.pdf', + ], + 398038 => [ + 'old' => 'crm2/CRM_Active_Files/Documents/398038/Прочие_документы_11-14-2025-16-06-07_Храмов_3_CTP#realfile.pdf', + 'new' => $projectPrefix . 'Прочие_документы_398038.pdf', + ], + 398040 => [ + 'old' => 'crm2/CRM_Active_Files/Documents/398040/7_zayavlenie_potrebitelya_Hramov.pdf', + 'new' => $projectPrefix . '7_заявление_потребителя_398040.pdf', + ], + 398063 => [ + 'old' => 'crm2/CRM_Active_Files/Documents/398063/napravleniya_pretenzii.pdf', + 'new' => $projectPrefix . 'Направление_претензии_398063.pdf', + ], + 399067 => [ + 'old' => 'clientright/0/1763997676315.pdf', + 'new' => $projectPrefix . 'Документ_399067.pdf', + ], + 399068 => [ + 'old' => 'clientright/0/1763997790309.pdf', + 'new' => $projectPrefix . 'Документ_399068.pdf', + ], +]; + +echo "=== ПЕРЕМЕЩЕНИЕ ФАЙЛОВ ПРОЕКТА {$projectId} ===\n"; +echo str_repeat("=", 80) . "\n\n"; + +$dryRun = isset($argv[1]) && $argv[1] === '--dry-run'; + +if ($dryRun) { + echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run)\n\n"; +} + +try { + $s3Client = new \Aws\S3\S3Client([ + 'version' => 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + $stats = [ + 'total' => count($filesToMove), + 'moved' => 0, + 'skipped' => 0, + 'failed' => 0, + ]; + + foreach ($filesToMove as $docId => $paths) { + $oldPath = $paths['old']; + $newPath = $paths['new']; + $filename = basename($newPath); + + echo "Документ ID: {$docId}\n"; + echo " Файл: {$filename}\n"; + echo " От: {$oldPath}\n"; + echo " К: {$newPath}\n"; + + // Проверяем существование старого файла + if (!$s3Client->doesObjectExist($s3Bucket, $oldPath)) { + echo " ⚠️ Старый файл не найден\n\n"; + $stats['skipped']++; + continue; + } + + // Проверяем, не существует ли уже новый файл + if ($s3Client->doesObjectExist($s3Bucket, $newPath)) { + echo " ⚠️ Новый файл уже существует\n\n"; + $stats['skipped']++; + continue; + } + + if (!$dryRun) { + try { + // Копируем файл + $s3Client->copyObject([ + 'Bucket' => $s3Bucket, + 'Key' => $newPath, + 'CopySource' => "{$s3Bucket}/{$oldPath}", + ]); + echo " ✅ Скопирован\n"; + + // Удаляем старый + $s3Client->deleteObject([ + 'Bucket' => $s3Bucket, + 'Key' => $oldPath, + ]); + echo " ✅ Старый удален\n"; + + $stats['moved']++; + sleep(1); + + } catch (Exception $e) { + echo " ❌ Ошибка: " . $e->getMessage() . "\n"; + $stats['failed']++; + } + } else { + echo " ⏸️ Будет перемещен (dry-run)\n"; + $stats['moved']++; + } + + echo "\n"; + } + + // Итоги + echo str_repeat("=", 80) . "\n"; + echo "ИТОГИ:\n"; + echo "Всего файлов: {$stats['total']}\n"; + + if (!$dryRun) { + echo "Перемещено: {$stats['moved']}\n"; + echo "Пропущено: {$stats['skipped']}\n"; + echo "Ошибок: {$stats['failed']}\n"; + + if ($stats['moved'] > 0) { + echo "\n⚠️ ВАЖНО: Обновите БД с правильными путями!\n"; + } + } else { + echo "Будет перемещено: {$stats['moved']}\n"; + } + + echo "\n=== ГОТОВО ===\n"; + +} catch (Exception $e) { + echo "❌ Критическая ошибка: " . $e->getMessage() . "\n"; + exit(1); +} + + diff --git a/nextcloud_scan_files.sh b/nextcloud_scan_files.sh new file mode 100755 index 00000000..0756faba --- /dev/null +++ b/nextcloud_scan_files.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Скрипт для регулярной индексации файлов Nextcloud +# Запускать каждые 6 часов через cron + +CONTAINER_NAME="nextcloud-fresh" +USER="www-data" + +# Сканируем все файлы +docker exec -u $USER $CONTAINER_NAME php occ files:scan --all >> /var/log/nextcloud_scan.log 2>&1 + +# Сканируем только внешнее хранилище (быстрее) +# docker exec -u $USER $CONTAINER_NAME php occ files:scan --path="/crm" >> /var/log/nextcloud_scan.log 2>&1 + +echo "$(date): Nextcloud files scan completed" >> /var/log/nextcloud_scan.log \ No newline at end of file diff --git a/restore_all_deleted_files.php b/restore_all_deleted_files.php new file mode 100755 index 00000000..53f170dc --- /dev/null +++ b/restore_all_deleted_files.php @@ -0,0 +1,274 @@ + 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + $stats = [ + 'total_markers' => 0, + 'restored' => 0, + 'failed' => 0, + 'skipped' => 0, + 'errors' => [] + ]; + + $processedKeys = []; // Для отслеживания уже обработанных ключей + + echo "Поиск delete markers в префиксе: $prefix\n"; + echo "Ограничение: " . ($limit ? "$limit файлов" : "нет") . "\n\n"; + + $isTruncated = true; + $continuationToken = null; + $pageCount = 0; + $maxPages = isset($argv[4]) ? (int)$argv[4] : 10; // БЕЗОПАСНО: максимум 10 страниц по умолчанию (можно увеличить через параметр) + + echo "⚠️ ВНИМАНИЕ: Обработка ограничена {$maxPages} страницами для безопасности\n"; + echo " Для обработки большего количества используйте: php restore_all_deleted_files.php [--dry-run] [limit] [prefix] [maxPages]\n\n"; + + while ($isTruncated && $pageCount < $maxPages && (!$limit || $stats['restored'] + $stats['failed'] < $limit)) { + $params = [ + 'Bucket' => $s3Bucket, + 'Prefix' => $prefix, + 'MaxKeys' => 100 // БЕЗОПАСНО: уменьшено с 1000 до 100 для снижения нагрузки + ]; + + if ($continuationToken) { + $params['ContinuationToken'] = $continuationToken; + } + + echo "Обработка страницы " . ($pageCount + 1) . "/{$maxPages}...\r"; + + try { + $versions = $s3Client->listObjectVersions($params); + $pageCount++; + + // БЕЗОПАСНОСТЬ: пауза между страницами для снижения нагрузки + if ($pageCount < $maxPages && $isTruncated) { + usleep(500000); // 0.5 секунды пауза между страницами + } + + if (isset($versions['DeleteMarkers']) && !empty($versions['DeleteMarkers'])) { + foreach ($versions['DeleteMarkers'] as $marker) { + $key = $marker['Key']; + $versionId = $marker['VersionId']; + $deleteDate = $marker['LastModified'] ?? 'не указана'; + + // Пропускаем, если уже обработали этот ключ + if (isset($processedKeys[$key])) { + continue; + } + + $stats['total_markers']++; + + // Проверяем лимит + if ($limit && ($stats['restored'] + $stats['failed']) >= $limit) { + break 2; // Выходим из обоих циклов + } + + // Пропускаем папки (заканчиваются на /) + if (substr($key, -1) === '/') { + $stats['skipped']++; + continue; + } + + try { + if (!$dryRun) { + // Сначала проверяем, есть ли версии файла + try { + $versionsList = $s3Client->listObjectVersions([ + 'Bucket' => $s3Bucket, + 'Prefix' => $key, + 'MaxKeys' => 10 + ]); + + $hasVersions = isset($versionsList['Versions']) && !empty($versionsList['Versions']); + + if ($hasVersions) { + // Есть версии - удаляем delete marker и файл восстановится автоматически + $s3Client->deleteObject([ + 'Bucket' => $s3Bucket, + 'Key' => $key, + 'VersionId' => $versionId + ]); + + // Проверяем, восстановился ли файл + try { + $headResult = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $key + ]); + + $stats['restored']++; + $processedKeys[$key] = true; + + // БЕЗОПАСНОСТЬ: пауза каждые 10 файлов + if ($stats['restored'] % 10 == 0) { + echo "Восстановлено: {$stats['restored']} файлов...\r"; + usleep(200000); // 0.2 секунды пауза + } + + } catch (\Aws\Exception\AwsException $e) { + if ($e->getAwsErrorCode() == 'NotFound') { + // Файл все еще не доступен, пробуем восстановить последнюю версию вручную + $latestVersion = $versionsList['Versions'][0]; + $latestVersionId = $latestVersion['VersionId']; + + try { + // Копируем версию в текущий объект + $s3Client->copyObject([ + 'Bucket' => $s3Bucket, + 'CopySource' => urlencode($s3Bucket . '/' . $key) . '?versionId=' . $latestVersionId, + 'Key' => $key + ]); + + $stats['restored']++; + $processedKeys[$key] = true; + + if ($stats['restored'] % 100 == 0) { + echo "Восстановлено: {$stats['restored']} файлов...\r"; + } + } catch (\Aws\Exception\AwsException $e2) { + $stats['failed']++; + $stats['errors'][] = "Ошибка восстановления версии $key: " . $e2->getMessage(); + } + } else { + $stats['failed']++; + $stats['errors'][] = "Ошибка проверки $key: " . $e->getMessage(); + } + } + } else { + // Нет версий - файл удален безвозвратно + $stats['failed']++; + $stats['errors'][] = "Не найдена версия для восстановления: $key (файл удален безвозвратно)"; + } + + } catch (\Aws\Exception\AwsException $e) { + $stats['failed']++; + $stats['errors'][] = "Ошибка проверки версий для $key: " . $e->getMessage(); + } + } else { + // Dry-run режим - проверяем наличие версий + try { + $versionsList = $s3Client->listObjectVersions([ + 'Bucket' => $s3Bucket, + 'Prefix' => $key, + 'MaxKeys' => 1 + ]); + + if (isset($versionsList['Versions']) && !empty($versionsList['Versions'])) { + $stats['restored']++; + } else { + $stats['failed']++; + } + $processedKeys[$key] = true; + + if (($stats['restored'] + $stats['failed']) % 100 == 0) { + echo "Проверено: " . ($stats['restored'] + $stats['failed']) . " файлов...\r"; + } + } catch (\Aws\Exception\AwsException $e) { + $stats['failed']++; + $processedKeys[$key] = true; + } + } + + } catch (\Aws\Exception\AwsException $e) { + $stats['failed']++; + $stats['errors'][] = "Ошибка удаления delete marker для $key: " . $e->getMessage(); + + if (count($stats['errors']) <= 10) { + echo "\n Ошибка: {$stats['errors'][count($stats['errors']) - 1]}\n"; + } + } + } + } + + $isTruncated = isset($versions['IsTruncated']) && $versions['IsTruncated']; + $continuationToken = isset($versions['NextContinuationToken']) ? $versions['NextContinuationToken'] : null; + + if (!$isTruncated) { + break; + } + + } catch (\Aws\Exception\AwsException $e) { + echo "\n❌ Ошибка при обработке страницы: " . $e->getMessage() . "\n"; + break; + } + } + + echo "\n\n"; + echo str_repeat("=", 80) . "\n"; + echo "ИТОГОВЫЙ ОТЧЕТ:\n\n"; + + echo "Всего найдено delete markers: {$stats['total_markers']}\n"; + echo "Восстановлено файлов: {$stats['restored']}\n"; + echo "Ошибок: {$stats['failed']}\n"; + echo "Пропущено (папки): {$stats['skipped']}\n\n"; + + if (!empty($stats['errors']) && count($stats['errors']) <= 20) { + echo "Ошибки (первые " . count($stats['errors']) . "):\n"; + foreach ($stats['errors'] as $error) { + echo " - $error\n"; + } + echo "\n"; + } elseif (!empty($stats['errors'])) { + echo "Всего ошибок: " . count($stats['errors']) . " (показаны первые 20)\n"; + foreach (array_slice($stats['errors'], 0, 20) as $error) { + echo " - $error\n"; + } + echo "\n"; + } + + if ($dryRun) { + echo "⚠️ Это был режим проверки. Для реального восстановления запустите:\n"; + echo " php restore_all_deleted_files.php\n\n"; + } else { + echo "✅ Восстановление завершено!\n"; + echo " Проверьте доступность файлов в интерфейсе CRM\n\n"; + } + + // Сохраняем статистику в файл + $logFile = '/var/www/fastuser/data/www/crm.clientright.ru/restore_log_' . date('Y-m-d_H-i-s') . '.json'; + file_put_contents($logFile, json_encode($stats, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + echo "📝 Лог сохранен в: $logFile\n"; + +} catch (Exception $e) { + echo "❌ КРИТИЧЕСКАЯ ОШИБКА: " . $e->getMessage() . "\n"; + echo "Trace: " . $e->getTraceAsString() . "\n"; +} + diff --git a/restore_deleted_files.php b/restore_deleted_files.php new file mode 100644 index 00000000..e42fd525 --- /dev/null +++ b/restore_deleted_files.php @@ -0,0 +1,163 @@ + 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + foreach ($docIds as $docId) { + echo "Восстановление файла для документа $docId:\n"; + echo str_repeat("-", 80) . "\n"; + + // Получаем информацию о документе + $pdo = new PDO( + "mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8", + $dbconfig['db_username'], + $dbconfig['db_password'], + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); + + $stmt = $pdo->prepare('SELECT notesid, title, s3_key, s3_etag FROM vtiger_notes WHERE notesid = ?'); + $stmt->execute([$docId]); + $doc = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$doc) { + echo " Документ не найден в БД\n\n"; + continue; + } + + $s3Key = $doc['s3_key']; + echo " Название: {$doc['title']}\n"; + echo " Путь: $s3Key\n\n"; + + // Получаем delete markers + try { + $versions = $s3Client->listObjectVersions([ + 'Bucket' => $s3Bucket, + 'Prefix' => $s3Key, + 'MaxKeys' => 100 + ]); + + if (isset($versions['DeleteMarkers']) && !empty($versions['DeleteMarkers'])) { + echo " Найдено delete markers: " . count($versions['DeleteMarkers']) . "\n"; + + // Удаляем все delete markers (самый новый будет удален последним) + // Сортируем по дате удаления (от новых к старым) + usort($versions['DeleteMarkers'], function($a, $b) { + $dateA = isset($a['LastModified']) ? strtotime($a['LastModified']) : 0; + $dateB = isset($b['LastModified']) ? strtotime($b['LastModified']) : 0; + return $dateB - $dateA; // От новых к старым + }); + + foreach ($versions['DeleteMarkers'] as $marker) { + $versionId = $marker['VersionId']; + $deleteDate = $marker['LastModified'] ?? 'не указана'; + + echo " Удаление delete marker: VersionId=$versionId (дата удаления: $deleteDate)\n"; + + try { + $s3Client->deleteObject([ + 'Bucket' => $s3Bucket, + 'Key' => $s3Key, + 'VersionId' => $versionId + ]); + + echo " ✅ Delete marker удален\n"; + } catch (\Aws\Exception\AwsException $e) { + echo " ❌ Ошибка при удалении delete marker: " . $e->getMessage() . "\n"; + } + } + + // Проверяем, восстановился ли файл + echo "\n Проверка восстановления файла...\n"; + try { + $headResult = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $s3Key + ]); + + echo " ✅ Файл успешно восстановлен!\n"; + echo " Размер: " . number_format($headResult['ContentLength'] / 1024, 2) . " KB\n"; + echo " ETag: " . (isset($headResult['ETag']) ? trim($headResult['ETag'], '"') : 'не указан') . "\n"; + echo " Дата: " . ($headResult['LastModified'] ?? 'не указана') . "\n"; + + } catch (\Aws\Exception\AwsException $e) { + if ($e->getAwsErrorCode() == 'NotFound') { + echo " ⚠️ Файл все еще не доступен (возможно, нужно восстановить конкретную версию)\n"; + + // Пробуем восстановить последнюю версию напрямую + if (isset($versions['Versions']) && !empty($versions['Versions'])) { + $latestVersion = $versions['Versions'][0]; // Первая версия - самая новая + $versionId = $latestVersion['VersionId']; + + echo " Попытка восстановить версию: VersionId=$versionId\n"; + + try { + // Копируем версию в текущий объект + $s3Client->copyObject([ + 'Bucket' => $s3Bucket, + 'CopySource' => $s3Bucket . '/' . $s3Key . '?versionId=' . $versionId, + 'Key' => $s3Key + ]); + + echo " ✅ Версия восстановлена\n"; + + // Проверяем еще раз + $headResult = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $s3Key + ]); + + echo " ✅ Файл восстановлен и доступен!\n"; + echo " Размер: " . number_format($headResult['ContentLength'] / 1024, 2) . " KB\n"; + + } catch (\Aws\Exception\AwsException $e) { + echo " ❌ Ошибка при восстановлении версии: " . $e->getMessage() . "\n"; + } + } + } else { + echo " ❌ Ошибка при проверке: " . $e->getMessage() . "\n"; + } + } + + } else { + echo " ❌ Delete markers не найдены\n"; + } + + } catch (\Aws\Exception\AwsException $e) { + echo " ❌ Ошибка при работе с версиями: " . $e->getMessage() . "\n"; + } + + echo "\n"; + } + + echo str_repeat("=", 80) . "\n"; + echo "Восстановление завершено!\n"; + echo "Проверьте доступность файлов в интерфейсе CRM\n"; + +} catch (Exception $e) { + echo "ОШИБКА: " . $e->getMessage() . "\n"; + echo "Trace: " . $e->getTraceAsString() . "\n"; +} + diff --git a/restore_log_2025-11-25_10-20-37.json b/restore_log_2025-11-25_10-20-37.json new file mode 100644 index 00000000..00694492 --- /dev/null +++ b/restore_log_2025-11-25_10-20-37.json @@ -0,0 +1,7 @@ +{ + "total_markers": 11, + "restored": 10, + "failed": 0, + "skipped": 0, + "errors": [] +} \ No newline at end of file diff --git a/restore_log_2025-11-25_10-21-27.json b/restore_log_2025-11-25_10-21-27.json new file mode 100644 index 00000000..064b2966 --- /dev/null +++ b/restore_log_2025-11-25_10-21-27.json @@ -0,0 +1,7 @@ +{ + "total_markers": 51, + "restored": 50, + "failed": 0, + "skipped": 0, + "errors": [] +} \ No newline at end of file diff --git a/restore_log_2025-11-25_10-23-09.json b/restore_log_2025-11-25_10-23-09.json new file mode 100644 index 00000000..cb84d837 --- /dev/null +++ b/restore_log_2025-11-25_10-23-09.json @@ -0,0 +1,550 @@ +{ + "total_markers": 542, + "restored": 0, + "failed": 542, + "skipped": 0, + "errors": [ + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1149\/Оферта_август_2022_(1)_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1151\/inquiry.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1153\/Screen.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117066\/operation_statement_05.04.2024.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117081\/Заявление_о_выдачи_исполнительного_листа_по_делу_2-1658-2023_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117335\/Договор_04-21-2024-19-49-06_Мустафаев_21_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117337\/Подтверждение_оплаты_04-21-2024-19-41-17_Мустафаев_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117339\/Скриншот_прогресса_обучения_04-21-2024-19-48-13_Мустафаев_7_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117341\/Претензия_04-21-2024-19-56-37_Мустафаев_2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117343\/Ответ_на_претензию_04-21-2024-19-57-51_Мустафаев_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117597\/Договор_04-23-2024-12-18-54_Гришакова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117599\/Подтверждение_оплаты_04-23-2024-12-20-02_Гришакова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117601\/Скриншот_прогресса_обучения_04-23-2024-12-23-43_Гришакова__5_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117603\/Претензия_04-23-2024-12-28-08_Гришакова__2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117605\/Ответ_на_претензию_04-23-2024-12-28-30_Гришакова__2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117607\/Прочие_документы_04-23-2024-12-34-20_Гришакова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117649\/Договор_04-23-2024-14-32-10_Жидкова__12_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117651\/Подтверждение_оплаты_04-23-2024-14-34-01_Жидкова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117653\/Программа_обучения_04-23-2024-14-34-39_Жидкова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117655\/Скриншот_прогресса_обучения_04-23-2024-14-34-47_Жидкова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117660\/Выписка_из_ГРАФПИЮЛ.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117824\/от_скилл_бокс.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117863\/Сканирование_20240425-1902.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117947\/Ходатайство_по_делу__1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118166\/возврат.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118260\/Договор_04-26-2024-17-05-28_Енин__21_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118262\/Подтверждение_оплаты_04-26-2024-17-07-56_Енин__2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118264\/Скриншот_прогресса_обучения_04-26-2024-17-12-16_Енин__3_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118266\/Претензия_04-26-2024-17-17-30_Енин__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118268\/Ответ_на_претензию_04-26-2024-17-13-49_Енин__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118270\/возвраты.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118328\/Счёт_№9109027.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118331\/чек_27.04.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118337\/Договор_04-27-2024-23-53-19_Авдеева__19_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118339\/Подтверждение_оплаты_04-27-2024-23-54-03_Авдеева__3_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118341\/Программа_обучения_04-27-2024-23-55-28_Авдеева__10_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118343\/Скриншот_прогресса_обучения_04-27-2024-23-56-39_Авдеева__10_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118345\/Прочие_документы_04-28-2024-00-00-46_Авдеева__10_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118365\/Договор_04-30-2024-13-16-36_Салиев_14_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118367\/Подтверждение_оплаты_04-30-2024-13-17-19_Салиев_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118369\/Программа_обучения_04-30-2024-13-26-10_Салиев_18_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118371\/Скриншот_прогресса_обучения_04-30-2024-13-22-19_Салиев_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118469\/Выписка_по_вкладу_(на_русском).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118586\/Полис_05-02-2024-20-59-43__1_CTP.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118589\/Подтверждение_бронирования_05-02-2024-21-05-13_Зарубина_19_CTP.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118592\/Подтверждение_оплаты_бронирования_05-02-2024-21-05-12_Зарубина_1_CTP.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118595\/Отказ_в_заселении_05-02-2024-21-06-50_Зарубина_2_CTP.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118598\/Отсутствие_вида_05-02-2024-21-06-30_Зарубина_1_CTP.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118601\/Документ_удостоверяющий_личность_05-02-2024-21-12-05_Зарубина_1_CTP.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118612\/Протокол встречи.txt", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118738\/Договор_05-05-2024-16-17-26_Бережной_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118740\/Подтверждение_оплаты_05-05-2024-16-13-09_Бережной_15_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118742\/Скриншот_прогресса_обучения_05-05-2024-16-36-04_Бережной_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118828\/Акт_за_апрель.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118831\/Счет_за_май+_четверть_модуля.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118847\/Binder1_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118903\/Заявление_в_АО_-ТИНЬКОФФ_БАНК-_об_исполнении_решения_по_делу_2-244-2024_Клиентправ.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118905\/Бордеро_Апрель_2024.xlsx", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118937\/6_Расчет_исковых_требований_Григоричев__ИП_Айриян_Арам_Ашотович_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118939\/0_Исковое_заявление_по_делу_Григоричев__ИП_Айриян_Арам_Ашотович_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118987\/Договор_05-07-2024-12-48-52_Новичков_3_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118989\/Подтверждение_оплаты_05-07-2024-12-49-12_Новичков_3_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118991\/Скриншот_прогресса_обучения_05-07-2024-12-50-06_Новичков_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1190\/Договор.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119017\/Заявление_о_выдачи_исполнительного_листа_по_делу_2-817-2024_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119032\/Опись_104807.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119052\/доказательство_направления_иска_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119088\/Договор_05-07-2024-17-27-35_Удовикина_12_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119090\/Подтверждение_оплаты_05-07-2024-17-26-35_Удовикина_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119092\/Программа_обучения_05-07-2024-17-25-41_Удовикина_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119094\/Скриншот_прогресса_обучения_05-07-2024-17-25-17_Удовикина_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119103\/Договор_05-07-2024-17-56-21_Захарова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119105\/Подтверждение_оплаты_05-07-2024-17-58-27_Захарова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119107\/Программа_обучения_05-07-2024-17-52-07_Захарова__13_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119109\/Скриншот_прогресса_обучения_05-07-2024-17-53-11_Захарова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119111\/Претензия_05-07-2024-18-04-05_Захарова__2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119113\/Ответ_на_претензию_05-07-2024-18-11-10_Захарова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119123\/Ходатайство_по_делу_М-1132-2024_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119126\/0_Исковое_заявление_по_делу_Гусев__ООО_-СКИЛБОКС-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119128\/6_Расчет_исковых_требований_Гусев__ООО_-СКИЛБОКС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119138\/Ходатайство_по_делу_М-538-2024_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1192\/платеж.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119259\/Договор_05-08-2024-16-49-53_Панина_12_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119261\/Подтверждение_оплаты_05-08-2024-16-50-13_Панина_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119263\/Программа_обучения_05-08-2024-16-50-36_Панина_4_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119265\/Скриншот_прогресса_обучения_05-08-2024-16-50-58_Панина_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119267\/Претензия_05-08-2024-16-51-31_Панина_2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119269\/Ответ_на_претензию_05-08-2024-16-52-19_Панина_2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119271\/Возврат_средств_05-08-2024-16-52-07_Панина_2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1194\/Новая_папка.7z", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119409\/10_Доказательство_проведение_претензионной_работы__Исаева_Екатерина_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119449\/7_заявление_потребителя_Панина__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119465\/7_заявление_потребителя_Бережной__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119583\/Заявление_о_выдачи_исполнительного_листа_по_делу_02-11106-2023_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119598\/IMG_3814.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1196\/Общение_с_представителями_школы.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119613\/Пдф_для_суда.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119623\/12доказательство_направления_иска.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119641\/6_Расчет_исковых_требований_Носенко__ООО_-СКИЛБОКС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119643\/0_Исковое_заявление_по_делу_Носенко__ООО_-СКИЛБОКС-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119653\/Опись_104863_merged.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119719\/Adobe_Scan_23_апр._2024 г..pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119756\/приказ_10.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119759\/Договор_подряда__ООО_Лира.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119762\/приказ_9.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119765\/приказ_Гринчук.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119768\/ТД_художник_дизайнер.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119771\/ТД_рук_отд_продаж.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119774\/ДИ_рук_отд_продаж.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119819\/6_Расчет_исковых_требований_Кириллов___ИП_Ваняшин_Андрей_Эдуардович_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119821\/0_Исковое_заявление_по_делу_Кириллов___ИП_Ваняшин_Андрей_Эдуардович_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119831\/Опись+квитанция_об_оплате_105029.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119847\/Заявление_в_АО_-ТИНЬКОФФ_БАНК-_об_исполнении_решения_по_делу_2-1658-2023_Клиентправ.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119851\/Заявление_в_АО_-ТИНЬКОФФ_БАНК-_об_исполнении_решения_по_делу_2-1419-2023_Клиентправ.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119958\/10_Доказательство_проведение_претензионной_работы__Коржилова__Наталья__2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119970\/10_Доказательство_проведение_претензионной_работы__Мацак_Елена_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119982\/10_Доказательство_проведение_претензионной_работы__Камаш_Александра_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120006\/10_Доказательство_проведение_претензионной_работы__Чубик_Дарья_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120030\/10_Доказательство_проведение_претензионной_работы__Силина_Виктория_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120079\/Опись_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120087\/6_Расчет_исковых_требований_Мурин__ЧОУ_ДПО_-ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_-СКИЛБОКС_(КОРОБКА_НАВЫКОВ)-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120089\/0_Исковое_заявление_по_делу_Мурин__ЧОУ_ДПО_-ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_-СКИЛБОКС_(КОРОБКА_НАВЫКОВ)-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120148\/Письмо_ИП_Будишевский-2.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120195\/отказ.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120198\/прогресс.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120201\/программа_курса.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120239\/7_заявление_потребителя_Дё__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120242\/Договор_05-14-2024-14-12-21_Дё_13_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120245\/Подтверждение_оплаты_05-14-2024-14-22-29_Дё_5_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120248\/Прочие_документы_05-14-2024-14-53-09_Дё_2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120251\/Скриншот_прогресса_обучения_05-14-2024-14-25-25_Дё_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120259\/Договор_05-14-2024-13-17-36_Плохова__12_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120262\/Подтверждение_оплаты_05-14-2024-17-21-09_Плохова__2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120265\/Скриншот_прогресса_обучения_05-14-2024-14-50-09_Плохова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120269\/7_заявление_потребителя_Плохова___.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120319\/0_Исковое_заявление_по_делу_Акулов_ООО_-ЭВОТРЕН-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120321\/6_Расчет_исковых_требований_Акулов_ООО_-ЭВОТРЕН-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120350\/Договор_05-14-2024-20-27-08_Трубнякова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120352\/Подтверждение_оплаты_05-14-2024-20-34-20_Трубнякова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120354\/Программа_обучения_05-14-2024-20-40-01_Трубнякова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120356\/Скриншот_прогресса_обучения_05-14-2024-20-43-20_Трубнякова__7_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120358\/Претензия_05-14-2024-20-53-35_Трубнякова__3_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120360\/Ответ_на_претензию_05-14-2024-20-56-00_Трубнякова__7_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120368\/7_заявление_потребителя_Трубнякова___.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120395\/7_заявление_потребителя_Новичков_.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120397\/11_Доказательство_соблюдения_претензионного_порядка__Новичков_Константин_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120413\/Заявление_в_АО_-ТИНЬКОФФ_БАНК-_об_исполнении_решения_по_делу_02-1320-44-2023_Клиентправ.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120437\/отправка_претензии_почтой.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120479\/2024-05-04-2.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120484\/2024-05-04-1.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120491\/Договор_05-15-2024-15-11-47_Тупас__17_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120493\/Подтверждение_оплаты_05-15-2024-15-12-30_Тупас__2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120495\/Прочие_документы_05-15-2024-15-14-02_Тупас__25_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120724\/7_заявление_потребителя_Самуткина__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120728\/Договор_05-16-2024-12-02-31_Самуткина__11_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120730\/Подтверждение_оплаты_05-16-2024-12-03-28_Самуткина__8_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120732\/Программа_обучения_05-16-2024-12-04-07_Самуткина__4_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120734\/Скриншот_прогресса_обучения_05-16-2024-12-04-28_Самуткина__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120736\/Претензия_05-16-2024-12-07-09_Самуткина__2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120738\/Ответ_на_претензию_05-16-2024-12-07-19_Самуткина__2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120740\/Прочие_документы_05-16-2024-13-03-51_Самуткина__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120754\/Заявление_истца_на_выдачу_листа_по_делу_02-4298-345-2023_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120787\/7_заявление_потребителя_Мустафаев_.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120796\/7_заявление_потребителя_Захарова__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120803\/Договор_05-16-2024-15-11-45_Кузовлева__6_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120805\/Подтверждение_оплаты_05-16-2024-15-13-53_Кузовлева__3_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120807\/Прочие_документы_05-16-2024-15-14-08_Кузовлева__20_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120809\/Претензия_и_документы_05-16-2024-15-15-45_Кузовлева__2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120811\/Возврат_средств_контрагентом_05-16-2024-15-17-21_Кузовлева__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120865\/11._Подтверждение_проведения_претензионной_работы_Захарова__Наталья__2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120877\/11._Подтверждение_проведения_претензионной_работы_Самуткина__Ксения_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120889\/7_заявление_потребителя_Сарина__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120895\/document_(2).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120897\/Скриншот_прогресса_обучения_05-16-2024-22-44-55_Сарина__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120899\/Претензия_05-16-2024-22-48-30_Сарина__25_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120901\/Ответ_на_претензию_05-16-2024-22-48-55_Сарина__3_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120924\/11._Подтверждение_проведения_претензионной_работы_Дё_Виктория_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120968\/Договор_05-17-2024-12-03-41_Зозуля__14_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120970\/Подтверждение_оплаты_05-17-2024-12-10-43_Зозуля__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120972\/Прочие_документы_05-17-2024-12-02-05_Зозуля__23_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120978\/Почта-суд.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120996\/11._Подтверждение_проведения_претензионной_работы_Сарина__Людмила__2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/121032\/11._Подтверждение_проведения_претензионной_работы_Трубнякова__Дарья__2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/121037\/Ответ_на_претензию_04-24-2024-12-31-08_Окуньков_3_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/121057\/11._Подтверждение_проведения_претензионной_работы_Мустафаев_Энвер_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/121140\/7_заявление_потребителя_Балашов_.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/121148\/11._Подтверждение_проведения_претензионной_работы_Балашов_Александр_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/121160\/7_заявление_потребителя_Жидкова__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/121177\/11._Подтверждение_проведения_претензионной_работы_Жидкова__Ольга__2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1216\/договор.rar", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1218\/оплата.rar", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1220\/личный_кабинет.rar", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1234\/Заявление_(2).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123567\/7_заявление_потребителя_Матвеев_.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123571\/Договор_05-18-2024-01-44-40_Матвеев_12_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123573\/Подтверждение_оплаты_05-17-2024-21-06-55_Матвеев_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123575\/Скриншот_прогресса_обучения_05-17-2024-18-17-22_Матвеев_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123617\/отправка_иска_почтой.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123620\/Пользовательское-соглашение-в-ред.-от-12.07.22-(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123623\/лк_медведева.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123626\/подтверждение_оплаты_медведева.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123629\/доказательство_претензии_медведева.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123632\/претензия_медведева_2.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123658\/Опись_и_квитанция.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123723\/6_Расчет_исковых_требований_Завьялова__ЧОУ_ДПО_-ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_-СКИЛБОКС_(КОРОБКА_НАВЫКОВ)-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123725\/0_Исковое_заявление_по_делу_Завьялова__ЧОУ_ДПО_-ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_-СКИЛБОКС_(КОРОБКА_НАВЫКОВ)-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123735\/Опись_104988.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123786\/7_заявление_потребителя_Ливенцева_.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123792\/Договор_05-20-2024-16-58-35_Ливенцева_6_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123794\/Подтверждение_оплаты_05-20-2024-16-59-28_Ливенцева_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123796\/Программа_обучения_05-20-2024-16-55-35_Ливенцева_3_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123798\/Скриншот_прогресса_обучения_05-20-2024-16-58-01_Ливенцева_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123800\/Претензия_05-20-2024-17-04-57_Ливенцева_3_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123847\/7_заявление_потребителя_Гришакова__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123858\/6_Расчет_исковых_требований_Юмаев___Turkish_Airlines_(Турецкие_Авиалинии)_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123863\/0_Исковое_заявление_по_делу_Юмаев___Turkish_Airlines_(Турецкие_Авиалинии)_6_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123870\/доказательство_направления_иска_ответчику_Юмаев___Turkish_Airlines_(Турецкие_Авиалинии)_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123889\/7_заявление_потребителя_Лисовенко_.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123896\/Договор_05-20-2024-22-33-05_Лисовенко_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123898\/Подтверждение_оплаты_05-20-2024-22-33-47_Лисовенко_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123900\/Программа_обучения_05-20-2024-22-35-11_Лисовенко_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123902\/Скриншот_прогресса_обучения_05-20-2024-22-36-28_Лисовенко_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123909\/7_заявление_потребителя_Маклакова__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123915\/Договор_05-20-2024-22-40-25_Маклакова__2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123917\/Подтверждение_оплаты_05-20-2024-22-40-56_Маклакова__3_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123919\/Программа_обучения_05-20-2024-22-41-34_Маклакова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123921\/Скриншот_прогресса_обучения_05-20-2024-22-44-31_Маклакова__2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123923\/Претензия_05-20-2024-22-58-20_Маклакова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123925\/Прочие_документы_05-20-2024-22-59-48_Маклакова__3_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123931\/7_заявление_потребителя_Енин__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123948\/Опись_104985.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123975\/доказательство_направления_иска_ответчику_Березанский__Turkish_Airlines_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123992\/7_заявление_потребителя_Дорничева_.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1240\/Пользовательское_соглашение_Гикбрейнс_21.12.2021.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/124000\/Договор_05-21-2024-09-25-07_Дорничева_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/124002\/Подтверждение_оплаты_05-21-2024-09-22-53_Дорничева_2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/124004\/Скриншот_прогресса_обучения_05-21-2024-09-26-55_Дорничева_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/124006\/Претензия_05-21-2024-09-31-20_Дорничева_2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/124017\/13_судебная_практика_по_данной_категории_дел_5_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1242\/Справка_об_оплате_обучения_Петров_Илья_Павлович.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1244\/ГБ.rar", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12627\/image0.jpeg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12632\/Доказательство_проведение_претензионной_работы__Чекалин__Матвей__2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12635\/Доказательство_проведение_претензионной_работы__Черников_Александр_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126413\/7_заявление_потребителя_Кривенцова_.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126419\/Договор_05-21-2024-11-40-35_Кривенцова_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126421\/Подтверждение_оплаты_05-21-2024-11-38-24_Кривенцова_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126423\/Программа_обучения_05-21-2024-11-40-46_Кривенцова_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126425\/Скриншот_прогресса_обучения_05-21-2024-11-40-54_Кривенцова_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126441\/Скриншот_прогресса_обучения_05-18-2024-13-19-17_Сальников__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126444\/Подтверждение_оплаты_05-18-2024-13-16-56_Сальников__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126447\/Договор_05-18-2024-13-14-04_Сальников__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126450\/7_заявление_потребителя_Сальников__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126469\/11._Подтверждение_проведения_претензионной_работы_Сальников__Андрей__2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126478\/7_заявление_потребителя_Федорова_.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12648\/_8.0_9.0_Оферта_Команда_Александра_Никитина_РФ.docx.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126484\/Договор_05-21-2024-12-13-49_Федорова_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126486\/Подтверждение_оплаты_05-21-2024-12-14-49_Федорова_2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126488\/Скриншот_прогресса_обучения_05-21-2024-12-15-48_Федорова_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12650\/0040045670_20230128.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126510\/11._Подтверждение_проведения_претензионной_работы_Кривенцова_Татьяна_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12652\/2023-06-16_12-26-08.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126523\/11._Подтверждение_проведения_претензионной_работы_Гришакова__Светлана__2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126535\/7_заявление_потребителя_Бекназарова_.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12654\/Претензия.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126541\/Договор_05-21-2024-13-17-31_Бекназарова_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126543\/Подтверждение_оплаты_05-21-2024-13-14-32_Бекназарова_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126545\/Скриншот_прогресса_обучения_05-21-2024-13-16-04_Бекназарова_1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126564\/7_заявление_потребителя_минеева__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126570\/Договор_05-21-2024-14-33-17_минеева__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126572\/Подтверждение_оплаты_05-21-2024-14-33-54_минеева__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126574\/Программа_обучения_05-21-2024-14-35-31_минеева__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126576\/Скриншот_прогресса_обучения_05-21-2024-14-36-26_минеева__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126582\/120885_Опись_104925_merged.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126585\/120959_Ответ_от_2024—05—17_в_11.55.11.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126616\/7_заявление_потребителя_Еловиков__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126621\/Договор_05-21-2024-15-34-42_Еловиков__11_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126623\/Подтверждение_оплаты_05-21-2024-15-43-32_Еловиков__2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126625\/Скриншот_прогресса_обучения_05-21-2024-15-33-35_Еловиков__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126644\/7_заявление_потребителя_Попов__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126649\/Договор_05-21-2024-15-58-51_Попов__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126651\/Подтверждение_оплаты_05-21-2024-16-10-56_Попов__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126653\/Программа_обучения_05-21-2024-16-05-10_Попов__2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126655\/Скриншот_прогресса_обучения_05-21-2024-15-53-06_Попов__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126657\/Претензия_05-21-2024-16-23-23_Попов__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126659\/Ответ_на_претензию_05-21-2024-16-22-47_Попов__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12685\/Оферта_август_2022_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12687\/оплата.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12689\/12_Прогресс_обучения,_на_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12698\/Руслан_договор_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12700\/photo_1_2023-06-16_22-07-13.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12702\/photo_3_2023-06-16_22-07-13.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12709\/Доказательство_проведение_претензионной_работы__Селедцова_Любовь_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12718\/ТАСПИН_ОФЕРТА.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12720\/check_0006124557045916_7281440500284613_176023_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12722\/лк.rar", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12724\/таспин_суд.rar", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12726\/kred_dogovor.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12728\/dosud_pretenziya.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12737\/оферта.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12739\/оплата_Лайк.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12741\/Чат_Марафон_инвестиций.PNG", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12743\/переписка_09.2021.jpeg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12779\/Kreditnyj_dogovor_individualʹnyh_uslovij_po_kreditu_(IUK).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12781\/Kreditnyj_dogovor_individualʹnyh_uslovij_po_kreditu_(IUK).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12783\/Kreditnyj_dogovor_individualʹnyh_uslovij_po_kreditu_(IUK).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12884\/Договор-оферта_Skillbox_+_доп.соглашение.7z", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12886\/operation_statement_16.09.2022.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12888\/Скрин_личного_кабинета.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12890\/подтверждение_претензионной_работы.7z", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12892\/Кассовый_чек_(возврат)_88260_рублей_от_02.12.2022.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12894\/Сообщение_Skillbox_от_14.06.2023.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12919\/заявление_в_МОО_Клиентправ.jpeg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12934\/Заявление_в_Клиентправ_Шмелев_ООО_-СКИЛБОКС-.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1294\/Заявления-Претензия.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12943\/Договор_оферты_.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12945\/Заявление_о_предоставлении_кредита_и_открытии_банковского_счёта.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12947\/Личный_кабинет_1.png", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12949\/Ответ_службы_поддержки.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12951\/Кассовый_чек_ООО_ГИКБРЕЙНС_95574_руб.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12964\/Св-во_о_перемене_имени.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12975\/oferta.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12977\/check_0004438001022024_9960440503169974_19024.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12979\/1.rar", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12981\/ответ.rar", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12983\/претензия.rar", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13028\/В_МОО_«Клиентправ»_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/130334\/Прием_РПО_внутреннее.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13038\/Доказательство_проведение_претензионной_работы__Байкова_Татьяна_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/130445\/Опись_104995.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13047\/Оферта_learnhub_08.02.2023_v1.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13049\/contract.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13051\/Снимок_экрана_2023-06-21_114806.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13053\/Документ_Microsoft_Word_(2).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13061\/Оферта_learnhub_08.02.2023_v1.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13063\/contract.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13065\/Снимок_экрана_2023-06-21_114806.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13067\/photo_2023-06-21_12-14-28.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13069\/Документ_Microsoft_Word_(2).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13082\/SKMBT_C224e23061723020.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13086\/доказательство_направления_иска_ответчику_Носков_ООО_-ГИКБРЕИНС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13121\/oferta.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13123\/check_0006185384006725_7281440500562238_4211.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13125\/Мое_обучение_—_Skillbox.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13190\/лицензия.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13200\/WhatsApp_Image_2023-06-21_at_20.11.48.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13202\/WhatsApp_Image_2023-06-21_at_20.12.10.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13204\/WhatsApp_Image_2023-06-21_at_20.12.04.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13211\/Оферта_learnhub_08.02.2023_v1_(2).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13213\/c0e78194-be00-4190-bdc5-d843d43f6ada_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13215\/скрины_с_платформы.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13222\/Оферта_learnhub_08.02.2023_v1.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13224\/sber_statement_21-06-2023_23-52-15.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13226\/Screenshot_20230621-235424_iSpring_Learn.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13228\/Screenshot_20230622-000404_Telegram.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13239\/Доказательство_проведение_претензионной_работы__Хименко_Анна_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13242\/Доказательство_проведение_претензионной_работы__Шмелев_Руслан_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13245\/Доказательство_проведение_претензионной_работы__Сулейменова_Дина_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13348\/доказательство_направления_иска_ответчику_Виноградов_ООО_-СКИЛБОКС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13350\/0_Исковое_заявление_по_делу_Виноградов_ООО_-СКИЛБОКС-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13352\/2_Расчет_исковых_требований_Виноградов_ООО_-СКИЛБОКС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13360\/Претензия_в_защиту_интересов_Сопова_Алина_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13368\/Image0082_(1).JPG", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13374\/oferta.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13376\/Screenshot_20230623_101840_Gmail.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13378\/Screenshot_20230623_101551_Chrome.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13380\/Screenshot_20230623_101528_Chrome.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13387\/Договор_ОТП.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13389\/Договор_ОТП.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13391\/Деньги_под_ключ.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13398\/Договор_ОТП.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13400\/Договор_ОТП.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13402\/Деньги_под_ключ.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13412\/doc01781120220725125515.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13414\/Заявление_о_предоставлении_кредита_и_открытии_банковского_счёта_(экземпляр_банка).PDF", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13416\/ЛК.PNG", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13425\/oferta.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13427\/photo1687688292.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13429\/изображение_2023-06-25_162046444.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13436\/Договор.rar", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13438\/Downloads.rar", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13440\/скрин_.rar", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1348\/Пользовательское_соглашение_в_ред._от_12.07.22.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1350\/Снимок_экрана_2023-04-07_в_16.49.48.jpg.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1352\/Снимок_экрана_2023-04-07_в_16.46.13.jpg.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/135648\/10_Доказательство_проведение_претензионной_работы__Белокуров_Павел_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/135779\/11._Подтверждение_проведения_претензионной_работы_Матвеев_Иван_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/135797\/11._Подтверждение_проведения_претензионной_работы_Еловиков__Матвей__2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/135901\/Заявление_о_выдачи_исполнительного_листа_по_делу_2-892-2024_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1361\/Мягкий_Вячеслав_Эдуардович.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1363\/check_0004438001022024_9287440300779803_69531.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136455\/7_заявление_потребителя_Виноградова__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136460\/Договор_05-22-2024-11-39-20_Виноградова__2_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136462\/Подтверждение_оплаты_05-22-2024-11-39-29_Виноградова__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136464\/Скриншот_прогресса_обучения_05-22-2024-11-40-02_Виноградова__3_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136470\/Mail.ru_Письмо_от_oksana.volovikova@skillbox.ru.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1365\/V3dQu8J3.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136591\/IMG_3971.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136606\/0_Исковое_заявление_по_делу_Сулиманов_ЧОУ_ДПО_-ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_-СКИЛБОКС_КОРОБКА_НАВЫКОВ-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136608\/6_Расчет_исковых_требований_Сулиманов_ЧОУ_ДПО_-ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_-СКИЛБОКС_КОРОБКА_НАВЫКОВ-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136837\/operation_statement_16.04.2024.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13797\/5_Доказательство_проведение_претензионной_работы__Галюк_Игорь_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/139911\/Опись_105005.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14044\/0_Исковое_заявление_по_делу_Сопова_ИП_Гратило_Кристина_Сергеевна_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14046\/2_Расчет_исковых_требований_Сопова_ИП_Гратило_Кристина_Сергеевна_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14058\/oferta_май_2021_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14060\/Zayavlenie_na_oplatu_tovarov_uslug.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14062\/Снимок_экрана_(3).png", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14064\/oferta_май_2023.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14107\/Доказательство_проведение_претензионной_работы__Петрова_Ольга_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14109\/Доказательство_проведение_претензионной_работы__Рубашова_Лилия_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14111\/Доказательство_проведение_претензионной_работы__Спасских_Мария_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14113\/Доказательство_проведение_претензионной_работы__Королева_Габриела_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14119\/Претензия_в_защиту_интересов_Имайкина_Ляйсан_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14123\/Доказательство_проведение_претензионной_работы__Имайкина_Ляйсан_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14125\/Доказательство_проведение_претензионной_работы__Бубличенко_Светлана_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1415\/Пользовательское_соглашение_Гикбрейнс_21.12.2021_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1417\/Справка_об_оплате_обучения.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14171\/Dogovor.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14173\/Check.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14175\/LK.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14181\/доказательство_направления_иска_ответчику_Фатьянов_ООО_-ГИКБРЕИНС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14183\/0_Исковое_заявление_по_делу_Фатьянов_ООО_-ГИКБРЕИНС-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14185\/2_Расчет_исковых_требований_Фатьянов_ООО_-ГИКБРЕИНС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1419\/Screenshot_10.png", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/141953\/соглашение_о_юрпомощи_Костоев_ООО_-ГИКБРЕИНС-.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14199\/oferta.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14201\/yiBD6HIbPWU.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14203\/OaCjmcvzmDU.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14205\/Претензия.docx", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1421\/Письмо_о_смене_номера_лицензии_на_осуществление_образовательной_деятельности_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14210\/oferta.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14212\/yiBD6HIbPWU.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14214\/OaCjmcvzmDU.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14216\/Претензия.docx", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14225\/oferta.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14227\/WinRAR_ZIP_archive.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14229\/WinRAR_ZIP_archive.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14231\/WinRAR_ZIP_archive.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14238\/oferta.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14240\/оплата.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14242\/прогресс.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14244\/претензия.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14246\/возврат.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1431\/kvk.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1433\/kvk.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/143394\/Исполнение_заявление_в_банк_Доверитель.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1435\/Nikita_Ditelev_-_profil_polzovatelya_na_obrazovatelnom_portale_GeekBrains.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1437\/IMG_0237.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14372\/wsZBzMxr4H7IHCnhWiYw2po7I1onBJlr.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14374\/zayavleniye_na_oplatu_tovarov_(uslug)_20.03.2023.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14376\/прогресссс.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14378\/претензионка.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14382\/sber_statement_03-07-2023_17-08-57.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14389\/kreditnyy_dogovor_individual'nykh_usloviy_po_kreditu_(iuk)_07.05.2023.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1439\/IMG_0238.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14391\/image_2023-05-25_20-16-31.png", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14393\/image_2023-05-23_19-06-51.png", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14395\/photo_2023-05-25_20-08-26.jpg", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14397\/ответ_претензия.png", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14404\/oferta_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14406\/0060235508_20230302.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14408\/Screenshot_2023-05-26_at_11.17.44.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14410\/Требование_(претензия)_о_расторжении_договора_и_возврате_денежных_средств.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14412\/operation_statement_21.04.2023.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14414\/Прочее.rar", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14422\/Доказательство_проведение_претензионной_работы__Михайловский_Максим_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14435\/00_документы_суд.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14475\/Д16052023.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14477\/2023-05-15_Хохлов_Максим_Дмитриевич_Приложение_к_оферте_CL-26387_84833.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14479\/2023-07-04_17-59-33.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14488\/00_Петров_документы_суд.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14593\/Доказательство_проведение_претензионной_работы__Ширмухамедов_Акбар_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14611\/доказательство_направления_иска_ответчику_Колосов_ООО_-СКИЛБОКС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14613\/0_Исковое_заявление_по_делу_Колосов_ООО_-СКИЛБОКС-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14615\/2_Расчет_исковых_требований_Колосов_ООО_-СКИЛБОКС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14624\/Пользовательское_соглашение_в_ред._от_12.07.22.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14626\/Подтверждение_оплаты_в_СберБанке.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14628\/2023-07-05_17-15-34.png", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14630\/Требования_поддержке.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1469\/Доказательство_проведение_претензионной_работы__Плешкова_Алина_2_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14718\/доказательство_направления_иска_ответчику_Цапенко_ООО_-ГИКБРЕИНС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14720\/0_Исковое_заявление_по_делу_Цапенко_ООО_-ГИКБРЕИНС-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14722\/2_Расчет_исковых_требований_Цапенко_ООО_-ГИКБРЕИНС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14737\/Колосов_документы_суд.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14744\/Оферта._Редакция_от_29_августа_2022г.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14746\/Zaâvlenie_na_oplatu_tovarov_(uslug).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14748\/IMG_4083.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14750\/IMG_4084.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14820\/Цапенко_документы_суд.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14827\/kvk_(34)_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14829\/3rNhCFnHlOs.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14831\/onOUwSZWvdU.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14839\/kvk_(34)_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14841\/3rNhCFnHlOs.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14843\/onOUwSZWvdU.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14846\/доказательство_направления_иска_ответчику_Уварова_ИП_Ложкина_Анжелика_Геннадиевна_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14848\/0_Исковое_заявление_по_делу_Уварова_ИП_Ложкина_Анжелика_Геннадиевна_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14850\/2_Расчет_исковых_требований_Уварова_ИП_Ложкина_Анжелика_Геннадиевна_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14853\/доказательство_направления_иска_ответчику_Гарбузов_АНО_-ЦРП-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14855\/0_Исковое_заявление_по_делу_Гарбузов_АНО_-ЦРП-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14857\/2_Расчет_исковых_требований_Гарбузов_АНО_-ЦРП-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14860\/доказательство_направления_иска_ответчику_Поселянина_ООО_-ГИКБРЕИНС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14862\/0_Исковое_заявление_по_делу_Поселянина_ООО_-ГИКБРЕИНС-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14864\/2_Расчет_исковых_требований_Поселянина_ООО_-ГИКБРЕИНС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14867\/доказательство_направления_иска_ответчику_Кукса_ООО_-СКИЛБОКС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14869\/0_Исковое_заявление_по_делу_Кукса_ООО_-СКИЛБОКС-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14874\/2_Расчет_исковых_требований_Кукса_ООО_-СКИЛБОКС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14898\/доказательство_направления_иска_ответчику_Жуперина_ООО_-Скилбокс-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14900\/0_Исковое_заявление_по_делу_Жуперина_ООО_-Скилбокс-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14902\/2_Расчет_исковых_требований_Жуперина_ООО_-Скилбокс-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14905\/доказательство_направления_иска_ответчику_Мягкий_ООО_-СКИЛБОКС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14907\/0_Исковое_заявление_по_делу_Мягкий_ООО_-СКИЛБОКС-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14909\/2_Расчет_исковых_требований_Мягкий_ООО_-СКИЛБОКС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14912\/доказательство_направления_иска_ответчику_Кузьмина__ООО_-СКИЛБОКС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14914\/0_Исковое_заявление_по_делу_Кузьмина__ООО_-СКИЛБОКС-_4_стр_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14916\/2_Расчет_исковых_требований_Кузьмина__ООО_-СКИЛБОКС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14934\/доказательство_направления_иска_ответчику_Дителев_ООО_-ГИКБРЕИНС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14936\/0_Исковое_заявление_по_делу_Дителев_ООО_-ГИКБРЕИНС-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14941\/2_Расчет_исковых_требований_Дителев_ООО_-ГИКБРЕИНС-_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14944\/доказательство_направления_иска_ответчику_Нечаева__ИП_Ширков_Данила_Семёнович_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14946\/0_Исковое_заявление_по_делу_Нечаева__ИП_Ширков_Данила_Семёнович_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14948\/2_Расчет_исковых_требований_Нечаева__ИП_Ширков_Данила_Семёнович_1_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1516\/Оферта._Редакция_от_29_августа_2022г.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1518\/КАЮМОВ_ДАНИИЛ_ФЛЮРОВИЧ_(1).pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1520\/Фотографии_личного_кабинета.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/15223\/Договор_Дизайн_цифровых_продуктов_70_первые_полгода_бесплатно_Амбарцумян_Диана_Эдуардовна.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/15225\/Договор_Дизайн_цифровых_продуктов_70_первые_полгода_бесплатно_Амбарцумян_Диана_Эдуардовна.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/15227\/Договор_Дизайн_цифровых_продуктов_70_первые_полгода_бесплатно_Амбарцумян_Диана_Эдуардовна.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/15229\/претензия_ответ_Амбарцумян_Д.Э..pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/152984\/7_заявление_потребителя_Пономарева__.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/152989\/Договор_05-23-2024-08-50-46_Пономарева__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/152991\/Подтверждение_оплаты_05-23-2024-08-54-48_Пономарева__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/152993\/Программа_обучения_05-23-2024-08-55-21_Пономарева__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/152995\/Скриншот_прогресса_обучения_05-23-2024-08-58-09_Пономарева__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/152997\/Претензия_05-23-2024-08-59-05_Пономарева__1_CTP#realfile.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/15309\/Заявление_(1).PDF", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/15345\/_.zip", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/154432\/0_Исковое_заявление_по_делу_Окуньков__ООО_-ТЕРРА_ЭЙАЙ-_4_стр.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/155051\/7777777_Заявление_о_выдачи_исполнительного_листа_.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/155057\/7777777_Заявление_о_выдачи_исполнительного_листа_.pdf", + "Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/155260\/7777777_Заявление_о_выдачи_исполнительного_листа_.pdf" + ] +} \ No newline at end of file diff --git a/restore_log_2025-11-25_10-24-33.json b/restore_log_2025-11-25_10-24-33.json new file mode 100644 index 00000000..e865c550 --- /dev/null +++ b/restore_log_2025-11-25_10-24-33.json @@ -0,0 +1,7 @@ +{ + "total_markers": 0, + "restored": 0, + "failed": 0, + "skipped": 0, + "errors": [] +} \ No newline at end of file diff --git a/restore_log_2025-11-25_10-28-14.json b/restore_log_2025-11-25_10-28-14.json new file mode 100644 index 00000000..e865c550 --- /dev/null +++ b/restore_log_2025-11-25_10-28-14.json @@ -0,0 +1,7 @@ +{ + "total_markers": 0, + "restored": 0, + "failed": 0, + "skipped": 0, + "errors": [] +} \ No newline at end of file diff --git a/restore_log_2025-11-25_17-08-20.json b/restore_log_2025-11-25_17-08-20.json new file mode 100644 index 00000000..cc1a255a --- /dev/null +++ b/restore_log_2025-11-25_17-08-20.json @@ -0,0 +1,7 @@ +{ + "total_markers": 46, + "restored": 2, + "failed": 43, + "skipped": 1, + "errors": [] +} \ No newline at end of file diff --git a/restore_project_373977.php b/restore_project_373977.php new file mode 100644 index 00000000..f0d06b20 --- /dev/null +++ b/restore_project_373977.php @@ -0,0 +1,168 @@ + '8_Договор_на_оказание_услуг_373981.pdf', + 373983 => '9_Подтверждение_оплаты_по_договору_373983.pdf', + 373985 => '10_2_Скрин_личного_кабинета_Истца_и_программа_обуч_373985.pdf', + 373987 => '10_1_Скрин_личного_кабинета_Истца_и_программа_обуч_373987.pdf', + 373989 => '11_1_Подтверждение_проведения_претензионной_работы_373989.pdf', + 373991 => '7_заявление_потребителя_373991.pdf', + 374017 => '11_Доказательство_соблюдения_претензионного_порядк_374017.pdf', + 375402 => '11.2_Претензия_в_защиту_интересов_Полулях_Ольга_1_375402.pdf', + 375404 => '11.3_Доказательство_оплаты_направления_претензии_о_375404.pdf', + 375406 => '11.4_Доказательство_направления_претензии_ответчик_375406.pdf', + 376051 => '0_Исковое_заявление_по_делу_Полулях_7_стр_376051.pdf', + 376054 => '6_Расчет_исковых_требований_Полулях_1_стр_376054.pdf', + 376080 => '12.1_Доказательство_оплаты_направления_иска_ответч_376080.pdf', + 376082 => '12.2_Доказательство_направления_иска_ответчику_376082.pdf', +]; + +echo "=== ВОССТАНОВЛЕНИЕ ДОКУМЕНТОВ ПРОЕКТА {$projectId} ===\n"; +echo str_repeat("=", 80) . "\n\n"; + +$dryRun = isset($argv[1]) && $argv[1] === '--dry-run'; + +if ($dryRun) { + echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run)\n\n"; +} + +try { + $s3Client = new \Aws\S3\S3Client([ + 'version' => 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + $stats = [ + 'total' => count($files), + 'restored' => 0, + 'already_exists' => 0, + 'no_versions' => 0, + 'failed' => 0, + ]; + + foreach ($files as $docId => $filename) { + $key = $prefix . $filename; + + echo "Документ ID: {$docId}\n"; + echo " Файл: {$filename}\n"; + + // Проверяем, существует ли файл + if ($s3Client->doesObjectExist($s3Bucket, $key)) { + echo " ✅ Файл уже существует\n\n"; + $stats['already_exists']++; + continue; + } + + // Проверяем версии + try { + $versions = $s3Client->listObjectVersions([ + 'Bucket' => $s3Bucket, + 'Prefix' => $key, + 'MaxKeys' => 10, + ]); + + $deleteMarkers = $versions['DeleteMarkers'] ?? []; + $fileVersions = $versions['Versions'] ?? []; + + if (empty($deleteMarkers) && empty($fileVersions)) { + echo " ❌ Нет версий для восстановления\n\n"; + $stats['no_versions']++; + continue; + } + + echo " Найдено delete markers: " . count($deleteMarkers) . "\n"; + echo " Найдено версий: " . count($fileVersions) . "\n"; + + if (!$dryRun) { + // Удаляем все delete markers + foreach ($deleteMarkers as $marker) { + try { + $s3Client->deleteObject([ + 'Bucket' => $s3Bucket, + 'Key' => $key, + 'VersionId' => $marker['VersionId'], + ]); + echo " ✅ Delete marker удален\n"; + } catch (Exception $e) { + echo " ⚠️ Ошибка удаления delete marker: " . $e->getMessage() . "\n"; + } + } + + // Проверяем, появился ли файл после удаления delete marker + if (!$s3Client->doesObjectExist($s3Bucket, $key) && !empty($fileVersions)) { + // Копируем последнюю версию + $latestVersion = $fileVersions[0]; + try { + $s3Client->copyObject([ + 'Bucket' => $s3Bucket, + 'Key' => $key, + 'CopySource' => "{$s3Bucket}/{$key}?versionId={$latestVersion['VersionId']}", + ]); + echo " ✅ Файл восстановлен из версии\n"; + } catch (Exception $e) { + echo " ⚠️ Ошибка копирования версии: " . $e->getMessage() . "\n"; + } + } else { + echo " ✅ Файл восстановлен\n"; + } + + $stats['restored']++; + sleep(1); + + } else { + echo " ⏸️ Будет восстановлен (dry-run)\n"; + $stats['restored']++; + } + + } catch (Exception $e) { + echo " ❌ Ошибка: " . $e->getMessage() . "\n"; + $stats['failed']++; + } + + echo "\n"; + } + + // Итоги + echo str_repeat("=", 80) . "\n"; + echo "ИТОГИ:\n"; + echo "Всего файлов: {$stats['total']}\n"; + + if (!$dryRun) { + echo "Восстановлено: {$stats['restored']}\n"; + echo "Уже существует: {$stats['already_exists']}\n"; + echo "Нет версий: {$stats['no_versions']}\n"; + echo "Ошибок: {$stats['failed']}\n"; + } else { + echo "Будет восстановлено: {$stats['restored']}\n"; + echo "Уже существует: {$stats['already_exists']}\n"; + echo "Нет версий: {$stats['no_versions']}\n"; + } + + echo "\n=== ГОТОВО ===\n"; + +} catch (Exception $e) { + echo "❌ Критическая ошибка: " . $e->getMessage() . "\n"; + exit(1); +} + + diff --git a/restore_project_391584.php b/restore_project_391584.php new file mode 100644 index 00000000..438dbc94 --- /dev/null +++ b/restore_project_391584.php @@ -0,0 +1,132 @@ + '8_Договор_на_оказание_услуг_391587.pdf', + 391589 => '9_Подтверждение_оплаты_по_договору_391589.pdf', + 391591 => '10_1_Скрин_личного_кабинета_Истца_и_программа_обуч_391591.pdf', + 391593 => '7_заявление_потребителя_391593.pdf', + 392332 => '11_Доказательство_соблюдения_претензионного_порядк_392332.pdf', + 392472 => '11.1_Доказательство_соблюдения_претензионного_поря_392472.pdf', + 392475 => '11.2_Доказательство_соблюдения_претензионного_поря_392475.pdf', + 395136 => '6_Расчет_иска_Чужба_395136.pdf', + 395157 => '0_Исковое_заявление_по_делу_Чужба_ЧОУ_ДПО_ОБРАЗОВА_395157.pdf', + 395744 => '12.1_Доказательство_оплаты_направления_иска_ответч_395744.pdf', +]; + +echo "=== ВОССТАНОВЛЕНИЕ ФАЙЛОВ ПРОЕКТА {$projectId} ===\n"; +echo str_repeat("=", 80) . "\n\n"; + +try { + $s3Client = new \Aws\S3\S3Client([ + 'version' => 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + $stats = [ + 'total' => count($documents), + 'restored' => 0, + 'not_found' => 0, + 'already_exists' => 0, + ]; + + foreach ($documents as $docId => $filename) { + $s3Key = $projectPrefix . $filename; + + echo "Документ ID: {$docId}\n"; + echo " Файл: {$filename}\n"; + + // Проверяем, существует ли файл + if ($s3Client->doesObjectExist($s3Bucket, $s3Key)) { + echo " ✅ Файл уже существует\n\n"; + $stats['already_exists']++; + continue; + } + + // Проверяем наличие версий и delete markers + try { + $versions = $s3Client->listObjectVersions([ + 'Bucket' => $s3Bucket, + 'Prefix' => $s3Key, + 'MaxKeys' => 10, + ]); + + $deleteMarkers = $versions['DeleteMarkers'] ?? []; + $fileVersions = $versions['Versions'] ?? []; + + if (empty($deleteMarkers) && empty($fileVersions)) { + echo " ❌ Файл не найден и нет версий\n\n"; + $stats['not_found']++; + continue; + } + + // Удаляем delete markers + foreach ($deleteMarkers as $marker) { + $s3Client->deleteObject([ + 'Bucket' => $s3Bucket, + 'Key' => $s3Key, + 'VersionId' => $marker['VersionId'], + ]); + echo " ✅ Delete marker удален\n"; + } + + // Если файл все еще не существует, восстанавливаем из версии + if (!$s3Client->doesObjectExist($s3Bucket, $s3Key) && !empty($fileVersions)) { + $latestVersion = $fileVersions[0]; + $s3Client->copyObject([ + 'Bucket' => $s3Bucket, + 'Key' => $s3Key, + 'CopySource' => "{$s3Bucket}/{$s3Key}?versionId={$latestVersion['VersionId']}", + ]); + echo " ✅ Файл восстановлен из версии\n"; + } else { + echo " ✅ Файл восстановлен\n"; + } + + $stats['restored']++; + sleep(1); + + } catch (Exception $e) { + echo " ❌ Ошибка: " . $e->getMessage() . "\n"; + $stats['not_found']++; + } + + echo "\n"; + } + + // Итоги + echo str_repeat("=", 80) . "\n"; + echo "ИТОГИ:\n"; + echo "Всего документов: {$stats['total']}\n"; + echo "✅ Восстановлено: {$stats['restored']}\n"; + echo "✅ Уже существовало: {$stats['already_exists']}\n"; + echo "❌ Не найдено: {$stats['not_found']}\n\n"; + + echo "=== ГОТОВО ===\n"; + +} catch (Exception $e) { + echo "❌ Критическая ошибка: " . $e->getMessage() . "\n"; + exit(1); +} + + diff --git a/restore_project_394091.php b/restore_project_394091.php new file mode 100644 index 00000000..217bd0fd --- /dev/null +++ b/restore_project_394091.php @@ -0,0 +1,199 @@ + 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + // Получаем документы проекта из БД + echo "Подключение к БД...\n"; + $db = PearDatabase::getInstance(); + echo "✅ БД подключена\n"; + + echo "Выполнение запроса...\n"; + $result = $db->pquery(" + SELECT n.notesid, n.filename, n.filelocationtype, n.s3_bucket, n.s3_key, n.filesize + FROM vtiger_notes n + INNER JOIN vtiger_senotesrel sn ON sn.notesid = n.notesid + WHERE sn.crmid = ? + ORDER BY n.notesid + ", array($projectId)); + + echo "✅ Запрос выполнен\n"; + + $totalDocs = $db->num_rows($result); + echo "Найдено документов в БД: {$totalDocs}\n\n"; + + if ($totalDocs == 0) { + echo "Документы не найдены!\n"; + exit(0); + } + + $stats = [ + 'total_docs' => $totalDocs, + 'existing' => 0, + 'deleted' => 0, + 'missing' => 0, + 'restored' => 0, + 'failed' => 0, + 'errors' => [] + ]; + + // Проверяем каждый документ + while ($row = $db->fetch_array($result)) { + $s3Key = $row['s3_key']; + $docId = $row['notesid']; + $filename = $row['filename']; + + echo "Документ ID: {$docId} | Файл: {$filename}\n"; + + if (empty($s3Key)) { + echo " ⚠️ Нет S3 ключа\n\n"; + $stats['missing']++; + continue; + } + + echo " S3 ключ: {$s3Key}\n"; + + // Проверяем существование файла + $exists = $s3Client->doesObjectExist($s3Bucket, $s3Key); + + if ($exists) { + echo " ✅ Файл существует\n\n"; + $stats['existing']++; + continue; + } + + // Проверяем версии и delete markers + try { + $versions = $s3Client->listObjectVersions([ + 'Bucket' => $s3Bucket, + 'Prefix' => $s3Key, + ]); + + $deleteMarker = null; + $fileVersion = null; + + foreach ($versions['Versions'] ?? [] as $version) { + if (isset($version['IsDeleteMarker']) && $version['IsDeleteMarker']) { + $deleteMarker = $version; + } else { + $fileVersion = $version; + } + } + + if ($deleteMarker) { + echo " ❌ Файл удален (delete marker от " . $deleteMarker['LastModified']->format('Y-m-d H:i:s') . ")\n"; + + if (!$dryRun) { + // Удаляем delete marker + try { + $s3Client->deleteObject([ + 'Bucket' => $s3Bucket, + 'Key' => $s3Key, + 'VersionId' => $deleteMarker['VersionId'], + ]); + + // Если есть версия файла, копируем её + if ($fileVersion) { + $s3Client->copyObject([ + 'Bucket' => $s3Bucket, + 'Key' => $s3Key, + 'CopySource' => "{$s3Bucket}/{$s3Key}?versionId={$fileVersion['VersionId']}", + ]); + echo " ✅ Файл восстановлен из версии\n"; + } else { + echo " ⚠️ Delete marker удален, но версия файла не найдена\n"; + } + + $stats['restored']++; + sleep(1); // Пауза между запросами + + } catch (Exception $e) { + echo " ❌ Ошибка восстановления: " . $e->getMessage() . "\n"; + $stats['failed']++; + $stats['errors'][] = "{$s3Key}: " . $e->getMessage(); + } + } else { + echo " ⏸️ Будет восстановлен (dry-run)\n"; + $stats['restored']++; + } + + $stats['deleted']++; + + } else { + echo " ⚠️ Файл отсутствует, но delete marker не найден\n"; + $stats['missing']++; + } + + } catch (Exception $e) { + echo " ❌ Ошибка проверки версий: " . $e->getMessage() . "\n"; + $stats['failed']++; + $stats['errors'][] = "{$s3Key}: " . $e->getMessage(); + } + + echo "\n"; + } + + // Итоговая статистика + echo str_repeat("=", 80) . "\n"; + echo "ИТОГОВАЯ СТАТИСТИКА:\n\n"; + echo "Всего документов: {$stats['total_docs']}\n"; + echo "Существующих файлов: {$stats['existing']}\n"; + echo "Удаленных файлов (delete marker): {$stats['deleted']}\n"; + echo "Отсутствующих файлов: {$stats['missing']}\n"; + + if (!$dryRun) { + echo "Восстановлено: {$stats['restored']}\n"; + echo "Ошибок: {$stats['failed']}\n"; + } else { + echo "Будет восстановлено: {$stats['restored']}\n"; + } + + if (!empty($stats['errors'])) { + echo "\nОшибки:\n"; + foreach ($stats['errors'] as $error) { + echo " - {$error}\n"; + } + } + + echo "\n=== ГОТОВО ===\n"; + +} catch (Exception $e) { + echo "❌ Критическая ошибка: " . $e->getMessage() . "\n"; + echo "Stack trace:\n" . $e->getTraceAsString() . "\n"; + exit(1); +} diff --git a/restore_project_394091_simple.php b/restore_project_394091_simple.php new file mode 100644 index 00000000..38d3213c --- /dev/null +++ b/restore_project_394091_simple.php @@ -0,0 +1,156 @@ + 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + $stats = [ + 'total' => count($files), + 'existing' => 0, + 'deleted' => 0, + 'missing' => 0, + 'restored' => 0, + 'failed' => 0, + ]; + + foreach ($files as $s3Key) { + $filename = basename($s3Key); + echo "Файл: {$filename}\n"; + echo " Путь: {$s3Key}\n"; + + // Проверяем существование + $exists = $s3Client->doesObjectExist($s3Bucket, $s3Key); + + if ($exists) { + echo " ✅ Файл существует\n\n"; + $stats['existing']++; + continue; + } + + // Проверяем версии + try { + $versions = $s3Client->listObjectVersions([ + 'Bucket' => $s3Bucket, + 'Prefix' => $s3Key, + ]); + + $deleteMarker = null; + $fileVersion = null; + + foreach ($versions['Versions'] ?? [] as $version) { + if (isset($version['IsDeleteMarker']) && $version['IsDeleteMarker']) { + $deleteMarker = $version; + } else { + $fileVersion = $version; + } + } + + if ($deleteMarker) { + echo " ❌ Файл удален (delete marker от " . $deleteMarker['LastModified']->format('Y-m-d H:i:s') . ")\n"; + + if (!$dryRun) { + // Удаляем delete marker + $s3Client->deleteObject([ + 'Bucket' => $s3Bucket, + 'Key' => $s3Key, + 'VersionId' => $deleteMarker['VersionId'], + ]); + echo " ✅ Delete marker удален\n"; + + // Восстанавливаем файл из версии + if ($fileVersion) { + $s3Client->copyObject([ + 'Bucket' => $s3Bucket, + 'Key' => $s3Key, + 'CopySource' => "{$s3Bucket}/{$s3Key}?versionId={$fileVersion['VersionId']}", + ]); + echo " ✅ Файл восстановлен из версии\n"; + $stats['restored']++; + } else { + echo " ⚠️ Версия файла не найдена\n"; + $stats['missing']++; + } + + sleep(1); + } else { + echo " ⏸️ Будет восстановлен (dry-run)\n"; + $stats['restored']++; + } + + $stats['deleted']++; + + } else { + echo " ⚠️ Файл отсутствует, delete marker не найден\n"; + $stats['missing']++; + } + + } catch (Exception $e) { + echo " ❌ Ошибка: " . $e->getMessage() . "\n"; + $stats['failed']++; + } + + echo "\n"; + } + + // Итоги + echo str_repeat("=", 80) . "\n"; + echo "ИТОГИ:\n"; + echo "Всего файлов: {$stats['total']}\n"; + echo "Существующих: {$stats['existing']}\n"; + echo "Удаленных: {$stats['deleted']}\n"; + echo "Отсутствующих: {$stats['missing']}\n"; + + if (!$dryRun) { + echo "Восстановлено: {$stats['restored']}\n"; + echo "Ошибок: {$stats['failed']}\n"; + } else { + echo "Будет восстановлено: {$stats['restored']}\n"; + } + + echo "\n=== ГОТОВО ===\n"; + +} catch (Exception $e) { + echo "❌ Ошибка: " . $e->getMessage() . "\n"; + exit(1); +} + diff --git a/search_by_doc_id.php b/search_by_doc_id.php new file mode 100644 index 00000000..31f16b34 --- /dev/null +++ b/search_by_doc_id.php @@ -0,0 +1,355 @@ + PDO::ERRMODE_EXCEPTION] + ); + + $stmt = $pdo->prepare('SELECT notesid, title, s3_key, filename, filelocationtype FROM vtiger_notes WHERE notesid = ?'); + $stmt->execute([$docId]); + $doc = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$doc) { + die("Документ $docId не найден в БД\n"); + } + + echo "Информация о документе:\n"; + echo " ID: {$doc['notesid']}\n"; + echo " Название: {$doc['title']}\n"; + echo " S3 Key (ожидаемый): " . ($doc['s3_key'] ?: 'не указан') . "\n"; + echo " Filename: " . substr($doc['filename'], 0, 150) . "\n"; + echo " Тип хранения: " . ($doc['filelocationtype'] ?: 'не указан') . "\n\n"; + + // Инициализация S3 клиента + $s3Client = new \Aws\S3\S3Client([ + 'version' => 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + $foundFiles = []; + + // 1. Проверяем ожидаемый путь + echo "1. Проверка ожидаемого пути в S3...\n"; + if (!empty($doc['s3_key'])) { + try { + $result = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $doc['s3_key'] + ]); + echo " ✅ Файл найден по ожидаемому пути!\n"; + echo " Путь: {$doc['s3_key']}\n"; + echo " Размер: " . number_format($result['ContentLength'] / 1024, 2) . " KB\n"; + $foundFiles[] = [ + 'key' => $doc['s3_key'], + 'size' => $result['ContentLength'], + 'source' => 'ожидаемый путь' + ]; + } catch (\Aws\Exception\AwsException $e) { + echo " ❌ Файл не найден по ожидаемому пути: " . $e->getAwsErrorCode() . "\n"; + } + } + echo "\n"; + + // 2. Поиск по точному ID документа + echo "2. Поиск файлов с точным ID $docId...\n"; + $searchPatterns = [ + (string)$docId, + (string)($docId - 1), + (string)($docId + 1), + (string)($docId - 2), + (string)($docId + 2), + ]; + + $totalChecked = 0; + $maxFilesToCheck = 50000; + + foreach ($searchPatterns as $pattern) { + echo " Поиск по паттерну: $pattern\n"; + + try { + $isTruncated = true; + $continuationToken = null; + $pageCount = 0; + $maxPages = 50; + + while ($isTruncated && $pageCount < $maxPages && $totalChecked < $maxFilesToCheck) { + $params = [ + 'Bucket' => $s3Bucket, + 'MaxKeys' => 1000 + ]; + + if ($continuationToken) { + $params['ContinuationToken'] = $continuationToken; + } + + $objects = $s3Client->listObjectsV2($params); + $pageCount++; + $totalChecked += isset($objects['Contents']) ? count($objects['Contents']) : 0; + + if (isset($objects['Contents'])) { + foreach ($objects['Contents'] as $object) { + $key = $object['Key']; + + // Ищем файлы, содержащие паттерн + if (strpos($key, $pattern) !== false) { + // Проверяем, не добавили ли уже этот файл + $alreadyAdded = false; + foreach ($foundFiles as $found) { + if ($found['key'] === $key) { + $alreadyAdded = true; + break; + } + } + + if (!$alreadyAdded) { + try { + $headResult = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $key + ]); + + echo " ✅ НАЙДЕН: $key\n"; + echo " Размер: " . number_format($headResult['ContentLength'] / 1024, 2) . " KB\n"; + echo " Дата: " . ($headResult['LastModified'] ?? 'не указана') . "\n"; + + $foundFiles[] = [ + 'key' => $key, + 'size' => $headResult['ContentLength'], + 'source' => "поиск по ID $pattern", + 'date' => $headResult['LastModified'] ?? null + ]; + } catch (\Aws\Exception\AwsException $e) { + // Пропускаем недоступные файлы + } + } + } + } + } + + $isTruncated = isset($objects['IsTruncated']) && $objects['IsTruncated']; + $continuationToken = isset($objects['NextContinuationToken']) ? $objects['NextContinuationToken'] : null; + + if (!$isTruncated) { + break; + } + } + } catch (\Aws\Exception\AwsException $e) { + echo " Ошибка при поиске: " . $e->getMessage() . "\n"; + } + } + echo " Проверено файлов: $totalChecked\n\n"; + + // 3. Поиск по ключевым словам из названия документа + echo "3. Поиск по ключевым словам из названия...\n"; + $title = mb_strtolower($doc['title']); + $keywords = []; + + // Извлекаем ключевые слова + if (strpos($title, 'исковое') !== false) $keywords[] = 'iskovoe'; + if (strpos($title, 'заявление') !== false) $keywords[] = 'zayavlenie'; + if (strpos($title, 'марсель') !== false) $keywords[] = 'marse'; + if (strpos($title, 'гафиев') !== false) $keywords[] = 'gafiev'; + if (strpos($title, 'gafiev') !== false) $keywords[] = 'gafiev'; + + echo " Ключевые слова: " . implode(', ', $keywords) . "\n"; + + if (!empty($keywords)) { + // Ищем в temp/ папках + try { + $objects = $s3Client->listObjectsV2([ + 'Bucket' => $s3Bucket, + 'Prefix' => 'temp/', + 'MaxKeys' => 10000 + ]); + + if (isset($objects['Contents'])) { + foreach ($objects['Contents'] as $object) { + $key = $object['Key']; + $keyLower = mb_strtolower($key); + + // Проверяем совпадение по ключевым словам + $matches = 0; + foreach ($keywords as $keyword) { + if (strpos($keyLower, $keyword) !== false) { + $matches++; + } + } + + // Если совпало хотя бы 2 ключевых слова + if ($matches >= 2) { + $alreadyAdded = false; + foreach ($foundFiles as $found) { + if ($found['key'] === $key) { + $alreadyAdded = true; + break; + } + } + + if (!$alreadyAdded) { + try { + $headResult = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $key + ]); + + echo " ✅ НАЙДЕН по ключевым словам: $key\n"; + echo " Размер: " . number_format($headResult['ContentLength'] / 1024, 2) . " KB\n"; + + $foundFiles[] = [ + 'key' => $key, + 'size' => $headResult['ContentLength'], + 'source' => 'поиск по ключевым словам', + 'date' => $headResult['LastModified'] ?? null + ]; + } catch (\Aws\Exception\AwsException $e) { + // Пропускаем + } + } + } + } + } + } catch (\Aws\Exception\AwsException $e) { + echo " Ошибка при поиске: " . $e->getMessage() . "\n"; + } + } + echo "\n"; + + // 4. Поиск в папке проекта (если известен projectId) + echo "4. Поиск в папке проекта...\n"; + $stmt = $pdo->prepare('SELECT crmid FROM vtiger_senotesrel WHERE notesid = ? LIMIT 1'); + $stmt->execute([$docId]); + $projectRel = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($projectRel) { + $projectId = $projectRel['crmid']; + echo " Проект ID: $projectId\n"; + + $projectPrefixes = [ + "crm2/CRM_Active_Files/Documents/Project/", + "temp/$projectId/", + ]; + + foreach ($projectPrefixes as $prefix) { + try { + $objects = $s3Client->listObjectsV2([ + 'Bucket' => $s3Bucket, + 'Prefix' => $prefix, + 'MaxKeys' => 1000 + ]); + + if (isset($objects['Contents'])) { + foreach ($objects['Contents'] as $object) { + $key = $object['Key']; + + // Ищем файлы с ID документа + if (strpos($key, (string)$docId) !== false || + strpos($key, (string)($docId - 1)) !== false || + strpos($key, (string)($docId + 1)) !== false) { + + $alreadyAdded = false; + foreach ($foundFiles as $found) { + if ($found['key'] === $key) { + $alreadyAdded = true; + break; + } + } + + if (!$alreadyAdded) { + try { + $headResult = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $key + ]); + + echo " ✅ НАЙДЕН в папке проекта: $key\n"; + echo " Размер: " . number_format($headResult['ContentLength'] / 1024, 2) . " KB\n"; + + $foundFiles[] = [ + 'key' => $key, + 'size' => $headResult['ContentLength'], + 'source' => "папка проекта $projectId", + 'date' => $headResult['LastModified'] ?? null + ]; + } catch (\Aws\Exception\AwsException $e) { + // Пропускаем + } + } + } + } + } + } catch (\Aws\Exception\AwsException $e) { + // Пропускаем ошибки + } + } + } + echo "\n"; + + // Итоги + echo str_repeat("=", 80) . "\n"; + echo "ИТОГОВЫЕ РЕЗУЛЬТАТЫ:\n"; + echo " Всего найдено файлов: " . count($foundFiles) . "\n\n"; + + if (!empty($foundFiles)) { + echo "НАЙДЕННЫЕ ФАЙЛЫ:\n"; + foreach ($foundFiles as $i => $file) { + echo " " . ($i + 1) . ". {$file['key']}\n"; + echo " Размер: " . number_format($file['size'] / 1024, 2) . " KB\n"; + echo " Источник: {$file['source']}\n"; + if ($file['date']) { + echo " Дата: {$file['date']}\n"; + } + echo "\n"; + } + + // Определяем наиболее вероятный файл + echo "💡 РЕКОМЕНДАЦИЯ:\n"; + $bestMatch = null; + foreach ($foundFiles as $file) { + if ($file['source'] === 'ожидаемый путь') { + $bestMatch = $file; + break; + } + } + + if (!$bestMatch && !empty($foundFiles)) { + // Берем первый найденный файл + $bestMatch = $foundFiles[0]; + } + + if ($bestMatch) { + echo " Наиболее вероятный файл: {$bestMatch['key']}\n"; + echo " Этот файл можно использовать для обновления s3_key в БД\n"; + } + } else { + echo " ❌ Файлы не найдены в S3\n"; + echo " Возможно, файл находится в Nextcloud или локальном хранилище\n"; + } + +} catch (Exception $e) { + echo "ОШИБКА: " . $e->getMessage() . "\n"; + echo "Trace: " . $e->getTraceAsString() . "\n"; +} + diff --git a/search_file_in_s3.php b/search_file_in_s3.php new file mode 100644 index 00000000..9543fa03 --- /dev/null +++ b/search_file_in_s3.php @@ -0,0 +1,216 @@ + 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + // Получаем s3_key из БД + require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php'; + $pdo = new PDO( + "mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8", + $dbconfig['db_username'], + $dbconfig['db_password'], + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); + + $stmt = $pdo->prepare('SELECT s3_key, filename FROM vtiger_notes WHERE notesid = ?'); + $stmt->execute([$docId]); + $doc = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$doc) { + die("Документ $docId не найден в БД\n"); + } + + $s3Key = $doc['s3_key']; + echo "S3 Key из БД: $s3Key\n\n"; + + // Варианты путей для проверки + $variants = [ + $s3Key, // Оригинальный путь + rawurlencode($s3Key), // URL-encoded + str_replace(' ', '_', $s3Key), // Замена пробелов + str_replace(' ', '%20', $s3Key), // Пробелы как %20 + ]; + + echo "1. Проверка существования файла по разным вариантам пути:\n"; + foreach ($variants as $i => $variant) { + echo " Вариант " . ($i + 1) . ": " . substr($variant, 0, 100) . "...\n"; + try { + $result = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $variant + ]); + echo " ✅ Файл найден! Size: " . $result['ContentLength'] . " bytes\n"; + echo " ContentType: " . $result['ContentType'] . "\n"; + break; + } catch (\Aws\Exception\AwsException $e) { + if ($e->getAwsErrorCode() == 'NotFound') { + echo " ❌ Не найден\n"; + } else { + echo " ⚠️ Ошибка: " . $e->getAwsErrorCode() . "\n"; + } + } + } + echo "\n"; + + // Поиск всех файлов в папке проекта + echo "2. Поиск всех файлов в папке проекта:\n"; + $prefixes = [ + "crm2/CRM_Active_Files/Documents/Project/", + "crm2/CRM_Active_Files/Documents/", + "Documents/Project/", + "Documents/", + ]; + + $allFoundFiles = []; + foreach ($prefixes as $projectPrefix) { + echo " Проверка префикса: $projectPrefix\n"; + try { + $objects = $s3Client->listObjectsV2([ + 'Bucket' => $s3Bucket, + 'Prefix' => $projectPrefix, + 'MaxKeys' => 1000 + ]); + + if (isset($objects['Contents'])) { + foreach ($objects['Contents'] as $object) { + $key = $object['Key']; + // Ищем файлы, связанные с проектом 371231 или документом 371983 + if (strpos($key, (string)$projectId) !== false || strpos($key, '371983') !== false || + strpos($key, 'Ломакин') !== false || strpos($key, 'НЕТОЛОГИЯ') !== false) { + if (!in_array($key, $allFoundFiles)) { + $allFoundFiles[] = $key; + } + } + } + } + } catch (\Aws\Exception\AwsException $e) { + echo " Ошибка при поиске: " . $e->getMessage() . "\n"; + } + } + + if (!empty($allFoundFiles)) { + echo " Найдено " . count($allFoundFiles) . " файлов:\n"; + foreach ($allFoundFiles as $file) { + echo " - $file\n"; + } + } else { + echo " Файлы проекта не найдены в S3\n"; + } + echo "\n"; + + // Поиск файла по имени (без полного пути) + echo "3. Поиск файла по имени и ID документа:\n"; + $searchPatterns = [ + "11.1_Доказательство_соблюдения_претензионного_поря_371983.pdf", + "371983", + "11.1", + "Доказательство_соблюдения" + ]; + + $foundFiles = []; + foreach ($searchPatterns as $pattern) { + echo " Поиск по паттерну: $pattern\n"; + try { + // Ищем в разных префиксах + $searchPrefixes = [ + "crm2/CRM_Active_Files/", + "Documents/", + "" + ]; + + foreach ($searchPrefixes as $prefix) { + $objects = $s3Client->listObjectsV2([ + 'Bucket' => $s3Bucket, + 'Prefix' => $prefix, + 'MaxKeys' => 5000 + ]); + + if (isset($objects['Contents'])) { + foreach ($objects['Contents'] as $object) { + $key = $object['Key']; + if (stripos($key, $pattern) !== false) { + if (!in_array($key, $foundFiles)) { + $foundFiles[] = $key; + } + } + } + } + } + } catch (\Aws\Exception\AwsException $e) { + echo " Ошибка при поиске: " . $e->getMessage() . "\n"; + } + } + + if (!empty($foundFiles)) { + echo " Найдено " . count($foundFiles) . " файлов:\n"; + foreach ($foundFiles as $file) { + echo " - $file\n"; + // Проверяем доступность + try { + $result = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $file + ]); + echo " ✅ Доступен, размер: " . $result['ContentLength'] . " bytes\n"; + } catch (\Aws\Exception\AwsException $e) { + echo " ❌ Недоступен: " . $e->getAwsErrorCode() . "\n"; + } + } + } else { + echo " Файл не найден по имени\n"; + } + + // Проверяем все документы проекта из БД + echo "\n4. Проверка всех документов проекта из БД:\n"; + $stmt = $pdo->prepare('SELECT n.notesid, n.title, n.s3_key FROM vtiger_notes n INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid WHERE snr.crmid = ? AND e.deleted = 0 AND n.filelocationtype = "E" ORDER BY n.notesid DESC LIMIT 10'); + $stmt->execute([$projectId]); + $docs = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo " Проверка " . count($docs) . " документов:\n"; + $foundCount = 0; + foreach ($docs as $doc) { + $key = $doc['s3_key']; + try { + $result = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $key + ]); + echo " ✅ ID {$doc['notesid']}: " . substr($doc['title'], 0, 50) . "...\n"; + $foundCount++; + } catch (\Aws\Exception\AwsException $e) { + if ($e->getAwsErrorCode() == 'NotFound') { + echo " ❌ ID {$doc['notesid']}: " . substr($doc['title'], 0, 50) . "... (не найден)\n"; + } + } + } + echo " Найдено в S3: $foundCount из " . count($docs) . "\n"; + +} catch (Exception $e) { + echo "ОШИБКА: " . $e->getMessage() . "\n"; + echo "Trace: " . $e->getTraceAsString() . "\n"; +} + diff --git a/search_in_trash.php b/search_in_trash.php new file mode 100644 index 00000000..2533d22c --- /dev/null +++ b/search_in_trash.php @@ -0,0 +1,259 @@ + PDO::ERRMODE_EXCEPTION] + ); + + // Инициализация S3 клиента + $s3Client = new \Aws\S3\S3Client([ + 'version' => 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + foreach ($docIds as $docId) { + echo "Поиск файла для документа $docId:\n"; + echo str_repeat("-", 80) . "\n"; + + $stmt = $pdo->prepare('SELECT notesid, title, s3_key, s3_etag, filesize FROM vtiger_notes WHERE notesid = ?'); + $stmt->execute([$docId]); + $doc = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$doc) { + echo " Документ не найден в БД\n\n"; + continue; + } + + echo " Название: {$doc['title']}\n"; + echo " Ожидаемый путь: {$doc['s3_key']}\n"; + echo " ETag: {$doc['s3_etag']}\n"; + echo " Размер: " . number_format($doc['filesize'] / 1024, 2) . " KB\n\n"; + + $s3Key = $doc['s3_key']; + $targetEtag = trim($doc['s3_etag'], '"'); + $targetSize = $doc['filesize']; + + // 1. Проверяем версии объекта (если включено versioning) + echo " 1. Проверка версий объекта...\n"; + try { + $versions = $s3Client->listObjectVersions([ + 'Bucket' => $s3Bucket, + 'Prefix' => $s3Key, + 'MaxKeys' => 100 + ]); + + if (isset($versions['Versions']) && !empty($versions['Versions'])) { + echo " ✅ Найдено версий: " . count($versions['Versions']) . "\n"; + foreach ($versions['Versions'] as $version) { + $versionKey = $version['Key']; + $versionId = $version['VersionId']; + $isDeleteMarker = isset($version['IsDeleteMarker']) && $version['IsDeleteMarker']; + $versionEtag = isset($version['ETag']) ? trim($version['ETag'], '"') : null; + $versionSize = isset($version['Size']) ? $version['Size'] : null; + + if ($isDeleteMarker) { + echo " ⚠️ Delete Marker: VersionId=$versionId, Дата: " . ($version['LastModified'] ?? 'не указана') . "\n"; + } else { + $matchInfo = []; + if ($versionEtag === $targetEtag) { + $matchInfo[] = "ETag совпадает"; + } + if ($versionSize && abs($versionSize - $targetSize) <= 100) { + $matchInfo[] = "размер совпадает (" . number_format($versionSize / 1024, 2) . " KB)"; + } + + echo " ✅ Версия: VersionId=$versionId\n"; + echo " Размер: " . ($versionSize ? number_format($versionSize / 1024, 2) . " KB" : 'не указан') . "\n"; + echo " ETag: " . ($versionEtag ?: 'не указан') . "\n"; + echo " Дата: " . ($version['LastModified'] ?? 'не указана') . "\n"; + if (!empty($matchInfo)) { + echo " " . implode(', ', $matchInfo) . "\n"; + } + echo "\n"; + } + } + } else { + echo " ❌ Версии не найдены (возможно, versioning не включен)\n"; + } + } catch (\Aws\Exception\AwsException $e) { + if ($e->getAwsErrorCode() == 'AccessDenied' || strpos($e->getMessage(), 'versioning') !== false) { + echo " ⚠️ Версионирование не включено или нет доступа: " . $e->getAwsErrorCode() . "\n"; + } else { + echo " ❌ Ошибка: " . $e->getMessage() . "\n"; + } + } + echo "\n"; + + // 2. Проверяем delete markers + echo " 2. Проверка delete markers...\n"; + try { + $deleteMarkers = $s3Client->listObjectVersions([ + 'Bucket' => $s3Bucket, + 'Prefix' => $s3Key, + 'MaxKeys' => 100 + ]); + + if (isset($deleteMarkers['DeleteMarkers']) && !empty($deleteMarkers['DeleteMarkers'])) { + echo " ✅ Найдено delete markers: " . count($deleteMarkers['DeleteMarkers']) . "\n"; + foreach ($deleteMarkers['DeleteMarkers'] as $marker) { + echo " ⚠️ Delete Marker найден:\n"; + echo " VersionId: " . ($marker['VersionId'] ?? 'не указан') . "\n"; + echo " Дата удаления: " . ($marker['LastModified'] ?? 'не указана') . "\n"; + echo " Ключ: " . ($marker['Key'] ?? 'не указан') . "\n"; + echo "\n"; + echo " 💡 Файл был удален, но можно восстановить, удалив delete marker\n"; + } + } else { + echo " ❌ Delete markers не найдены\n"; + } + } catch (\Aws\Exception\AwsException $e) { + echo " ⚠️ Ошибка при проверке delete markers: " . $e->getAwsErrorCode() . "\n"; + } + echo "\n"; + + // 3. Проверяем папки с названиями типа trash, deleted, recycle и т.д. + echo " 3. Поиск в папках корзины...\n"; + $trashPrefixes = [ + 'trash/', + 'deleted/', + 'recycle/', + '.trash/', + 'deleted_files/', + 'removed/', + ]; + + $foundInTrash = false; + foreach ($trashPrefixes as $trashPrefix) { + try { + $objects = $s3Client->listObjectsV2([ + 'Bucket' => $s3Bucket, + 'Prefix' => $trashPrefix, + 'MaxKeys' => 1000 + ]); + + if (isset($objects['Contents'])) { + foreach ($objects['Contents'] as $object) { + $key = $object['Key']; + + // Ищем файлы с ID документа или похожим размером + if (strpos($key, (string)$docId) !== false || + (isset($object['Size']) && abs($object['Size'] - $targetSize) <= 100)) { + + try { + $headResult = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $key + ]); + + $fileEtag = isset($headResult['ETag']) ? trim($headResult['ETag'], '"') : null; + + echo " ✅ НАЙДЕН в $trashPrefix:\n"; + echo " Путь: $key\n"; + echo " Размер: " . number_format($headResult['ContentLength'] / 1024, 2) . " KB\n"; + echo " ETag: " . ($fileEtag ?: 'не указан') . "\n"; + + if ($fileEtag === $targetEtag) { + echo " ✅ ТОЧНОЕ СОВПАДЕНИЕ ПО ETAG!\n"; + } + + $foundInTrash = true; + echo "\n"; + } catch (\Aws\Exception\AwsException $e) { + // Пропускаем + } + } + } + } + } catch (\Aws\Exception\AwsException $e) { + // Пропускаем ошибки + } + } + + if (!$foundInTrash) { + echo " ❌ Файлы не найдены в папках корзины\n"; + } + echo "\n"; + + // 4. Проверяем, может быть файл был перемещен в другую папку проекта + echo " 4. Поиск по ETag во всех папках проекта...\n"; + $projectId = 384256; // Из предыдущих запросов + + $projectPrefixes = [ + "crm2/CRM_Active_Files/Documents/Project/Гафиев_ООО_ЭДЭКС_$projectId/", + "temp/$projectId/", + ]; + + $foundByEtag = false; + foreach ($projectPrefixes as $prefix) { + try { + $objects = $s3Client->listObjectsV2([ + 'Bucket' => $s3Bucket, + 'Prefix' => $prefix, + 'MaxKeys' => 1000 + ]); + + if (isset($objects['Contents'])) { + foreach ($objects['Contents'] as $object) { + $key = $object['Key']; + + try { + $headResult = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $key + ]); + + $fileEtag = isset($headResult['ETag']) ? trim($headResult['ETag'], '"') : null; + + if ($fileEtag === $targetEtag) { + echo " ✅ НАЙДЕН ПО ETAG в $prefix:\n"; + echo " Путь: $key\n"; + echo " Размер: " . number_format($headResult['ContentLength'] / 1024, 2) . " KB\n"; + echo " ETag: $fileEtag\n"; + echo " 💡 Это точно нужный файл!\n"; + $foundByEtag = true; + echo "\n"; + } + } catch (\Aws\Exception\AwsException $e) { + // Пропускаем + } + } + } + } catch (\Aws\Exception\AwsException $e) { + // Пропускаем ошибки + } + } + + if (!$foundByEtag) { + echo " ❌ Файл с таким ETag не найден в папках проекта\n"; + } + echo "\n"; + } + +} catch (Exception $e) { + echo "ОШИБКА: " . $e->getMessage() . "\n"; + echo "Trace: " . $e->getTraceAsString() . "\n"; +} + diff --git a/search_missing_files.php b/search_missing_files.php new file mode 100644 index 00000000..b970c17b --- /dev/null +++ b/search_missing_files.php @@ -0,0 +1,211 @@ + 384443, 'title' => '11 Доказательство соблюдения претензионного порядк'], + ['id' => 386865, 'title' => '12 Доказательство направления иска ответчику'], + ['id' => 386867, 'title' => '6 Расчет иска Гафиев'], + ['id' => 386869, 'title' => '0 Исковое заявление Марсель Гафиев'], + ['id' => 388119, 'title' => '12.1 Доказательство оплаты направления иска ответч'], + ['id' => 388121, 'title' => '12.2 Доказательство направления иска ответчику'], + ['id' => 389699, 'title' => 'Ходатайство_по_делу_М-1035-2025_1_стр'], + ['id' => 394973, 'title' => 'Решение ГАфиев'], +]; + +echo "Поиск недостающих файлов проекта $projectId в S3\n"; +echo str_repeat("=", 80) . "\n\n"; + +try { + $s3Client = new \Aws\S3\S3Client([ + 'version' => 'latest', + 'region' => $config['s3']['region'], + 'endpoint' => $config['s3']['endpoint'], + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $config['s3']['key'], + 'secret' => $config['s3']['secret'], + ], + 'suppress_php_deprecation_warning' => true + ]); + + // Ищем во всех temp/ папках + echo "1. Поиск во всех temp/ папках...\n"; + $tempPrefixes = []; + + // Получаем список всех temp/ папок + try { + $objects = $s3Client->listObjectsV2([ + 'Bucket' => $s3Bucket, + 'Prefix' => 'temp/', + 'Delimiter' => '/', + 'MaxKeys' => 1000 + ]); + + if (isset($objects['CommonPrefixes'])) { + foreach ($objects['CommonPrefixes'] as $prefix) { + $tempPrefixes[] = $prefix['Prefix']; + } + } + } catch (\Aws\Exception\AwsException $e) { + echo " Ошибка при получении списка temp/ папок: " . $e->getMessage() . "\n"; + } + + echo " Найдено temp/ папок: " . count($tempPrefixes) . "\n\n"; + + $foundFiles = []; + + // Ищем файлы по ID документов во всех temp/ папках + foreach ($missingDocs as $doc) { + $docId = $doc['id']; + echo "Поиск файла для документа $docId: {$doc['title']}\n"; + + $found = false; + + // Ищем в каждой temp/ папке + foreach ($tempPrefixes as $tempPrefix) { + try { + $objects = $s3Client->listObjectsV2([ + 'Bucket' => $s3Bucket, + 'Prefix' => $tempPrefix, + 'MaxKeys' => 1000 + ]); + + if (isset($objects['Contents'])) { + foreach ($objects['Contents'] as $object) { + $key = $object['Key']; + + // Ищем файлы с ID документа или близкими ID + if (strpos($key, (string)$docId) !== false || + strpos($key, (string)($docId - 1)) !== false || + strpos($key, (string)($docId + 1)) !== false || + strpos($key, (string)($docId - 2)) !== false || + strpos($key, (string)($docId + 2)) !== false) { + + // Проверяем доступность + try { + $headResult = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $key + ]); + + echo " ✅ НАЙДЕН в $tempPrefix\n"; + echo " Путь: $key\n"; + echo " Размер: " . number_format($headResult['ContentLength'] / 1024, 2) . " KB\n"; + + $foundFiles[] = [ + 'doc_id' => $docId, + 'doc_title' => $doc['title'], + 's3_key' => $key, + 'size' => $headResult['ContentLength'] + ]; + + $found = true; + break 2; + } catch (\Aws\Exception\AwsException $e) { + // Пропускаем + } + } + } + } + } catch (\Aws\Exception\AwsException $e) { + // Пропускаем ошибки + } + } + + // Также ищем по ключевым словам из названия + if (!$found) { + $keywords = []; + $title = mb_strtolower($doc['title']); + + // Извлекаем ключевые слова + if (strpos($title, 'доказательство') !== false) $keywords[] = 'dokazatelstvo'; + if (strpos($title, 'расчет') !== false) $keywords[] = 'raschet'; + if (strpos($title, 'исковое') !== false) $keywords[] = 'iskovoe'; + if (strpos($title, 'ходатайство') !== false) $keywords[] = 'hodataystvo'; + if (strpos($title, 'решение') !== false) $keywords[] = 'reshenie'; + if (strpos($title, 'оплаты') !== false) $keywords[] = 'oplaty'; + if (strpos($title, 'направления') !== false) $keywords[] = 'napravleniya'; + + // Ищем по ключевым словам во всех temp/ папках + foreach ($tempPrefixes as $tempPrefix) { + try { + $objects = $s3Client->listObjectsV2([ + 'Bucket' => $s3Bucket, + 'Prefix' => $tempPrefix, + 'MaxKeys' => 1000 + ]); + + if (isset($objects['Contents'])) { + foreach ($objects['Contents'] as $object) { + $key = $object['Key']; + $keyLower = mb_strtolower($key); + + foreach ($keywords as $keyword) { + if (strpos($keyLower, $keyword) !== false) { + try { + $headResult = $s3Client->headObject([ + 'Bucket' => $s3Bucket, + 'Key' => $key + ]); + + echo " ✅ НАЙДЕН по ключевому слову в $tempPrefix\n"; + echo " Путь: $key\n"; + echo " Размер: " . number_format($headResult['ContentLength'] / 1024, 2) . " KB\n"; + + $foundFiles[] = [ + 'doc_id' => $docId, + 'doc_title' => $doc['title'], + 's3_key' => $key, + 'size' => $headResult['ContentLength'] + ]; + + $found = true; + break 3; + } catch (\Aws\Exception\AwsException $e) { + // Пропускаем + } + } + } + } + } + } catch (\Aws\Exception\AwsException $e) { + // Пропускаем ошибки + } + } + } + + if (!$found) { + echo " ❌ Файл не найден\n"; + } + echo "\n"; + } + + echo str_repeat("=", 80) . "\n"; + echo "РЕЗУЛЬТАТЫ:\n"; + echo " Найдено файлов: " . count($foundFiles) . " из " . count($missingDocs) . "\n\n"; + + if (!empty($foundFiles)) { + echo "НАЙДЕННЫЕ ФАЙЛЫ:\n"; + foreach ($foundFiles as $file) { + echo " Документ {$file['doc_id']}: {$file['doc_title']}\n"; + echo " Путь в S3: {$file['s3_key']}\n"; + echo " Размер: " . number_format($file['size'] / 1024, 2) . " KB\n\n"; + } + + echo "💡 Эти файлы можно переместить в правильную структуру\n"; + } + +} catch (Exception $e) { + echo "ОШИБКА: " . $e->getMessage() . "\n"; + echo "Trace: " . $e->getTraceAsString() . "\n"; +} + diff --git a/storage/2025/November/week4/398984_1_заявление_потребителя_Селдушев____стр.pdf b/storage/2025/November/week4/398984_1_заявление_потребителя_Селдушев____стр.pdf new file mode 100644 index 00000000..08eefde9 Binary files /dev/null and b/storage/2025/November/week4/398984_1_заявление_потребителя_Селдушев____стр.pdf differ diff --git a/storage/2025/November/week4/399004_1_заявление_потребителя_Селдушев____стр.pdf b/storage/2025/November/week4/399004_1_заявление_потребителя_Селдушев____стр.pdf new file mode 100644 index 00000000..73a2897b Binary files /dev/null and b/storage/2025/November/week4/399004_1_заявление_потребителя_Селдушев____стр.pdf differ diff --git a/storage/2025/November/week4/399012_1_заявление_потребителя_Селдушев____стр.pdf b/storage/2025/November/week4/399012_1_заявление_потребителя_Селдушев____стр.pdf new file mode 100644 index 00000000..ec2233bd Binary files /dev/null and b/storage/2025/November/week4/399012_1_заявление_потребителя_Селдушев____стр.pdf differ diff --git a/storage/2025/November/week4/399064_Ответ_на_досудебную_претензию_Храмов_Антон_Николаевич_5.pdf b/storage/2025/November/week4/399064_Ответ_на_досудебную_претензию_Храмов_Антон_Николаевич_5.pdf new file mode 100644 index 00000000..113a9731 Binary files /dev/null and b/storage/2025/November/week4/399064_Ответ_на_досудебную_претензию_Храмов_Антон_Николаевич_5.pdf differ diff --git a/storage/2025/November/week4/399089_1_заявление_потребителя_Селдушев____стр.pdf b/storage/2025/November/week4/399089_1_заявление_потребителя_Селдушев____стр.pdf new file mode 100644 index 00000000..681a1336 Binary files /dev/null and b/storage/2025/November/week4/399089_1_заявление_потребителя_Селдушев____стр.pdf differ diff --git a/storage/2025/November/week4/399139_piev_45271761160546.pdf b/storage/2025/November/week4/399139_piev_45271761160546.pdf new file mode 100644 index 00000000..2c9715fe Binary files /dev/null and b/storage/2025/November/week4/399139_piev_45271761160546.pdf differ diff --git a/storage/2025/November/week4/399159_1_заявление_потребителя_Згурский_____стр.pdf b/storage/2025/November/week4/399159_1_заявление_потребителя_Згурский_____стр.pdf new file mode 100644 index 00000000..e9dd8322 Binary files /dev/null and b/storage/2025/November/week4/399159_1_заявление_потребителя_Згурский_____стр.pdf differ diff --git a/storage/2025/November/week4/399164_Доказательство_проведение_претензионной_работы__Згурский__Борис_2_стр.pdf b/storage/2025/November/week4/399164_Доказательство_проведение_претензионной_работы__Згурский__Борис_2_стр.pdf new file mode 100644 index 00000000..22270874 Binary files /dev/null and b/storage/2025/November/week4/399164_Доказательство_проведение_претензионной_работы__Згурский__Борис_2_стр.pdf differ diff --git a/storage/2025/November/week4/399221_1_заявление_потребителя_Селдушев____стр.pdf b/storage/2025/November/week4/399221_1_заявление_потребителя_Селдушев____стр.pdf new file mode 100644 index 00000000..09276e8a Binary files /dev/null and b/storage/2025/November/week4/399221_1_заявление_потребителя_Селдушев____стр.pdf differ diff --git a/storage/2025/November/week4/399316_1_заявление_потребителя_Селдушев____стр.pdf b/storage/2025/November/week4/399316_1_заявление_потребителя_Селдушев____стр.pdf new file mode 100644 index 00000000..2e1ad7f3 Binary files /dev/null and b/storage/2025/November/week4/399316_1_заявление_потребителя_Селдушев____стр.pdf differ diff --git a/storage/2025/November/week4/399318_Ходатайство_по_делу_.pdf b/storage/2025/November/week4/399318_Ходатайство_по_делу_.pdf new file mode 100644 index 00000000..9d0bf556 Binary files /dev/null and b/storage/2025/November/week4/399318_Ходатайство_по_делу_.pdf differ diff --git a/storage/2025/November/week4/399382_1_заявление_потребителя_Селдушев____стр.pdf b/storage/2025/November/week4/399382_1_заявление_потребителя_Селдушев____стр.pdf new file mode 100644 index 00000000..9ed01846 Binary files /dev/null and b/storage/2025/November/week4/399382_1_заявление_потребителя_Селдушев____стр.pdf differ diff --git a/storage/2025/November/week4/399432_Ходатайство_по_делу_.pdf b/storage/2025/November/week4/399432_Ходатайство_по_делу_.pdf new file mode 100644 index 00000000..c96f3d5c Binary files /dev/null and b/storage/2025/November/week4/399432_Ходатайство_по_делу_.pdf differ diff --git a/test/LanguageManager/Workflow2 b/test/LanguageManager/Workflow2 index ced6c8e9..260a6f32 100644 --- a/test/LanguageManager/Workflow2 +++ b/test/LanguageManager/Workflow2 @@ -1 +1 @@ -2025-11-21 14:05:08 \ No newline at end of file +2025-11-28 14:40:08 \ No newline at end of file diff --git a/test/templates_c/v7/12fc8a5f5653a1bb6de7b45a4b2382af661b0b91.file.ReportFilters.tpl.php b/test/templates_c/v7/12fc8a5f5653a1bb6de7b45a4b2382af661b0b91.file.ReportFilters.tpl.php new file mode 100644 index 00000000..c1ac0158 --- /dev/null +++ b/test/templates_c/v7/12fc8a5f5653a1bb6de7b45a4b2382af661b0b91.file.ReportFilters.tpl.php @@ -0,0 +1,90 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '12fc8a5f5653a1bb6de7b45a4b2382af661b0b91' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/ReportFilters.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '958547709692477b75edbb5-16336030', + 'function' => + array ( + ), + 'variables' => + array ( + 'REL_FIELDS' => 0, + 'BLOCKJS_STD' => 0, + 'VIEW' => 0, + 'MODULE' => 0, + 'REPORTTYPE' => 0, + 'display_summaries_filter' => 0, + 'SUMMARIES_CRITERIA' => 0, + 'COLUMN_INDEX' => 0, + 'COLUMN_CRITERIA' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477b760a05', +),false); /*/%%SmartyHeaderCode%%*/?> + + + + +getSubTemplate ('modules/ITS4YouReports/AdvanceFilter.tpl', $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> + + +tpl_vars['BLOCKJS_STD']->value;?> + + + + + + +tpl_vars['VIEW']->value){?>
getSubTemplate ('modules/ITS4YouReports/FiltersCriteria.tpl', $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> +
tpl_vars['VIEW']->value){?>tpl_vars["display_summaries_filter"] = new Smarty_variable("display:block;", null, 0);?>tpl_vars['REPORTTYPE']->value=="tabular"){?>tpl_vars["display_summaries_filter"] = new Smarty_variable("display:none;", null, 0);?>
tpl_vars['COLUMN_CRITERIA'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->_loop = false; + $_smarty_tpl->tpl_vars['COLUMN_INDEX'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['SUMMARIES_CRITERIA']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->key => $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->value){ +$_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->_loop = true; + $_smarty_tpl->tpl_vars['COLUMN_INDEX']->value = $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->key; +?>tpl_vars['COLUMN_CRITERIA'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->_loop = false; + $_smarty_tpl->tpl_vars['COLUMN_INDEX'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['SUMMARIES_CRITERIA']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->key => $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->value){ +$_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->_loop = true; + $_smarty_tpl->tpl_vars['COLUMN_INDEX']->value = $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->key; +?>
tpl_vars['VIEW']->value){?>
+ \ No newline at end of file diff --git a/test/templates_c/v7/2eb99c07ae66dc414eb65f68f69ad309d1bccc60.file.ListViewPreProcess.tpl.php b/test/templates_c/v7/2eb99c07ae66dc414eb65f68f69ad309d1bccc60.file.ListViewPreProcess.tpl.php new file mode 100644 index 00000000..f54b1fe1 --- /dev/null +++ b/test/templates_c/v7/2eb99c07ae66dc414eb65f68f69ad309d1bccc60.file.ListViewPreProcess.tpl.php @@ -0,0 +1,60 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '2eb99c07ae66dc414eb65f68f69ad309d1bccc60' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/Reports/ListViewPreProcess.tpl', + 1 => 1711810494, + 2 => 'file', + ), + ), + 'nocache_hash' => '1460024140692477a64fd144-36946999', + 'function' => + array ( + ), + 'variables' => + array ( + 'MODULE' => 0, + 'CURRENT_USER_MODEL' => 0, + 'LEFTPANELHIDE' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477a6535e2', +),false); /*/%%SmartyHeaderCode%%*/?> + + +getSubTemplate ("modules/Vtiger/partials/Topbar.tpl", $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> + + +
+
+ getSubTemplate (vtemplate_path("partials/SidebarHeader.tpl",$_smarty_tpl->tpl_vars['MODULE']->value), $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> + + getSubTemplate (vtemplate_path("ModuleHeader.tpl",$_smarty_tpl->tpl_vars['MODULE']->value), $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> + +
+
+ + +
+ tpl_vars['LEFTPANELHIDE'] = new Smarty_variable($_smarty_tpl->tpl_vars['CURRENT_USER_MODEL']->value->get('leftpanelhide'), null, 0);?> +
+ +
+ + +
\ No newline at end of file diff --git a/test/templates_c/v7/31a3a7b4c69f4c31aa2e85b34477ebeb721b58dc.file.ReportContents.tpl.php b/test/templates_c/v7/31a3a7b4c69f4c31aa2e85b34477ebeb721b58dc.file.ReportContents.tpl.php new file mode 100644 index 00000000..45a209fe --- /dev/null +++ b/test/templates_c/v7/31a3a7b4c69f4c31aa2e85b34477ebeb721b58dc.file.ReportContents.tpl.php @@ -0,0 +1,81 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '31a3a7b4c69f4c31aa2e85b34477ebeb721b58dc' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/ReportContents.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '1782494867692477b7cbd261-18785334', + 'function' => + array ( + ), + 'variables' => + array ( + 'NEW_COUNT' => 0, + 'DATE_FILTERS' => 0, + 'REPORTTYPE' => 0, + 'CALCULATION_FIELDS' => 0, + 'MODULE' => 0, + 'CALCULATION_FIELD' => 0, + 'CALCULATION_FIELD_KEYS' => 0, + 'ESCAPE_CHAR' => 0, + 'FIELD_IMPLODE' => 0, + 'FIELD_LABEL' => 0, + 'MODULE_NAME' => 0, + 'CALCULATION_VALUE' => 0, + 'DATA' => 0, + 'VALUES' => 0, + 'VALUE' => 0, + 'LIMIT_EXCEEDED' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477b7cd3e5', +),false); /*/%%SmartyHeaderCode%%*/?> + + +value);?> +' />value));?> +' />value;?> +' />
tpl_vars['CALCULATION_FIELDS']->value)){?>tpl_vars['ESCAPE_CHAR'] = new Smarty_variable(array('_SUM','_AVG','_MIN','_MAX'), null, 0);?>tpl_vars['CALCULATION_FIELD'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['CALCULATION_FIELD']->_loop = false; + $_smarty_tpl->tpl_vars['index'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['CALCULATION_FIELDS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['CALCULATION_FIELD']->key => $_smarty_tpl->tpl_vars['CALCULATION_FIELD']->value){ +$_smarty_tpl->tpl_vars['CALCULATION_FIELD']->_loop = true; + $_smarty_tpl->tpl_vars['index']->value = $_smarty_tpl->tpl_vars['CALCULATION_FIELD']->key; +?>tpl_vars['CALCULATION_FIELD_KEYS'] = new Smarty_variable(array_keys($_smarty_tpl->tpl_vars['CALCULATION_FIELD']->value), null, 0);?>tpl_vars['CALCULATION_FIELD_KEYS'] = new Smarty_variable(smarty_modifier_replace($_smarty_tpl->tpl_vars['CALCULATION_FIELD_KEYS']->value,$_smarty_tpl->tpl_vars['ESCAPE_CHAR']->value,''), null, 0);?>tpl_vars['FIELD_IMPLODE'] = new Smarty_variable(explode('_',$_smarty_tpl->tpl_vars['CALCULATION_FIELD_KEYS']->value['0']), null, 0);?>tpl_vars['MODULE_NAME'] = new Smarty_variable($_smarty_tpl->tpl_vars['FIELD_IMPLODE']->value['0'], null, 0);?>tpl_vars['FIELD_LABEL'] = new Smarty_variable(implode(" ",$_smarty_tpl->tpl_vars['FIELD_IMPLODE']->value), null, 0);?>tpl_vars['FIELD_LABEL'] = new Smarty_variable(smarty_modifier_replace($_smarty_tpl->tpl_vars['FIELD_LABEL']->value,$_smarty_tpl->tpl_vars['MODULE_NAME']->value,''), null, 0);?>tpl_vars['CALCULATION_VALUE'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['CALCULATION_VALUE']->_loop = false; + $_from = $_smarty_tpl->tpl_vars['CALCULATION_FIELD']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['CALCULATION_VALUE']->key => $_smarty_tpl->tpl_vars['CALCULATION_VALUE']->value){ +$_smarty_tpl->tpl_vars['CALCULATION_VALUE']->_loop = true; +?>
tpl_vars['MODULE']->value);?> +tpl_vars['MODULE']->value);?> +tpl_vars['MODULE']->value);?> +tpl_vars['MODULE']->value);?> +tpl_vars['MODULE']->value);?> +
tpl_vars['MODULE_NAME']->value,$_smarty_tpl->tpl_vars['MODULE']->value);?> + tpl_vars['FIELD_LABEL']->value,$_smarty_tpl->tpl_vars['MODULE']->value);?> +tpl_vars['CALCULATION_VALUE']->value;?> +
tpl_vars['DATA']->value!=''){?>tpl_vars['HEADERS'] = new Smarty_variable($_smarty_tpl->tpl_vars['DATA']->value[0], null, 0);?>tpl_vars['VALUES'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['VALUES']->_loop = false; + $_from = $_smarty_tpl->tpl_vars['DATA']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['VALUES']->key => $_smarty_tpl->tpl_vars['VALUES']->value){ +$_smarty_tpl->tpl_vars['VALUES']->_loop = true; +?>tpl_vars['VALUE'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['VALUE']->_loop = false; + $_smarty_tpl->tpl_vars['NAME'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['VALUES']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['VALUE']->key => $_smarty_tpl->tpl_vars['VALUE']->value){ +$_smarty_tpl->tpl_vars['VALUE']->_loop = true; + $_smarty_tpl->tpl_vars['NAME']->value = $_smarty_tpl->tpl_vars['VALUE']->key; +?>tpl_vars['VALUE']->value;?> +tpl_vars['LIMIT_EXCEEDED']->value){?>
tpl_vars['MODULE']->value);?> + tpl_vars['MODULE']->value);?> +
tpl_vars['MODULE']->value);?> +
+ \ No newline at end of file diff --git a/test/templates_c/v7/360fc795e14e13a071b098fb502a6472dc052979.file.ListViewPreProcess.tpl.php b/test/templates_c/v7/360fc795e14e13a071b098fb502a6472dc052979.file.ListViewPreProcess.tpl.php new file mode 100644 index 00000000..9828a100 --- /dev/null +++ b/test/templates_c/v7/360fc795e14e13a071b098fb502a6472dc052979.file.ListViewPreProcess.tpl.php @@ -0,0 +1,36 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '360fc795e14e13a071b098fb502a6472dc052979' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/ListViewPreProcess.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '1732611834692477aeab7798-74083737', + 'function' => + array ( + ), + 'variables' => + array ( + 'MODULE' => 0, + 'CURRENT_USER_MODEL' => 0, + 'LEFTPANELHIDE' => 0, + 'NO_LICENSE' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477aeac194', +),false); /*/%%SmartyHeaderCode%%*/?> + +getSubTemplate ("modules/Vtiger/partials/Topbar.tpl", $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> +
getSubTemplate (vtemplate_path("partials/SidebarHeader.tpl",$_smarty_tpl->tpl_vars['MODULE']->value), $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> +getSubTemplate (vtemplate_path("ModuleHeader.tpl",$_smarty_tpl->tpl_vars['MODULE']->value), $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> +
tpl_vars['LEFTPANELHIDE'] = new Smarty_variable($_smarty_tpl->tpl_vars['CURRENT_USER_MODEL']->value->get('leftpanelhide'), null, 0);?>
\ No newline at end of file diff --git a/test/templates_c/v7/3ad3f9fdfca9e31643826093b780e2802f5dde65.file.MoreCurrenciesList.tpl.php b/test/templates_c/v7/3ad3f9fdfca9e31643826093b780e2802f5dde65.file.MoreCurrenciesList.tpl.php new file mode 100644 index 00000000..b4aa9f74 --- /dev/null +++ b/test/templates_c/v7/3ad3f9fdfca9e31643826093b780e2802f5dde65.file.MoreCurrenciesList.tpl.php @@ -0,0 +1,142 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '3ad3f9fdfca9e31643826093b780e2802f5dde65' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/Products/MoreCurrenciesList.tpl', + 1 => 1711810494, + 2 => 'file', + ), + ), + 'nocache_hash' => '8912771816926bac84de2c5-62307678', + 'function' => + array ( + ), + 'variables' => + array ( + 'MODULE' => 0, + 'TITLE' => 0, + 'PRICE_DETAILS' => 0, + 'price' => 0, + 'check_value' => 0, + 'disable_value' => 0, + 'USER_MODEL' => 0, + 'base_cur_check' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_6926bac8525f1', +),false); /*/%%SmartyHeaderCode%%*/?> + + + +
+ +
\ No newline at end of file diff --git a/test/templates_c/v7/3c98f6b3d313a8b82b05eb7bcd33de4bd16354a4.file.ListViewContents.tpl.php b/test/templates_c/v7/3c98f6b3d313a8b82b05eb7bcd33de4bd16354a4.file.ListViewContents.tpl.php new file mode 100644 index 00000000..49e7d47f --- /dev/null +++ b/test/templates_c/v7/3c98f6b3d313a8b82b05eb7bcd33de4bd16354a4.file.ListViewContents.tpl.php @@ -0,0 +1,164 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '3c98f6b3d313a8b82b05eb7bcd33de4bd16354a4' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/Reports/ListViewContents.tpl', + 1 => 1711810495, + 2 => 'file', + ), + ), + 'nocache_hash' => '1148052082692477a65f8b85-18822179', + 'function' => + array ( + ), + 'variables' => + array ( + 'CURRENT_USER_MODEL' => 0, + 'LEFTPANELHIDE' => 0, + 'VIEW' => 0, + 'VIEWID' => 0, + 'PAGING_MODEL' => 0, + 'OPERATOR' => 0, + 'LISTVIEW_COUNT' => 0, + 'PAGE_NUMBER' => 0, + 'LISTVIEW_ENTRIES_COUNT' => 0, + 'SEARCH_DETAILS' => 0, + 'NO_SEARCH_PARAMS_CACHE' => 0, + 'ORDER_BY' => 0, + 'SORT_ORDER' => 0, + 'LIST_HEADER_FIELDS' => 0, + 'CURRENT_TAG' => 0, + 'FOLDER_ID' => 0, + 'FOLDER_VALUE' => 0, + 'VIEWNAME' => 0, + 'SEARCH_MODE_RESULTS' => 0, + 'MODULE' => 0, + 'LISTVIEW_MODEL' => 0, + 'LISTVIEW_HEADERS' => 0, + 'COLUMN_NAME' => 0, + 'LISTVIEW_HEADER_KEY' => 0, + 'NEXT_SORT_ORDER' => 0, + 'FASORT_IMAGE' => 0, + 'MODULE_MODEL' => 0, + 'LISTVIEW_HEADER' => 0, + 'DATA_TYPE' => 0, + 'FIELD_INFO' => 0, + 'PICKLIST_VALUES' => 0, + 'PICKLIST_LABEL' => 0, + 'PICKLIST_KEY' => 0, + 'SEARCH_VALUES' => 0, + 'ICON_CLASS' => 0, + 'LISTVIEW_ENTRIES' => 0, + 'LISTVIEW_ENTRY' => 0, + 'LISTVIEW_HEADERNAME' => 0, + 'LISTVIEW_ENTRY_RAWVALUE' => 0, + 'LISTVIEW_ENTRY_VALUE' => 0, + 'COLSPAN_WIDTH' => 0, + 'IS_MODULE_EDITABLE' => 0, + 'LIST_VIEW_MODEL' => 0, + 'SINGLE_MODULE' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477a662cc5', +),false); /*/%%SmartyHeaderCode%%*/?> + + +
tpl_vars['LEFTPANELHIDE'] = new Smarty_variable($_smarty_tpl->tpl_vars['CURRENT_USER_MODEL']->value->get('leftpanelhide'), null, 0);?>
value;?> +'/>tpl_vars['SEARCH_MODE_RESULTS']->value){?>getSubTemplate (vtemplate_path("ListViewActions.tpl",$_smarty_tpl->tpl_vars['MODULE']->value), $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> +
tpl_vars['VIEWNAME']->value;?> +tpl_vars["LISTVIEW_HEADERS"] = new Smarty_variable($_smarty_tpl->tpl_vars['LISTVIEW_MODEL']->value->getListViewHeadersForVtiger7($_tmp1), null, 0);?>tpl_vars['LISTVIEW_HEADER'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->_loop = false; + $_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['LISTVIEW_HEADERS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->key => $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->value){ +$_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->_loop = true; + $_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value = $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->key; +?>tpl_vars['MODULE_MODEL']->value->isQuickSearchEnabled()&&!$_smarty_tpl->tpl_vars['SEARCH_MODE_RESULTS']->value){?>tpl_vars['LISTVIEW_HEADER'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->_loop = false; + $_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['LISTVIEW_HEADERS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->key => $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->value){ +$_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->_loop = true; + $_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value = $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->key; +?>tpl_vars['LISTVIEW_ENTRY'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['LISTVIEW_ENTRY']->_loop = false; + $_from = $_smarty_tpl->tpl_vars['LISTVIEW_ENTRIES']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} + $_smarty_tpl->tpl_vars['smarty']->value['foreach']['listview']['index']=-1; +foreach ($_from as $_smarty_tpl->tpl_vars['LISTVIEW_ENTRY']->key => $_smarty_tpl->tpl_vars['LISTVIEW_ENTRY']->value){ +$_smarty_tpl->tpl_vars['LISTVIEW_ENTRY']->_loop = true; + $_smarty_tpl->tpl_vars['smarty']->value['foreach']['listview']['index']++; +?>value->getId();?> +' data-recordUrl='tpl_vars['LISTVIEW_ENTRY']->value->getDetailViewUrl();?> +' id="tpl_vars['MODULE']->value;?> +_listView_row_getVariable('smarty')->value['foreach']['listview']['index']+1;?> +">tpl_vars['LISTVIEW_HEADER'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->_loop = false; + $_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['LISTVIEW_HEADERS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->key => $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->value){ +$_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->_loop = true; + $_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value = $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->key; +?>tpl_vars['LISTVIEW_HEADERNAME'] = new Smarty_variable($_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value, null, 0);?>tpl_vars['LISTVIEW_ENTRY_RAWVALUE'] = new Smarty_variable($_smarty_tpl->tpl_vars['LISTVIEW_ENTRY']->value->getRaw($_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value), null, 0);?>tpl_vars['LISTVIEW_ENTRY_VALUE'] = new Smarty_variable($_smarty_tpl->tpl_vars['LISTVIEW_ENTRY']->value->get($_smarty_tpl->tpl_vars['LISTVIEW_HEADERNAME']->value), null, 0);?>tpl_vars['LISTVIEW_ENTRIES_COUNT']->value=='0'){?>tpl_vars['LISTVIEW_HEADERS']->value);?> +tpl_vars['COLSPAN_WIDTH'] = new Smarty_variable($_tmp2+1, null, 0);?>
tpl_vars['SEARCH_MODE_RESULTS']->value){?>
tpl_vars['SEARCH_MODE_RESULTS']->value){?>tpl_vars['MODULE']->value);?> +
tpl_vars['COLUMN_NAME']->value==$_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value){?> nowrap="nowrap" >tpl_vars['COLUMN_NAME']->value==$_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value){?> tpl_vars['LISTVIEW_HEADERS']->value[$_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value]['label'],$_smarty_tpl->tpl_vars['MODULE']->value);?> + tpl_vars['COLUMN_NAME']->value==$_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value){?>
tpl_vars["DATA_TYPE"] = new Smarty_variable($_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->value['type'], null, 0);?>tpl_vars['DATA_TYPE']->value=='string'){?>
value, ENT_QUOTES, 'UTF-8', true);?> +'/>
tpl_vars['DATA_TYPE']->value=='picklist'){?>tpl_vars['PICKLIST_VALUES'] = new Smarty_variable(Reports_Field_Model::getPicklistValueByField($_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value), null, 0);?>tpl_vars['SEARCH_VALUES'] = new Smarty_variable(explode(',',$_smarty_tpl->tpl_vars['SEARCH_DETAILS']->value[$_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value]['searchValue']), null, 0);?>
getSubTemplate (vtemplate_path("ListViewRecordActions.tpl",$_smarty_tpl->tpl_vars['MODULE']->value), $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> +tpl_vars['LISTVIEW_HEADERNAME']->value=='reporttype'){?>tpl_vars['LISTVIEW_ENTRY_VALUE']->value=='summary'||$_smarty_tpl->tpl_vars['LISTVIEW_ENTRY_VALUE']->value=='tabular'){?>
tpl_vars['LISTVIEW_ENTRY_VALUE']->value=='chart'){?>
tpl_vars['LISTVIEW_HEADERNAME']->value=='primarymodule'){?>tpl_vars['LISTVIEW_ENTRY_VALUE']->value,$_smarty_tpl->tpl_vars['LISTVIEW_ENTRY_VALUE']->value)));?> +tpl_vars['LISTVIEW_HEADERNAME']->value=='foldername'){?>tpl_vars['LISTVIEW_ENTRY_VALUE']->value,$_smarty_tpl->tpl_vars['MODULE']->value));?> +tpl_vars['LISTVIEW_ENTRY_VALUE']->value);?> +
tpl_vars['SINGLE_MODULE'] = new Smarty_variable("SINGLE_".($_smarty_tpl->tpl_vars['MODULE']->value), null, 0);?> + tpl_vars['MODULE']->value,$_smarty_tpl->tpl_vars['MODULE']->value);?> + +.tpl_vars['IS_MODULE_EDITABLE']->value){?> + tpl_vars['MODULE']->value,'Import')&&$_smarty_tpl->tpl_vars['LIST_VIEW_MODEL']->value->isImportEnabled()){?> tpl_vars['MODULE']->value);?> + tpl_vars['MODULE']->value);?> + tpl_vars['MODULE']->value,$_smarty_tpl->tpl_vars['MODULE']->value);?> +tpl_vars['SINGLE_MODULE']->value,$_smarty_tpl->tpl_vars['MODULE']->value);?> +
\ No newline at end of file diff --git a/test/templates_c/v7/4430d7df5d1ac99e15a492ff3f5f7ca89e8e9419.file.ListViewRecordActions.tpl.php b/test/templates_c/v7/4430d7df5d1ac99e15a492ff3f5f7ca89e8e9419.file.ListViewRecordActions.tpl.php new file mode 100644 index 00000000..a9831292 --- /dev/null +++ b/test/templates_c/v7/4430d7df5d1ac99e15a492ff3f5f7ca89e8e9419.file.ListViewRecordActions.tpl.php @@ -0,0 +1,54 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '4430d7df5d1ac99e15a492ff3f5f7ca89e8e9419' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/ListViewRecordActions.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '1541815961692477aebe2c22-49358808', + 'function' => + array ( + ), + 'variables' => + array ( + 'SEARCH_MODE_RESULTS' => 0, + 'LISTVIEW_ENTRY' => 0, + 'QUICK_PREVIEW_ENABLED' => 0, + 'SELECTED_MENU_CATEGORY' => 0, + 'MODULE' => 0, + 'MODULE_MODEL' => 0, + 'STARRED' => 0, + 'RECORD_ACTIONS' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477aebf22c', +),false); /*/%%SmartyHeaderCode%%*/?> + +
tpl_vars['SEARCH_MODE_RESULTS']->value){?>tpl_vars['LISTVIEW_ENTRY']->value->get('starred')=='Yes'){?>tpl_vars['STARRED'] = new Smarty_variable(true, null, 0);?>tpl_vars['STARRED'] = new Smarty_variable(false, null, 0);?>tpl_vars['QUICK_PREVIEW_ENABLED']->value=='true'){?>tpl_vars['MODULE_MODEL']->value->isStarredEnabled()){?>
+ \ No newline at end of file diff --git a/test/templates_c/v7/4b187bb8283c39a94fc1145fe0286765c9e20728.file.FiltersCriteria.tpl.php b/test/templates_c/v7/4b187bb8283c39a94fc1145fe0286765c9e20728.file.FiltersCriteria.tpl.php new file mode 100644 index 00000000..8a494ce9 --- /dev/null +++ b/test/templates_c/v7/4b187bb8283c39a94fc1145fe0286765c9e20728.file.FiltersCriteria.tpl.php @@ -0,0 +1,201 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '4b187bb8283c39a94fc1145fe0286765c9e20728' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/FiltersCriteria.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '1046647003692477b76479f4-96758090', + 'function' => + array ( + ), + 'variables' => + array ( + 'std_filter_columns' => 0, + 'std_filter_criteria' => 0, + 'SEL_FIELDS' => 0, + 'BLOCKJS_STD' => 0, + 'REL_FIELDS' => 0, + 'DISPLAY_FILTER_HEADER' => 0, + 'MODULE' => 0, + 'CRITERIA_GROUPS' => 0, + 'GROUP_CRITERIA' => 0, + 'GROUP_COLUMNS' => 0, + 'GROUP_ID' => 0, + 'COLUMN_CRITERIA' => 0, + 'FCON_I' => 0, + 'COLUMN_INDEX' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477b7656b3', +),false); /*/%%SmartyHeaderCode%%*/?> + + + + +value;?> +' /> +
tpl_vars['std_filter_criteria']->value;?> +
+value;?> +' /> + + +tpl_vars['BLOCKJS_STD']->value;?> + + + + + + + tpl_vars['DISPLAY_FILTER_HEADER']->value===true){?> + + + + + + + +
+
+ tpl_vars['MODULE']->value);?> +   +
+
+
+ +
+ tpl_vars['FCON_I'] = new Smarty_variable("0", null, 0);?> + + tpl_vars['GROUP_CRITERIA'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['GROUP_CRITERIA']->_loop = false; + $_smarty_tpl->tpl_vars['GROUP_ID'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['CRITERIA_GROUPS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['GROUP_CRITERIA']->key => $_smarty_tpl->tpl_vars['GROUP_CRITERIA']->value){ +$_smarty_tpl->tpl_vars['GROUP_CRITERIA']->_loop = true; + $_smarty_tpl->tpl_vars['GROUP_ID']->value = $_smarty_tpl->tpl_vars['GROUP_CRITERIA']->key; +?> + tpl_vars['GROUP_COLUMNS'] = new Smarty_variable($_smarty_tpl->tpl_vars['GROUP_CRITERIA']->value['columns'], null, 0);?> + + tpl_vars['COLUMN_CRITERIA'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->_loop = false; + $_smarty_tpl->tpl_vars['COLUMN_INDEX'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['GROUP_COLUMNS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->key => $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->value){ +$_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->_loop = true; + $_smarty_tpl->tpl_vars['COLUMN_INDEX']->value = $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->key; +?> + + tpl_vars['COLUMN_CRITERIA']->value['column_condition']!=''){?> + value['column_condition'];?> +' /> + + tpl_vars['FCON_I'] = new Smarty_variable($_smarty_tpl->tpl_vars['FCON_I']->value+1, null, 0);?> + + tpl_vars['COLUMN_CRITERIA'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->_loop = false; + $_smarty_tpl->tpl_vars['COLUMN_INDEX'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['GROUP_COLUMNS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->key => $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->value){ +$_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->_loop = true; + $_smarty_tpl->tpl_vars['COLUMN_INDEX']->value = $_smarty_tpl->tpl_vars['COLUMN_CRITERIA']->key; +?> + + + tpl_vars['GROUP_CRITERIA']->_loop) { +?> + + tpl_vars['GROUP_CRITERIA'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['GROUP_CRITERIA']->_loop = false; + $_smarty_tpl->tpl_vars['GROUP_ID'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['CRITERIA_GROUPS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['GROUP_CRITERIA']->key => $_smarty_tpl->tpl_vars['GROUP_CRITERIA']->value){ +$_smarty_tpl->tpl_vars['GROUP_CRITERIA']->_loop = true; + $_smarty_tpl->tpl_vars['GROUP_ID']->value = $_smarty_tpl->tpl_vars['GROUP_CRITERIA']->key; +?> + + +
+ tpl_vars['DISPLAY_FILTER_HEADER']->value==true){?> +
+ +
+ +
+ + + +getSubTemplate ('modules/ITS4YouReports/FieldExpressions.tpl', $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> + + + \ No newline at end of file diff --git a/test/templates_c/v7/4c5c49a7b2a914c873143ca475ba21cc7664d14a.file.SidebarHeader.tpl.php b/test/templates_c/v7/4c5c49a7b2a914c873143ca475ba21cc7664d14a.file.SidebarHeader.tpl.php new file mode 100644 index 00000000..ddd2bc96 --- /dev/null +++ b/test/templates_c/v7/4c5c49a7b2a914c873143ca475ba21cc7664d14a.file.SidebarHeader.tpl.php @@ -0,0 +1,40 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '4c5c49a7b2a914c873143ca475ba21cc7664d14a' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/Reports/partials/SidebarHeader.tpl', + 1 => 1711810495, + 2 => 'file', + ), + ), + 'nocache_hash' => '1450369455692477a6596a39-18844481', + 'function' => + array ( + ), + 'variables' => + array ( + 'SELECTED_MENU_CATEGORY' => 0, + 'MODULE' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477a6599c8', +),false); /*/%%SmartyHeaderCode%%*/?> + + +tpl_vars['APP_IMAGE_MAP'] = new Smarty_variable(Vtiger_MenuStructure_Model::getAppIcons(), null, 0);?> + +
+
+ +
+
+ +getSubTemplate ("modules/Vtiger/partials/SidebarAppMenu.tpl", $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> + \ No newline at end of file diff --git a/test/templates_c/v7/5a09fbc29b8e39665fcb79febcd895db80e4e1c6.file.SidebarHeader.tpl.php b/test/templates_c/v7/5a09fbc29b8e39665fcb79febcd895db80e4e1c6.file.SidebarHeader.tpl.php new file mode 100644 index 00000000..a1fd5dc8 --- /dev/null +++ b/test/templates_c/v7/5a09fbc29b8e39665fcb79febcd895db80e4e1c6.file.SidebarHeader.tpl.php @@ -0,0 +1,37 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '5a09fbc29b8e39665fcb79febcd895db80e4e1c6' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/partials/SidebarHeader.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '1868848172692477aeb21c86-76730072', + 'function' => + array ( + ), + 'variables' => + array ( + 'MODULE' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477aeb2616', +),false); /*/%%SmartyHeaderCode%%*/?> + +tpl_vars["APP_IMAGE_MAP"] = new Smarty_variable(array('MARKETING'=>'fa-users','SALES'=>'fa-dot-circle-o','SUPPORT'=>'fa-life-ring','INVENTORY'=>'vicon-inventory','PROJECT'=>'fa-briefcase'), null, 0);?> + +
+
+ +
+
+ +getSubTemplate ("modules/Vtiger/partials/SidebarAppMenu.tpl", $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> + \ No newline at end of file diff --git a/test/templates_c/v7/5c127f5d528c116c3657d6c5fd8e7a78c154b2d5.file.ActivityPicklistFieldSearchView.tpl.php b/test/templates_c/v7/5c127f5d528c116c3657d6c5fd8e7a78c154b2d5.file.ActivityPicklistFieldSearchView.tpl.php new file mode 100644 index 00000000..94984a52 --- /dev/null +++ b/test/templates_c/v7/5c127f5d528c116c3657d6c5fd8e7a78c154b2d5.file.ActivityPicklistFieldSearchView.tpl.php @@ -0,0 +1,48 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '5c127f5d528c116c3657d6c5fd8e7a78c154b2d5' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/Calendar/uitypes/ActivityPicklistFieldSearchView.tpl', + 1 => 1711810495, + 2 => 'file', + ), + ), + 'nocache_hash' => '2162774206924036c0d4a07-96979156', + 'function' => + array ( + ), + 'variables' => + array ( + 'FIELD_MODEL' => 0, + 'FIELD_INFO' => 0, + 'SEARCH_INFO' => 0, + 'PICKLIST_VALUES' => 0, + 'PICKLIST_KEY' => 0, + 'SEARCH_VALUES' => 0, + 'PICKLIST_LABEL' => 0, + 'MODULE' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_6924036c0dcca', +),false); /*/%%SmartyHeaderCode%%*/?> + + +tpl_vars['FIELD_INFO'] = new Smarty_variable($_smarty_tpl->tpl_vars['FIELD_MODEL']->value->getFieldInfo(), null, 0);?>tpl_vars['PICKLIST_VALUES'] = new Smarty_variable($_smarty_tpl->tpl_vars['FIELD_INFO']->value['picklistvalues'], null, 0);?>tpl_vars['FIELD_INFO'] = new Smarty_variable(Vtiger_Util_Helper::toSafeHTML(Zend_Json::encode($_smarty_tpl->tpl_vars['FIELD_INFO']->value)), null, 0);?>tpl_vars['SEARCH_VALUES'] = new Smarty_variable(explode(',',$_smarty_tpl->tpl_vars['SEARCH_INFO']->value['searchValue']), null, 0);?>
+ + \ No newline at end of file diff --git a/test/templates_c/v7/614d2203d3ab8aee98276677dabcbf0257a8d35a.file.ListViewRecordActions.tpl.php b/test/templates_c/v7/614d2203d3ab8aee98276677dabcbf0257a8d35a.file.ListViewRecordActions.tpl.php new file mode 100644 index 00000000..2fc9cd75 --- /dev/null +++ b/test/templates_c/v7/614d2203d3ab8aee98276677dabcbf0257a8d35a.file.ListViewRecordActions.tpl.php @@ -0,0 +1,54 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '614d2203d3ab8aee98276677dabcbf0257a8d35a' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/Reports/ListViewRecordActions.tpl', + 1 => 1711810495, + 2 => 'file', + ), + ), + 'nocache_hash' => '1117150554692477a666a0b4-81395813', + 'function' => + array ( + ), + 'variables' => + array ( + 'SEARCH_MODE_RESULTS' => 0, + 'LISTVIEW_ENTRY' => 0, + 'REPORT_TYPE' => 0, + 'MODULE' => 0, + 'PINNED' => 0, + 'PIN_CLASS' => 0, + 'DASHBOARD_TABS' => 0, + 'TAB_INFO' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477a667f96', +),false); /*/%%SmartyHeaderCode%%*/?> + +
tpl_vars['SEARCH_MODE_RESULTS']->value){?>tpl_vars["REPORT_TYPE"] = new Smarty_variable($_smarty_tpl->tpl_vars['LISTVIEW_ENTRY']->value->get('reporttype'), null, 0);?>tpl_vars['REPORT_TYPE']->value=='chart'){?>tpl_vars["PINNED"] = new Smarty_variable($_smarty_tpl->tpl_vars['LISTVIEW_ENTRY']->value->get('pinned'), null, 0);?>tpl_vars['PINNED']->value!=null&&$_smarty_tpl->tpl_vars['REPORT_TYPE']->value=='chart'){?>tpl_vars['PIN_CLASS'] = new Smarty_variable('vicon-unpin', null, 0);?>tpl_vars['REPORT_TYPE']->value=='chart'){?>tpl_vars['PIN_CLASS'] = new Smarty_variable('vicon-pin', null, 0);?>tpl_vars['REPORT_TYPE']->value=='chart'){?>tpl_vars['DASHBOARD_TABS']->value)>1&&$_smarty_tpl->tpl_vars['PIN_CLASS']->value=='vicon-pin'){?> data-toggle='dropdown'data-dashboard-tab-count='tpl_vars['DASHBOARD_TABS']->value);?> +'>tpl_vars['LISTVIEW_ENTRY']->value->isEditableBySharing()){?>
\ No newline at end of file diff --git a/test/templates_c/v7/697b6e5c33f184d5794c621d6d65fbbb493fecb4.file.DetailViewActions.tpl.php b/test/templates_c/v7/697b6e5c33f184d5794c621d6d65fbbb493fecb4.file.DetailViewActions.tpl.php new file mode 100644 index 00000000..983bee0c --- /dev/null +++ b/test/templates_c/v7/697b6e5c33f184d5794c621d6d65fbbb493fecb4.file.DetailViewActions.tpl.php @@ -0,0 +1,123 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '697b6e5c33f184d5794c621d6d65fbbb493fecb4' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/DetailViewActions.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '72252607692477b7678d01-89848909', + 'function' => + array ( + ), + 'variables' => + array ( + 'DETAILVIEW_ACTIONS' => 0, + 'DETAILVIEW_LINK' => 0, + 'LINK_ICON_CLASS' => 0, + 'LINK_URL' => 0, + 'DASHBOARD_TABS' => 0, + 'REPORT_MODEL' => 0, + 'MODULE' => 0, + 'BTN_I' => 0, + 'LINK_NAME' => 0, + 'TAB_INFO' => 0, + 'COUNT' => 0, + 'COLUMNS_LIMIT' => 0, + 'SUMMARIES_LIMIT' => 0, + 'PDFMakerActive' => 0, + 'IS_TEST_WRITE_ABLE' => 0, + 'DETAILVIEW_LINKS' => 0, + 'LINKNAME' => 0, + 'ICON_CLASS' => 0, + 'MODULENAME' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477b76a1cb', +),false); /*/%%SmartyHeaderCode%%*/?> + +
tpl_vars['BTN_I'] = new Smarty_variable('0', null, 0);?>tpl_vars['DETAILVIEW_LINK'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->_loop = false; + $_from = $_smarty_tpl->tpl_vars['DETAILVIEW_ACTIONS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->key => $_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->value){ +$_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->_loop = true; +?>tpl_vars['LINK_URL'] = new Smarty_variable($_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->value->getUrl(), null, 0);?>tpl_vars['LINK_NAME'] = new Smarty_variable($_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->value->getLabel(), null, 0);?>tpl_vars['LINK_ICON_CLASS'] = new Smarty_variable($_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->value->get('linkiconclass'), null, 0);?>tpl_vars['LINK_ICON_CLASS']->value=='vtGlyph vticon-attach'){?>
tpl_vars['LINK_ICON_CLASS']->value=='vtGlyph vticon-attach'){?>tpl_vars['LINK_ICON_CLASS']->value=='vtGlyph vticon-attach'){?>
tpl_vars['BTN_I'] = new Smarty_variable($_smarty_tpl->tpl_vars['BTN_I']->value+1, null, 0);?>

tpl_vars['REPORT_MODEL']->value->getName();?> +

tpl_vars['MODULE']->value);?> + tpl_vars['COUNT']->value;?> +tpl_vars['COUNT']->value>1000){?> (tpl_vars['MODULE']->value);?> +) (tpl_vars['MODULE']->value);?> +)
tpl_vars['REPORT_MODEL']->value->getReportType()){?>
tpl_vars['MODULE']->value);?> +: (tpl_vars['COLUMNS_LIMIT']->value>0){?>tpl_vars['MODULE']->value);?> + tpl_vars['COLUMNS_LIMIT']->value;?> +tpl_vars['COLUMNS_LIMIT']->value>0&&$_smarty_tpl->tpl_vars['SUMMARIES_LIMIT']->value>0){?>, tpl_vars['COLUMNS_LIMIT']->value==0&&$_smarty_tpl->tpl_vars['SUMMARIES_LIMIT']->value==0){?>tpl_vars['MODULE']->value);?> +tpl_vars['SUMMARIES_LIMIT']->value>0){?>tpl_vars['MODULE']->value);?> + tpl_vars['SUMMARIES_LIMIT']->value;?> +)
tpl_vars['PDFMakerActive']->value!==true){?>tpl_vars['MODULE']->value);?> +tpl_vars['IS_TEST_WRITE_ABLE']->value!==true){?>tpl_vars['MODULE']->value);?> +
tpl_vars['REPORT_MODEL']->value->getReportType()){?>tpl_vars['DETAILVIEW_LINK'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->_loop = false; + $_from = $_smarty_tpl->tpl_vars['DETAILVIEW_LINKS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->key => $_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->value){ +$_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->_loop = true; +?>tpl_vars['LINKNAME'] = new Smarty_variable($_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->value->getLabel(), null, 0);?>tpl_vars['REPORT_MODEL']->value->getReportType()){?>tpl_vars['REPORT_MODEL']->value->getReportType()){?>tpl_vars['DETAILVIEW_LINK'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->_loop = false; + $_from = $_smarty_tpl->tpl_vars['DETAILVIEW_LINKS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->key => $_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->value){ +$_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->_loop = true; +?>tpl_vars['LINKNAME'] = new Smarty_variable($_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->value->getLabel(), null, 0);?>tpl_vars['DETAILVIEW_LINK']->value->get('id')){?>tpl_vars['LINKNAME'] = new Smarty_variable($_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->value->getLabel(), null, 0);?>tpl_vars['DETAILVIEW_LINK'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->_loop = false; + $_from = $_smarty_tpl->tpl_vars['DETAILVIEW_LINKS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->key => $_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->value){ +$_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->_loop = true; +?>tpl_vars['LINKNAME'] = new Smarty_variable($_smarty_tpl->tpl_vars['DETAILVIEW_LINK']->value->getLabel(), null, 0);?>
+ \ No newline at end of file diff --git a/test/templates_c/v7/771116ed665e7d24d34aaf5300b8a5fa0901c7bd.file.ModuleHeader.tpl.php b/test/templates_c/v7/771116ed665e7d24d34aaf5300b8a5fa0901c7bd.file.ModuleHeader.tpl.php new file mode 100644 index 00000000..a0dbd840 --- /dev/null +++ b/test/templates_c/v7/771116ed665e7d24d34aaf5300b8a5fa0901c7bd.file.ModuleHeader.tpl.php @@ -0,0 +1,98 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '771116ed665e7d24d34aaf5300b8a5fa0901c7bd' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/ModuleHeader.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '594469185692477aeb3a632-69823387', + 'function' => + array ( + ), + 'variables' => + array ( + 'MODULE' => 0, + 'MODULE_MODEL' => 0, + 'DEFAULT_FILTER_ID' => 0, + 'CVURL' => 0, + 'DEFAULT_FILTER_URL' => 0, + 'VIEW' => 0, + 'REPORT_NAME' => 0, + 'KM_ID' => 0, + 'KM_NAME' => 0, + 'VIEWNAME' => 0, + 'FOLDERS' => 0, + 'FOLDER' => 0, + 'FOLDERNAME' => 0, + 'NO_LICENSE' => 0, + 'LISTVIEW_LINKS' => 0, + 'LISTVIEW_BASICACTION' => 0, + 'childLinks' => 0, + 'childLink' => 0, + 'ICON_CLASS' => 0, + 'MODULE_NAME' => 0, + 'SETTING' => 0, + 'FIELDS_INFO' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477aeb64b6', +),false); /*/%%SmartyHeaderCode%%*/?> + + +
tpl_vars['MODULE_MODEL'] = new Smarty_variable(Vtiger_Module_Model::getInstance($_smarty_tpl->tpl_vars['MODULE']->value), null, 0);?>tpl_vars['DEFAULT_FILTER_ID'] = new Smarty_variable($_smarty_tpl->tpl_vars['MODULE_MODEL']->value->getDefaultCustomFilter(), null, 0);?>tpl_vars['DEFAULT_FILTER_ID']->value){?>tpl_vars['CVURL'] = new Smarty_variable(("&viewname=").($_smarty_tpl->tpl_vars['DEFAULT_FILTER_ID']->value), null, 0);?>tpl_vars['DEFAULT_FILTER_URL'] = new Smarty_variable(($_smarty_tpl->tpl_vars['MODULE_MODEL']->value->getListViewUrl()).($_smarty_tpl->tpl_vars['CVURL']->value), null, 0);?>tpl_vars['DEFAULT_FILTER_URL'] = new Smarty_variable($_smarty_tpl->tpl_vars['MODULE_MODEL']->value->getListViewUrlWithAllFilter(), null, 0);?>value;?> +'>

tpl_vars['MODULE']->value,$_smarty_tpl->tpl_vars['MODULE']->value);?> +

  tpl_vars['VIEW']->value=='Edit'||$_smarty_tpl->tpl_vars['VIEW']->value=='Detail'){?> tpl_vars['REPORT_NAME']->value){?>tpl_vars['REPORT_NAME']->value;?> + +  tpl_vars['MODULE']->value);?> +tpl_vars['MODULE']->value);?> +  tpl_vars['KM_NAME']->value;?> +  tpl_vars['VIEW']->value,$_smarty_tpl->tpl_vars['MODULE']->value);?> +tpl_vars['VIEW']->value,$_smarty_tpl->tpl_vars['MODULE']->value);?> +

tpl_vars['VIEWNAME']->value){?>tpl_vars['VIEWNAME']->value!='All'){?>tpl_vars['FOLDER'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['FOLDER']->_loop = false; + $_from = $_smarty_tpl->tpl_vars['FOLDERS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['FOLDER']->key => $_smarty_tpl->tpl_vars['FOLDER']->value){ +$_smarty_tpl->tpl_vars['FOLDER']->_loop = true; +?>tpl_vars['FOLDER']->value->getId()==$_smarty_tpl->tpl_vars['VIEWNAME']->value){?>tpl_vars['FOLDERNAME'] = new Smarty_variable($_smarty_tpl->tpl_vars['FOLDER']->value->getName(), null, 0);?>tpl_vars['FOLDERNAME'] = new Smarty_variable(vtranslate('LBL_ALL_REPORTS',$_smarty_tpl->tpl_vars['MODULE']->value), null, 0);?>

   tpl_vars['FOLDERNAME']->value;?> +

tpl_vars['FIELDS_INFO']->value!=null){?>
\ No newline at end of file diff --git a/test/templates_c/v7/7960bbdb89b4793a3078596dd77811a96e017dc9.file.FieldExpressions.tpl.php b/test/templates_c/v7/7960bbdb89b4793a3078596dd77811a96e017dc9.file.FieldExpressions.tpl.php new file mode 100644 index 00000000..2aca5b74 --- /dev/null +++ b/test/templates_c/v7/7960bbdb89b4793a3078596dd77811a96e017dc9.file.FieldExpressions.tpl.php @@ -0,0 +1,33 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '7960bbdb89b4793a3078596dd77811a96e017dc9' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/FieldExpressions.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '1833161144692477b765a8e1-56606492', + 'function' => + array ( + ), + 'variables' => + array ( + 'columnIndex' => 0, + 'MODULE' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477b765d0a', +),false); /*/%%SmartyHeaderCode%%*/?> + +tpl_vars["columnIndex"] = new Smarty_variable("WCCINRW", null, 0);?>
\ No newline at end of file diff --git a/test/templates_c/v7/87d6f754af62470e8cc17e73e06b06d287b55633.file.ListViewActions.tpl.php b/test/templates_c/v7/87d6f754af62470e8cc17e73e06b06d287b55633.file.ListViewActions.tpl.php new file mode 100644 index 00000000..9cbfcb16 --- /dev/null +++ b/test/templates_c/v7/87d6f754af62470e8cc17e73e06b06d287b55633.file.ListViewActions.tpl.php @@ -0,0 +1,48 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '87d6f754af62470e8cc17e73e06b06d287b55633' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/Reports/ListViewActions.tpl', + 1 => 1711810495, + 2 => 'file', + ), + ), + 'nocache_hash' => '1302564042692477a6630ff7-79176962', + 'function' => + array ( + ), + 'variables' => + array ( + 'LISTVIEW_MASSACTIONS' => 0, + 'LIST_MASSACTION' => 0, + 'LISTVIEW_MASSACTIONS_1' => 0, + 'deleteAction' => 0, + 'MODULE' => 0, + 'LISTVIEW_ENTRIES_COUNT' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477a6640f3', +),false); /*/%%SmartyHeaderCode%%*/?> + + +tpl_vars['LISTVIEW_MASSACTIONS_1'] = new Smarty_variable(array(), null, 0);?>
tpl_vars['LIST_MASSACTION'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['LIST_MASSACTION']->_loop = false; + $_from = $_smarty_tpl->tpl_vars['LISTVIEW_MASSACTIONS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['LIST_MASSACTION']->key => $_smarty_tpl->tpl_vars['LIST_MASSACTION']->value){ +$_smarty_tpl->tpl_vars['LIST_MASSACTION']->_loop = true; +?>tpl_vars['LIST_MASSACTION']->value->getLabel()=='LBL_EDIT'){?>tpl_vars['editAction'] = new Smarty_variable($_smarty_tpl->tpl_vars['LIST_MASSACTION']->value, null, 0);?>tpl_vars['LIST_MASSACTION']->value->getLabel()=='LBL_DELETE'){?>tpl_vars['deleteAction'] = new Smarty_variable($_smarty_tpl->tpl_vars['LIST_MASSACTION']->value, null, 0);?>tpl_vars['LIST_MASSACTION']->value->getLabel()=='LBL_ADD_COMMENT'){?>tpl_vars['commentAction'] = new Smarty_variable($_smarty_tpl->tpl_vars['LIST_MASSACTION']->value, null, 0);?>tpl_vars['a'] = new Smarty_variable(array_push($_smarty_tpl->tpl_vars['LISTVIEW_MASSACTIONS_1']->value,$_smarty_tpl->tpl_vars['LIST_MASSACTION']->value), null, 0);?>
tpl_vars['deleteAction']->value){?>tpl_vars['deleteAction']->value){?>
tpl_vars['RECORD_COUNT'] = new Smarty_variable($_smarty_tpl->tpl_vars['LISTVIEW_ENTRIES_COUNT']->value, null, 0);?>getSubTemplate (vtemplate_path("Pagination.tpl",$_smarty_tpl->tpl_vars['MODULE']->value), $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array('SHOWPAGEJUMP'=>true), 0);?> +
\ No newline at end of file diff --git a/test/templates_c/v7/885963af7bb1787b612e69517c5a6ef946d6c39a.file.SidebarEssentials.tpl.php b/test/templates_c/v7/885963af7bb1787b612e69517c5a6ef946d6c39a.file.SidebarEssentials.tpl.php new file mode 100644 index 00000000..cb63c807 --- /dev/null +++ b/test/templates_c/v7/885963af7bb1787b612e69517c5a6ef946d6c39a.file.SidebarEssentials.tpl.php @@ -0,0 +1,61 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '885963af7bb1787b612e69517c5a6ef946d6c39a' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/partials/SidebarEssentials.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '1591821842692477aeb73f51-62072954', + 'function' => + array ( + ), + 'variables' => + array ( + 'NO_LICENSE' => 0, + 'MODULE' => 0, + 'FOLDERS' => 0, + 'FOLDER' => 0, + 'VIEWNAME' => 0, + 'FOLDERID' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477aeb8b6a', +),false); /*/%%SmartyHeaderCode%%*/?> + + +tpl_vars['NO_LICENSE']->value){?> \ No newline at end of file diff --git a/test/templates_c/v7/898c77c103daf6e53c1fd50960eab91d6a3cb2df.file.ListViewActions.tpl.php b/test/templates_c/v7/898c77c103daf6e53c1fd50960eab91d6a3cb2df.file.ListViewActions.tpl.php new file mode 100644 index 00000000..31da9db8 --- /dev/null +++ b/test/templates_c/v7/898c77c103daf6e53c1fd50960eab91d6a3cb2df.file.ListViewActions.tpl.php @@ -0,0 +1,87 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '898c77c103daf6e53c1fd50960eab91d6a3cb2df' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/ListViewActions.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '778172812692477aebcf396-16581001', + 'function' => + array ( + ), + 'variables' => + array ( + 'LISTVIEW_MASSACTIONS' => 0, + 'LIST_MASSACTION' => 0, + 'LISTVIEW_MASSACTIONS_1' => 0, + 'deleteAction' => 0, + 'MODULE' => 0, + 'LISTVIEW_ENTRIES_COUNT' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477aebdb46', +),false); /*/%%SmartyHeaderCode%%*/?> + +
+ tpl_vars['LISTVIEW_MASSACTIONS_1'] = new Smarty_variable(array(), null, 0);?> + tpl_vars['LIST_MASSACTION'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['LIST_MASSACTION']->_loop = false; + $_from = $_smarty_tpl->tpl_vars['LISTVIEW_MASSACTIONS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['LIST_MASSACTION']->key => $_smarty_tpl->tpl_vars['LIST_MASSACTION']->value){ +$_smarty_tpl->tpl_vars['LIST_MASSACTION']->_loop = true; +?> + tpl_vars['LIST_MASSACTION']->value)&&$_smarty_tpl->tpl_vars['LIST_MASSACTION']->value->getLabel()=='LBL_EDIT'){?> + tpl_vars['editAction'] = new Smarty_variable($_smarty_tpl->tpl_vars['LIST_MASSACTION']->value, null, 0);?> + tpl_vars['LIST_MASSACTION']->value)&&$_smarty_tpl->tpl_vars['LIST_MASSACTION']->value->getLabel()=='LBL_DELETE'){?> + tpl_vars['deleteAction'] = new Smarty_variable($_smarty_tpl->tpl_vars['LIST_MASSACTION']->value, null, 0);?> + + tpl_vars['a'] = new Smarty_variable(array_push($_smarty_tpl->tpl_vars['LISTVIEW_MASSACTIONS_1']->value,$_smarty_tpl->tpl_vars['LIST_MASSACTION']->value), null, 0);?> + + + +
+
+
+ tpl_vars['deleteAction']->value){?> + + + tpl_vars['deleteAction']->value){?> + + +
+
+
+ +   + +
+
+ tpl_vars['RECORD_COUNT'] = new Smarty_variable($_smarty_tpl->tpl_vars['LISTVIEW_ENTRIES_COUNT']->value, null, 0);?> + getSubTemplate (vtemplate_path("Pagination.tpl",$_smarty_tpl->tpl_vars['MODULE']->value), $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array('SHOWPAGEJUMP'=>true), 0);?> + +
+
+
\ No newline at end of file diff --git a/test/templates_c/v7/8cbd308a91e69d8f3e5e0eab634bacbd4260098c.file.IndexViewPreProcess.tpl.php b/test/templates_c/v7/8cbd308a91e69d8f3e5e0eab634bacbd4260098c.file.IndexViewPreProcess.tpl.php new file mode 100644 index 00000000..152054ec --- /dev/null +++ b/test/templates_c/v7/8cbd308a91e69d8f3e5e0eab634bacbd4260098c.file.IndexViewPreProcess.tpl.php @@ -0,0 +1,32 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '8cbd308a91e69d8f3e5e0eab634bacbd4260098c' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/IndexViewPreProcess.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '798650806692477b6d42ba7-44323223', + 'function' => + array ( + ), + 'variables' => + array ( + 'MODULE' => 0, + 'VIEW' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477b6d7470', +),false); /*/%%SmartyHeaderCode%%*/?> + +getSubTemplate ("modules/Vtiger/partials/Topbar.tpl", $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> +
getSubTemplate ("modules/ITS4YouReports/partials/SidebarHeader.tpl", $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> +getSubTemplate (vtemplate_path("ModuleHeader.tpl",$_smarty_tpl->tpl_vars['MODULE']->value), $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> +
tpl_vars['VIEW']->value&&'EditKeyMetricsRow'!=$_smarty_tpl->tpl_vars['VIEW']->value){?>
+ \ No newline at end of file diff --git a/test/templates_c/v7/9bfb75194198e58fb7cb8c5a673a0ece558867f8.file.ListViewContents.tpl.php b/test/templates_c/v7/9bfb75194198e58fb7cb8c5a673a0ece558867f8.file.ListViewContents.tpl.php new file mode 100644 index 00000000..ba0f3324 --- /dev/null +++ b/test/templates_c/v7/9bfb75194198e58fb7cb8c5a673a0ece558867f8.file.ListViewContents.tpl.php @@ -0,0 +1,167 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + '9bfb75194198e58fb7cb8c5a673a0ece558867f8' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/ListViewContents.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '1789866997692477aeb96c38-88279195', + 'function' => + array ( + ), + 'variables' => + array ( + 'CURRENT_USER_MODEL' => 0, + 'LEFTPANELHIDE' => 0, + 'VIEW' => 0, + 'VIEWID' => 0, + 'PAGING_MODEL' => 0, + 'OPERATOR' => 0, + 'LISTVIEW_COUNT' => 0, + 'PAGE_NUMBER' => 0, + 'LISTVIEW_ENTRIES_COUNT' => 0, + 'SEARCH_DETAILS' => 0, + 'NO_SEARCH_PARAMS_CACHE' => 0, + 'ORDER_BY' => 0, + 'SORT_ORDER' => 0, + 'LIST_HEADER_FIELDS' => 0, + 'CURRENT_TAG' => 0, + 'FOLDER_ID' => 0, + 'FOLDER_VALUE' => 0, + 'VIEWNAME' => 0, + 'SEARCH_MODE_RESULTS' => 0, + 'MODULE' => 0, + 'LISTVIEW_HEADERS' => 0, + 'COLUMN_NAME' => 0, + 'LISTVIEW_HEADER_KEY' => 0, + 'NEXT_SORT_ORDER' => 0, + 'FASORT_IMAGE' => 0, + 'LISTVIEW_HEADER' => 0, + 'MODULE_MODEL' => 0, + 'DATA_TYPE' => 0, + 'FIELD_INFO' => 0, + 'PICKLIST_VALUES' => 0, + 'PICKLIST_ARR' => 0, + 'PICKLIST_KEY' => 0, + 'SEARCH_VALUES' => 0, + 'ICON_CLASS' => 0, + 'PICKLIST_LABEL' => 0, + 'LISTVIEW_ENTRIES' => 0, + 'LISTVIEW_ENTRY' => 0, + 'LISTVIEW_HEADERNAME' => 0, + 'LISTVIEW_ENTRY_RAWVALUE' => 0, + 'LISTVIEW_ENTRY_VALUE' => 0, + 'COLSPAN_WIDTH' => 0, + 'IS_MODULE_EDITABLE' => 0, + 'LIST_VIEW_MODEL' => 0, + 'SINGLE_MODULE' => 0, + 'VERSION' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477aebcae2', +),false); /*/%%SmartyHeaderCode%%*/?> + + +
tpl_vars['LEFTPANELHIDE'] = new Smarty_variable($_smarty_tpl->tpl_vars['CURRENT_USER_MODEL']->value->get('leftpanelhide'), null, 0);?>
value;?> +'/>tpl_vars['SEARCH_MODE_RESULTS']->value){?>getSubTemplate (vtemplate_path("ListViewActions.tpl",$_smarty_tpl->tpl_vars['MODULE']->value), $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> +
tpl_vars['LISTVIEW_HEADER'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->_loop = false; + $_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['LISTVIEW_HEADERS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->key => $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->value){ +$_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->_loop = true; + $_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value = $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->key; +?>tpl_vars['MODULE_MODEL']->value->isQuickSearchEnabled()&&!$_smarty_tpl->tpl_vars['SEARCH_MODE_RESULTS']->value){?>tpl_vars['LISTVIEW_HEADER'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->_loop = false; + $_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['LISTVIEW_HEADERS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->key => $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->value){ +$_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->_loop = true; + $_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value = $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->key; +?>tpl_vars['LISTVIEW_ENTRY'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['LISTVIEW_ENTRY']->_loop = false; + $_from = $_smarty_tpl->tpl_vars['LISTVIEW_ENTRIES']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} + $_smarty_tpl->tpl_vars['smarty']->value['foreach']['listview']['index']=-1; +foreach ($_from as $_smarty_tpl->tpl_vars['LISTVIEW_ENTRY']->key => $_smarty_tpl->tpl_vars['LISTVIEW_ENTRY']->value){ +$_smarty_tpl->tpl_vars['LISTVIEW_ENTRY']->_loop = true; + $_smarty_tpl->tpl_vars['smarty']->value['foreach']['listview']['index']++; +?>value->get("reportid");?> +' data-recordUrl='tpl_vars['LISTVIEW_ENTRY']->value->getDetailViewUrl();?> +' id="tpl_vars['MODULE']->value;?> +_listView_row_getVariable('smarty')->value['foreach']['listview']['index']+1;?> +">tpl_vars['LISTVIEW_HEADER'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->_loop = false; + $_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['LISTVIEW_HEADERS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->key => $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->value){ +$_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->_loop = true; + $_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value = $_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->key; +?>tpl_vars['LISTVIEW_HEADERNAME'] = new Smarty_variable($_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value, null, 0);?>tpl_vars['LISTVIEW_ENTRY_RAWVALUE'] = new Smarty_variable('', null, 0);?>tpl_vars['LISTVIEW_ENTRY']->value->rawData)){?>tpl_vars['LISTVIEW_ENTRY_RAWVALUE'] = new Smarty_variable($_smarty_tpl->tpl_vars['LISTVIEW_ENTRY']->value->getRaw($_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value), null, 0);?>tpl_vars['LISTVIEW_ENTRY_VALUE'] = new Smarty_variable($_smarty_tpl->tpl_vars['LISTVIEW_ENTRY']->value->get($_smarty_tpl->tpl_vars['LISTVIEW_HEADERNAME']->value), null, 0);?>tpl_vars['LISTVIEW_ENTRIES_COUNT']->value=='0'){?>tpl_vars['LISTVIEW_HEADERS']->value);?> +tpl_vars['COLSPAN_WIDTH'] = new Smarty_variable($_tmp2+1, null, 0);?>
tpl_vars['SEARCH_MODE_RESULTS']->value){?>
tpl_vars['SEARCH_MODE_RESULTS']->value){?>tpl_vars['MODULE']->value);?> +
tpl_vars['COLUMN_NAME']->value==$_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value){?> nowrap="nowrap" >tpl_vars['COLUMN_NAME']->value==$_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value){?> tpl_vars['LISTVIEW_HEADER']->value['name'];?> +tpl_vars['MODULE']->value);?> + tpl_vars['COLUMN_NAME']->value==$_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value){?>
tpl_vars["DATA_TYPE"] = new Smarty_variable($_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->value['type'], null, 0);?>tpl_vars['DATA_TYPE']->value=='text'){?>
value, ENT_QUOTES, 'UTF-8', true);?> +'/>
tpl_vars['DATA_TYPE']->value=='picklist'||$_smarty_tpl->tpl_vars['DATA_TYPE']->value=='user'){?>tpl_vars['PICKLIST_VALUES'] = new Smarty_variable($_smarty_tpl->tpl_vars['LISTVIEW_HEADER']->value['picklistValues'], null, 0);?>tpl_vars['SEARCH_VALUES'] = new Smarty_variable(explode(',',$_smarty_tpl->tpl_vars['SEARCH_DETAILS']->value[$_smarty_tpl->tpl_vars['LISTVIEW_HEADER_KEY']->value]['searchValue']), null, 0);?>
getSubTemplate (vtemplate_path("ListViewRecordActions.tpl",$_smarty_tpl->tpl_vars['MODULE']->value), $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> +tpl_vars['LISTVIEW_HEADERNAME']->value=='reportname'){?>tpl_vars['LISTVIEW_ENTRY_VALUE']->value;?> +tpl_vars['LISTVIEW_HEADERNAME']->value=='tablabel'){?>tpl_vars['LISTVIEW_ENTRY_VALUE']->value,$_smarty_tpl->tpl_vars['LISTVIEW_ENTRY_VALUE']->value)));?> +tpl_vars['LISTVIEW_HEADERNAME']->value=='foldername'){?>tpl_vars['LISTVIEW_ENTRY_VALUE']->value,$_smarty_tpl->tpl_vars['MODULE']->value));?> +tpl_vars['LISTVIEW_ENTRY_VALUE']->value);?> +
tpl_vars['SINGLE_MODULE'] = new Smarty_variable("SINGLE_".($_smarty_tpl->tpl_vars['MODULE']->value), null, 0);?> + tpl_vars['MODULE']->value,$_smarty_tpl->tpl_vars['MODULE']->value);?> + +.tpl_vars['IS_MODULE_EDITABLE']->value){?> + tpl_vars['MODULE']->value,'Import')&&$_smarty_tpl->tpl_vars['LIST_VIEW_MODEL']->value->isImportEnabled()){?> tpl_vars['MODULE']->value);?> + tpl_vars['MODULE']->value);?> + tpl_vars['MODULE']->value,$_smarty_tpl->tpl_vars['MODULE']->value);?> +tpl_vars['SINGLE_MODULE']->value,$_smarty_tpl->tpl_vars['MODULE']->value);?> +

tpl_vars['MODULE']->value,$_smarty_tpl->tpl_vars['MODULE']->value);?> + tpl_vars['VERSION']->value;?> + tpl_vars['MODULE']->value);?> +
+ \ No newline at end of file diff --git a/test/templates_c/v7/bed1a1c7f85af91bed96d6e430681010d61c990f.file.SidebarEssentials.tpl.php b/test/templates_c/v7/bed1a1c7f85af91bed96d6e430681010d61c990f.file.SidebarEssentials.tpl.php new file mode 100644 index 00000000..ccd02dc0 --- /dev/null +++ b/test/templates_c/v7/bed1a1c7f85af91bed96d6e430681010d61c990f.file.SidebarEssentials.tpl.php @@ -0,0 +1,56 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + 'bed1a1c7f85af91bed96d6e430681010d61c990f' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/Reports/partials/SidebarEssentials.tpl', + 1 => 1711810495, + 2 => 'file', + ), + ), + 'nocache_hash' => '2050673226692477a65e2bd6-28764319', + 'function' => + array ( + ), + 'variables' => + array ( + 'MODULE' => 0, + 'FOLDERS' => 0, + 'FOLDER' => 0, + 'VIEWNAME' => 0, + 'FOLDERID' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477a65f31f', +),false); /*/%%SmartyHeaderCode%%*/?> + + + \ No newline at end of file diff --git a/test/templates_c/v7/c110b0c6441dffe278b3aba09f968d92c82c9291.file.AdvanceFilter.tpl.php b/test/templates_c/v7/c110b0c6441dffe278b3aba09f968d92c82c9291.file.AdvanceFilter.tpl.php new file mode 100644 index 00000000..e168ebf4 --- /dev/null +++ b/test/templates_c/v7/c110b0c6441dffe278b3aba09f968d92c82c9291.file.AdvanceFilter.tpl.php @@ -0,0 +1,779 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + 'c110b0c6441dffe278b3aba09f968d92c82c9291' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/AdvanceFilter.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '700102341692477b760d7f7-57572919', + 'function' => + array ( + ), + 'variables' => + array ( + 'current_mk_time' => 0, + 'user_date_format' => 0, + 'fld_date_options' => 0, + 'COLUMNS_BLOCK_JSON' => 0, + 'QF_COLUMNS_BLOCK_JSON' => 0, + 'USER_DATE_FORMAT' => 0, + 'MODULE' => 0, + 'FOPTION' => 0, + 'REL_FIELDS' => 0, + 'JS_DATEFORMAT' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477b763b79', +),false); /*/%%SmartyHeaderCode%%*/?> + + + + + +value;?> +'/> +value;?> +'/> +value;?> +'/> +
tpl_vars['COLUMNS_BLOCK_JSON']->value;?> +
+
tpl_vars['QF_COLUMNS_BLOCK_JSON']->value;?> +
+ + \ No newline at end of file diff --git a/test/templates_c/v7/c1502db5b78e3d7bbcb06e1ad01077830cdd4c73.file.ReportHeader.tpl.php b/test/templates_c/v7/c1502db5b78e3d7bbcb06e1ad01077830cdd4c73.file.ReportHeader.tpl.php new file mode 100644 index 00000000..d35fe1f1 --- /dev/null +++ b/test/templates_c/v7/c1502db5b78e3d7bbcb06e1ad01077830cdd4c73.file.ReportHeader.tpl.php @@ -0,0 +1,64 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + 'c1502db5b78e3d7bbcb06e1ad01077830cdd4c73' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/ReportHeader.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '1278999366692477b7669181-03385451', + 'function' => + array ( + ), + 'variables' => + array ( + 'DATE_FILTERS' => 0, + 'REPORT_LIMIT' => 0, + 'PDFMakerActive' => 0, + 'IS_TEST_WRITE_ABLE' => 0, + 'RECORD_ID' => 0, + 'REPORT_CHANGED' => 0, + 'MODULE' => 0, + 'REPORT_MODEL' => 0, + 'CRITERIA_GROUPS' => 0, + 'filterConditionNotExists' => 0, + 'REPORTTYPE' => 0, + 'REPORT_FILTERS' => 0, + 'checkDashboardWidget' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477b767629', +),false); /*/%%SmartyHeaderCode%%*/?> + + + +
value));?> +'/>
value;?> +'/>value));?> +'/>getSubTemplate (vtemplate_path("DetailViewActions.tpl",$_smarty_tpl->tpl_vars['MODULE']->value), $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> +tpl_vars['REPORT_MODEL']->value->getReportType()){?>
tpl_vars['filterConditionNotExists'] = new Smarty_variable((empty($_smarty_tpl->tpl_vars['CRITERIA_GROUPS']->value)), null, 0);?>

tpl_vars['REPORTTYPE']->value!=="custom_report"){?>
tpl_vars['REPORT_FILTERS']->value;?> +

 tpl_vars['REPORTTYPE']->value!="custom_report"&&$_smarty_tpl->tpl_vars['REPORT_MODEL']->value->isEditable()==true){?>tpl_vars['checkDashboardWidget']->value!=''&&$_smarty_tpl->tpl_vars['checkDashboardWidget']->value!="Exist"){?>

+ \ No newline at end of file diff --git a/test/templates_c/v7/c95e8384f0bbfe66870957dfc0e785cb930f3c66.file.StatusPickListFieldSearchView.tpl.php b/test/templates_c/v7/c95e8384f0bbfe66870957dfc0e785cb930f3c66.file.StatusPickListFieldSearchView.tpl.php new file mode 100644 index 00000000..75bfbceb --- /dev/null +++ b/test/templates_c/v7/c95e8384f0bbfe66870957dfc0e785cb930f3c66.file.StatusPickListFieldSearchView.tpl.php @@ -0,0 +1,47 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + 'c95e8384f0bbfe66870957dfc0e785cb930f3c66' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/Calendar/uitypes/StatusPickListFieldSearchView.tpl', + 1 => 1711810495, + 2 => 'file', + ), + ), + 'nocache_hash' => '8417322536924036c08b948-70005047', + 'function' => + array ( + ), + 'variables' => + array ( + 'FIELD_MODEL' => 0, + 'FIELD_INFO' => 0, + 'EVENTS_MODULE_MODEL' => 0, + 'EVENT_STATUS_FIELD_MODEL' => 0, + 'PICKLIST_VALUES' => 0, + 'EVENT_STAUTS_PICKLIST_VALUES' => 0, + 'SEARCH_INFO' => 0, + 'PICKLIST_KEY' => 0, + 'SEARCH_VALUES' => 0, + 'PICKLIST_LABEL' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_6924036c0c5ac', +),false); /*/%%SmartyHeaderCode%%*/?> + + +tpl_vars['FIELD_INFO'] = new Smarty_variable($_smarty_tpl->tpl_vars['FIELD_MODEL']->value->getFieldInfo(), null, 0);?>tpl_vars['PICKLIST_VALUES'] = new Smarty_variable($_smarty_tpl->tpl_vars['FIELD_INFO']->value['picklistvalues'], null, 0);?>tpl_vars['FIELD_INFO'] = new Smarty_variable(Vtiger_Util_Helper::toSafeHTML(Zend_Json::encode($_smarty_tpl->tpl_vars['FIELD_INFO']->value)), null, 0);?>tpl_vars['EVENTS_MODULE_MODEL'] = new Smarty_variable(Vtiger_Module_Model::getInstance('Events'), null, 0);?>tpl_vars['EVENT_STATUS_FIELD_MODEL'] = new Smarty_variable($_smarty_tpl->tpl_vars['EVENTS_MODULE_MODEL']->value->getField('eventstatus'), null, 0);?>tpl_vars['EVENT_STAUTS_PICKLIST_VALUES'] = new Smarty_variable($_smarty_tpl->tpl_vars['EVENT_STATUS_FIELD_MODEL']->value->getPicklistValues(), null, 0);?>tpl_vars['PICKLIST_VALUES'] = new Smarty_variable(array_merge($_smarty_tpl->tpl_vars['PICKLIST_VALUES']->value,$_smarty_tpl->tpl_vars['EVENT_STAUTS_PICKLIST_VALUES']->value), null, 0);?>tpl_vars['SEARCH_VALUES'] = new Smarty_variable(explode(',',$_smarty_tpl->tpl_vars['SEARCH_INFO']->value['searchValue']), null, 0);?>
\ No newline at end of file diff --git a/test/templates_c/v7/d7170b29735ec543e04ccfd484d0bafae451ca28.file.ListViewPostProcess.tpl.php b/test/templates_c/v7/d7170b29735ec543e04ccfd484d0bafae451ca28.file.ListViewPostProcess.tpl.php new file mode 100644 index 00000000..c1b4bd10 --- /dev/null +++ b/test/templates_c/v7/d7170b29735ec543e04ccfd484d0bafae451ca28.file.ListViewPostProcess.tpl.php @@ -0,0 +1,25 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + 'd7170b29735ec543e04ccfd484d0bafae451ca28' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/Reports/ListViewPostProcess.tpl', + 1 => 1711810494, + 2 => 'file', + ), + ), + 'nocache_hash' => '1239741255692477a66bb692-49233046', + 'function' => + array ( + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477a66bbee', +),false); /*/%%SmartyHeaderCode%%*/?> + +
+
+ \ No newline at end of file diff --git a/test/templates_c/v7/e030303313349f80c40cef7474b1de9568183305.file.MassEditForm.tpl.php b/test/templates_c/v7/e030303313349f80c40cef7474b1de9568183305.file.MassEditForm.tpl.php new file mode 100644 index 00000000..7699520b --- /dev/null +++ b/test/templates_c/v7/e030303313349f80c40cef7474b1de9568183305.file.MassEditForm.tpl.php @@ -0,0 +1,44 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + 'e030303313349f80c40cef7474b1de9568183305' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/Vtiger/MassEditForm.tpl', + 1 => 1711810494, + 2 => 'file', + ), + ), + 'nocache_hash' => '173642876369242772ea5557-22337899', + 'function' => + array ( + ), + 'variables' => + array ( + 'MODULE' => 0, + 'CVID' => 0, + 'SELECTED_IDS' => 0, + 'EXCLUDED_IDS' => 0, + 'TAG_PARAMS' => 0, + 'SEARCH_PARAMS' => 0, + 'TITLE' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_69242772ed917', +),false); /*/%%SmartyHeaderCode%%*/?> + + \ No newline at end of file diff --git a/test/templates_c/v7/f0b5173dc913424ef95a34af0a64bb6adaad9918.file.Footer.tpl.php b/test/templates_c/v7/f0b5173dc913424ef95a34af0a64bb6adaad9918.file.Footer.tpl.php new file mode 100644 index 00000000..4d2420f1 --- /dev/null +++ b/test/templates_c/v7/f0b5173dc913424ef95a34af0a64bb6adaad9918.file.Footer.tpl.php @@ -0,0 +1,28 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + 'f0b5173dc913424ef95a34af0a64bb6adaad9918' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/ITS4YouReports/Footer.tpl', + 1 => 1711810496, + 2 => 'file', + ), + ), + 'nocache_hash' => '1220053640692477aec01485-22614471', + 'function' => + array ( + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477aec03e7', +),false); /*/%%SmartyHeaderCode%%*/?> + + +
+ + +
getSubTemplate (vtemplate_path("Footer.tpl",'Vtiger'), $_smarty_tpl->cache_id, $_smarty_tpl->compile_id, null, null, array(), 0);?> + \ No newline at end of file diff --git a/test/templates_c/v7/fbe8cd3b3a31cf071741e629233295f118c71f12.file.ModuleHeader.tpl.php b/test/templates_c/v7/fbe8cd3b3a31cf071741e629233295f118c71f12.file.ModuleHeader.tpl.php new file mode 100644 index 00000000..9658ad04 --- /dev/null +++ b/test/templates_c/v7/fbe8cd3b3a31cf071741e629233295f118c71f12.file.ModuleHeader.tpl.php @@ -0,0 +1,74 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + 'fbe8cd3b3a31cf071741e629233295f118c71f12' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/Reports/ModuleHeader.tpl', + 1 => 1711810495, + 2 => 'file', + ), + ), + 'nocache_hash' => '668099220692477a65b2725-63408611', + 'function' => + array ( + ), + 'variables' => + array ( + 'MODULE' => 0, + 'MODULE_MODEL' => 0, + 'DEFAULT_FILTER_ID' => 0, + 'CVURL' => 0, + 'DEFAULT_FILTER_URL' => 0, + 'VIEW' => 0, + 'REPORT_NAME' => 0, + 'VIEWNAME' => 0, + 'FOLDERS' => 0, + 'FOLDER' => 0, + 'FOLDERNAME' => 0, + 'LISTVIEW_LINKS' => 0, + 'LISTVIEW_BASICACTION' => 0, + 'childLinks' => 0, + 'childLink' => 0, + 'ICON_CLASS' => 0, + 'FIELDS_INFO' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_692477a65d455', +),false); /*/%%SmartyHeaderCode%%*/?> + + +
tpl_vars['MODULE_MODEL'] = new Smarty_variable(Vtiger_Module_Model::getInstance($_smarty_tpl->tpl_vars['MODULE']->value), null, 0);?>tpl_vars['DEFAULT_FILTER_ID'] = new Smarty_variable($_smarty_tpl->tpl_vars['MODULE_MODEL']->value->getDefaultCustomFilter(), null, 0);?>tpl_vars['DEFAULT_FILTER_ID']->value){?>tpl_vars['CVURL'] = new Smarty_variable(("&viewname=").($_smarty_tpl->tpl_vars['DEFAULT_FILTER_ID']->value), null, 0);?>tpl_vars['DEFAULT_FILTER_URL'] = new Smarty_variable(($_smarty_tpl->tpl_vars['MODULE_MODEL']->value->getListViewUrl()).($_smarty_tpl->tpl_vars['CVURL']->value), null, 0);?>tpl_vars['DEFAULT_FILTER_URL'] = new Smarty_variable($_smarty_tpl->tpl_vars['MODULE_MODEL']->value->getListViewUrlWithAllFilter(), null, 0);?>value;?> +'>

 tpl_vars['MODULE']->value,$_smarty_tpl->tpl_vars['MODULE']->value);?> + 

  tpl_vars['VIEW']->value=='Detail'||$_smarty_tpl->tpl_vars['VIEW']->value=='ChartDetail'){?>tpl_vars['REPORT_NAME']->value;?> +tpl_vars['VIEW']->value=='List'){?> tpl_vars['MODULE']->value);?> + tpl_vars['MODULE']->value);?> + 

tpl_vars['VIEWNAME']->value){?>tpl_vars['VIEWNAME']->value!='All'){?>tpl_vars['FOLDER'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['FOLDER']->_loop = false; + $_from = $_smarty_tpl->tpl_vars['FOLDERS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['FOLDER']->key => $_smarty_tpl->tpl_vars['FOLDER']->value){ +$_smarty_tpl->tpl_vars['FOLDER']->_loop = true; +?>tpl_vars['FOLDER']->value->getId()==$_smarty_tpl->tpl_vars['VIEWNAME']->value){?>tpl_vars['FOLDERNAME'] = new Smarty_variable($_smarty_tpl->tpl_vars['FOLDER']->value->getName(), null, 0);?>tpl_vars['FOLDERNAME'] = new Smarty_variable(vtranslate('LBL_ALL_REPORTS',$_smarty_tpl->tpl_vars['MODULE']->value), null, 0);?>

 tpl_vars['FOLDERNAME']->value;?> + 

tpl_vars['FIELDS_INFO'] = new Smarty_variable(Reports_Field_Model::getListViewFieldsInfo(), null, 0);?>tpl_vars['FIELDS_INFO']->value!=null){?>
\ No newline at end of file diff --git a/test/templates_c/v7/fee03f2eec43734de4d078d3bb4249057e125bc9.file.ProductTax.tpl.php b/test/templates_c/v7/fee03f2eec43734de4d078d3bb4249057e125bc9.file.ProductTax.tpl.php new file mode 100644 index 00000000..5883995d --- /dev/null +++ b/test/templates_c/v7/fee03f2eec43734de4d078d3bb4249057e125bc9.file.ProductTax.tpl.php @@ -0,0 +1,78 @@ + +decodeProperties(array ( + 'file_dependency' => + array ( + 'fee03f2eec43734de4d078d3bb4249057e125bc9' => + array ( + 0 => '/var/www/fastuser/data/www/crm.clientright.ru/includes/runtime/../../layouts/v7/modules/Vtiger/uitypes/ProductTax.tpl', + 1 => 1711810494, + 2 => 'file', + ), + ), + 'nocache_hash' => '1789428926926ba4d5e4242-34386166', + 'function' => + array ( + ), + 'variables' => + array ( + 'TAXCLASS_DETAILS' => 0, + 'tax' => 0, + 'tax_count' => 0, + 'PULL_RIGHT' => 0, + 'MODULE' => 0, + 'check_value' => 0, + 'show_value' => 0, + 'WIDTHTYPE' => 0, + 'QUALIFIED_MODULE' => 0, + 'REGIONS_INFO' => 0, + 'TAX_REGION_ID' => 0, + 'i' => 0, + 'TAX_REGION_MODEL' => 0, + 'COUNTER' => 0, + ), + 'has_nocache_code' => false, + 'version' => 'Smarty-3.1.7', + 'unifunc' => 'content_6926ba4d631e0', +),false); /*/%%SmartyHeaderCode%%*/?> + + +tpl_vars["tax_count"] = new Smarty_variable(1, null, 0);?>tpl_vars['tax'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['tax']->_loop = false; + $_smarty_tpl->tpl_vars['count'] = new Smarty_Variable; + $_from = $_smarty_tpl->tpl_vars['TAXCLASS_DETAILS']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['tax']->key => $_smarty_tpl->tpl_vars['tax']->value){ +$_smarty_tpl->tpl_vars['tax']->_loop = true; + $_smarty_tpl->tpl_vars['count']->value = $_smarty_tpl->tpl_vars['tax']->key; +?>tpl_vars['tax']->value['check_value']==1){?>tpl_vars['check_value'] = new Smarty_variable("checked", null, 0);?>tpl_vars['show_value'] = new Smarty_variable("visible", null, 0);?>tpl_vars['check_value'] = new Smarty_variable('', null, 0);?>tpl_vars['show_value'] = new Smarty_variable("hidden", null, 0);?>tpl_vars['tax_count']->value>1){?>tpl_vars['tax']->value['type']=='Fixed'){?> hide show " name="tpl_vars['tax']->value['taxname'];?> +" value="tpl_vars['tax']->value['percentage'];?> +" data-rule-required="true" data-rule-inventory_percentage="true" />
hide" id="tpl_vars['tax']->value['taxname'];?> +" style="width:70%;">
tpl_vars['i'] = new Smarty_variable(0, null, 0);?>tpl_vars['REGIONS_INFO'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['REGIONS_INFO']->_loop = false; + $_from = $_smarty_tpl->tpl_vars['tax']->value['regions']; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['REGIONS_INFO']->key => $_smarty_tpl->tpl_vars['REGIONS_INFO']->value){ +$_smarty_tpl->tpl_vars['REGIONS_INFO']->_loop = true; +?>tpl_vars['i'] = new Smarty_variable($_smarty_tpl->tpl_vars['i']->value+1, null, 0);?>
tpl_vars['TAX_REGION_ID'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['TAX_REGION_ID']->_loop = false; + $_from = $_smarty_tpl->tpl_vars['REGIONS_INFO']->value['list']; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');} +foreach ($_from as $_smarty_tpl->tpl_vars['TAX_REGION_ID']->key => $_smarty_tpl->tpl_vars['TAX_REGION_ID']->value){ +$_smarty_tpl->tpl_vars['TAX_REGION_ID']->_loop = true; +?>tpl_vars['TAX_REGION_ID']->value;?> +tpl_vars['TAX_REGION_MODEL'] = new Smarty_variable(Inventory_TaxRegion_Model::getRegionModel($_tmp1), null, 0);?>tpl_vars['TAX_REGION_MODEL']->value->getName();?> +
tpl_vars["tax_count"] = new Smarty_variable($_smarty_tpl->tpl_vars['tax_count']->value+1, null, 0);?>tpl_vars['COUNTER']->value==2){?>tpl_vars["COUNTER"] = new Smarty_variable(1, null, 0);?>tpl_vars["COUNTER"] = new Smarty_variable($_smarty_tpl->tpl_vars['COUNTER']->value+1, null, 0);?> \ No newline at end of file diff --git a/ticket_form/SESSION_LOG_2025-11-26_DOCUMENTS_FIX.md b/ticket_form/SESSION_LOG_2025-11-26_DOCUMENTS_FIX.md new file mode 100644 index 00000000..6b124976 --- /dev/null +++ b/ticket_form/SESSION_LOG_2025-11-26_DOCUMENTS_FIX.md @@ -0,0 +1,176 @@ +# Лог сессии: Исправление загрузки документов и SQL запросов + +**Дата:** 2025-11-26 +**Тема:** Исправление потери документов, дубликатов и правильного определения field_name + +--- + +## Проблемы, которые были решены + +### 1. Потеря документов при обновлении черновика +**Проблема:** При обработке нового документа через SQL `claimsave_final` существующие документы терялись. + +**Причина:** +- SQL перезаписывал `documents_meta` вместо объединения +- `documents_uploaded` мог быть перезаписан пустым массивом, если `jsonb_agg` возвращал NULL + +**Решение:** +- Исправлен SQL `SQL_CLAIMSAVE_FINAL_FIXED_NEW_FLOW_WITH_UPLOADED.sql`: + - `documents_meta` теперь объединяется с существующими + - `documents_uploaded` всегда начинается с существующих документов + - Добавлена проверка на пустой массив перед перезаписью + +### 2. Дубликаты документов в documents_meta +**Проблема:** В `documents_meta` были дубликаты (один и тот же `file_id` встречался несколько раз). + +**Решение:** +- Создан скрипт `fix_documents_meta_duplicates.py` для удаления дубликатов +- Исправлена логика объединения в SQL + +### 3. Неправильное определение типа документа +**Проблема:** Чек определялся как `contract` вместо `payment`. + +**Причина:** +- SQL проверял `field_name` раньше, чем `field_label` +- `field_name` был `uploads[0][0]` для всех документов + +**Решение:** +- Изменён порядок проверки в SQL: сначала `field_label`, потом `field_name` +- Исправлен файл `SQL_CLAIMSAVE_FINAL_FIXED_NEW_FLOW_WITH_UPLOADED.sql` + +### 4. Все документы имели одинаковый field_name +**Проблема:** В таблице `clpr_claim_documents` все документы имели `field_name: uploads[0][0]`, из-за чего второй документ перезаписывал первый. + +**Причина:** +- `group_index` (индекс документа в `documents_required`) не передавался с фронтенда +- Код n8n использовал `group_index_num` из OCR, который всегда был `0` + +**Решение:** +- Фронтенд (`StepWizardPlan.tsx`): добавлена передача `group_index` в запрос +- Бэкенд (`documents.py`): добавлено получение `group_index` из Form и передача в n8n +- Код n8n (`N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js`): приоритет `group_index` из body над `group_index_num` из OCR +- Создан скрипт `fix_claim_documents_field_names.py` для исправления существующих документов + +### 5. SQL для claimsave перезаписывал documents_meta +**Проблема:** SQL `claimsave` перезаписывал `documents_meta` вместо объединения. + +**Решение:** +- Исправлен файл `SQL_CLAIMSAVE_FIXED_NEW_FLOW.sql`: + - `documents_meta` объединяется с существующими + - Критичные поля удаляются из нового payload перед объединением + - Затем устанавливаются отдельно через `jsonb_set` + +### 6. Дубликаты в списке загруженных документов на фронтенде +**Проблема:** React ошибка "Encountered two children with the same key, `contract`". + +**Решение:** +- Исправлен `StepWizardPlan.tsx`: + - Убраны дубликаты при инициализации `uploadedDocs` + - Проверка на дубликаты при добавлении нового документа + - Использование `Array.from(new Set())` при рендеринге + +--- + +## Созданные файлы + +### SQL запросы +- `docs/SQL_CLAIMSAVE_FINAL_FIXED_NEW_FLOW_WITH_UPLOADED.sql` - SQL для сохранения документов с автоматическим созданием `documents_uploaded` +- `docs/SQL_CLAIMSAVE_FIXED_NEW_FLOW.sql` - Исправленный SQL для `claimsave` с объединением `documents_meta` +- `docs/SQL_FIX_DRAFT_BDDB6815.sql` - SQL для исправления конкретного черновика +- `docs/SQL_FIX_CLAIM_DOCUMENTS_FIELD_NAMES.sql` - SQL для исправления `field_name` в таблице + +### Код n8n +- `docs/N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js` - Исправленный код для обработки загруженных файлов с поддержкой `group_index` + +### Скрипты для исправления данных +- `fix_draft_bddb6815_with_contract.py` - Скрипт для исправления черновика с учётом загруженных документов +- `fix_documents_meta_duplicates.py` - Скрипт для удаления дубликатов из `documents_meta` +- `fix_claim_documents_field_names.py` - Скрипт для исправления `field_name` в таблице `clpr_claim_documents` +- `check_documents_detailed.py` - Скрипт для детальной проверки документов +- `check_documents_mismatch.py` - Скрипт для проверки несоответствий между `documents_uploaded` и таблицей + +--- + +## Изменённые файлы + +### Backend +- `backend/app/api/documents.py` - Добавлена передача `group_index` в n8n +- `backend/app/api/claims.py` - Обновлена логика загрузки черновиков, добавлена поддержка `documents_required` +- `backend/app/api/events.py` - Исправлены синтаксические ошибки (удалены дубликаты кода) +- `backend/app/api/models.py` - Добавлены поля `unified_id` и `contact_id` + +### Frontend +- `frontend/src/pages/ClaimForm.tsx` - Обновлена логика загрузки черновиков, добавлена поддержка нового флоу +- `frontend/src/components/form/StepWizardPlan.tsx` - Добавлена передача `group_index`, исправлены дубликаты в списке документов +- `frontend/src/components/form/StepDraftSelection.tsx` - Обновлена логика определения legacy черновиков +- `frontend/src/components/form/StepDescription.tsx` - Добавлена передача `unified_id` и `contact_id` + +--- + +## Результаты + +### Исправлено для черновика `bddb6815-8e17-4d54-a721-5e94382942c7`: +- ✅ Удалены дубликаты из `documents_meta` (было 4, стало 3) +- ✅ Исправлены типы документов в `documents_uploaded` (чек теперь `payment`, а не `contract`) +- ✅ Исправлены `field_name` в таблице `clpr_claim_documents`: + - `uploads[0][0]` - contract (договор) + - `uploads[1][0]` - payment (чек) + - `uploads[3][0]` - evidence_photo (фото доказательства) + +### Текущее состояние: +- `documents_required`: 4 документа +- `documents_uploaded`: 2 документа (contract, payment) +- `documents_meta`: 3 документа (без дубликатов) +- `current_doc_index`: 2 (следующий документ - correspondence) +- `status_code`: `draft_docs_progress` + +--- + +## Что нужно сделать дальше + +1. **Обновить код n8n:** + - Заменить код в узле "Process Uploaded Files" на версию из `N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js` + - Убедиться, что `group_index` передаётся из body + +2. **Обновить SQL в n8n:** + - Заменить SQL в узле "claimsave" на версию из `SQL_CLAIMSAVE_FIXED_NEW_FLOW.sql` + - Заменить SQL в узле "claimsave_final" на версию из `SQL_CLAIMSAVE_FINAL_FIXED_NEW_FLOW_WITH_UPLOADED.sql` + +3. **Проверить работу:** + - Загрузить новый документ через интерфейс + - Убедиться, что он получает правильный `field_name` (например, `uploads[2][0]` для третьего документа) + - Проверить, что документы не теряются при обновлении черновика + +--- + +## Важные моменты + +1. **Приоритет определения типа документа:** + - Сначала проверяется `field_label` (более точный) + - Потом проверяется `field_name` (fallback) + +2. **Объединение документов:** + - `documents_meta` всегда объединяется с существующими + - `documents_uploaded` всегда начинается с существующих документов + - Новые документы добавляются только если их нет в существующих + +3. **field_name:** + - Формат: `uploads[{group_index}][0]` + - `group_index` = индекс документа в `documents_required` (0-based) + - Передаётся с фронтенда через параметр `group_index` + +--- + +## Команды для проверки + +```bash +# Проверить документы в черновике +docker exec ticket_form_backend python3 /app/check_documents_detailed.py + +# Проверить документы в таблице +docker exec ticket_form_backend python3 /app/check_claim_documents_table.py + +# Исправить field_name для существующих документов +docker exec ticket_form_backend python3 /app/fix_claim_documents_field_names.py +``` + diff --git a/ticket_form/SESSION_LOG_2025-11-26_NEW_FLOW.md b/ticket_form/SESSION_LOG_2025-11-26_NEW_FLOW.md index 2bfde6d2..74725798 100644 --- a/ticket_form/SESSION_LOG_2025-11-26_NEW_FLOW.md +++ b/ticket_form/SESSION_LOG_2025-11-26_NEW_FLOW.md @@ -141,3 +141,147 @@ ticket_form/ - `ocr_events:{session_id}` — события для конкретного пользователя - `ticket_form:documents_list` — запрос на генерацию списка документов + + +**Дата:** 2025-11-26 +**Время:** ~13:00 MSK + +--- + +## 🎯 Цель сессии + +Концептуальная переработка флоу подачи заявки: +- **Проблема:** Визард генерируется слишком долго (2 минуты), анкета слишком длинная +- **Решение:** Сразу запрашиваем документы, параллельно генерируем визард в бэке + +--- + +## ✅ Что сделано + +### 1. Документация архитектуры +- **Файл:** `docs/NEW_FLOW_ARCHITECTURE.md` +- Описан новый флоу: Description → Documents → Waiting → Claim Review → SMS +- Определены статусы черновиков: `draft_new`, `draft_docs_progress`, `draft_docs_complete`, `draft_claim_ready`, `awaiting_sms` +- Структура payload черновика с новыми полями + +### 2. Frontend компоненты + +#### StepDocumentsNew.tsx (НОВЫЙ) +- Поэкранная загрузка документов (один документ на экран) +- Критичные документы помечены предупреждением +- Возможность пропустить любой документ +- Прогресс-бар загрузки +- Отображение уже загруженных документов + +#### StepWaitingClaim.tsx (НОВЫЙ) +- Экран ожидания формирования заявления +- SSE подписка на события: `document_ocr_completed`, `claim_ready` +- Шаги обработки: OCR → Анализ → Формирование → Готово +- Таймер ожидания +- Таймаут 5 минут с обработкой ошибок + +#### StepDraftSelection.tsx (ОБНОВЛЁН) +- Поддержка новых статусов черновиков +- Визуальное отображение разных статусов (цвета, иконки, описания) +- Прогресс документов (X из Y загружено) +- Legacy черновики помечаются как "устаревший формат" +- Разные действия для разных статусов + +### 3. Backend API + +#### documents.py (НОВЫЙ) +- `POST /api/v1/documents/upload` — загрузка одного документа +- `GET /api/v1/documents/status/{claim_id}` — статус обработки документов +- `POST /api/v1/documents/generate-list` — запрос на генерацию списка документов +- Интеграция с n8n webhook +- Публикация событий в Redis + +#### main.py (ОБНОВЛЁН) +- Добавлен роутер `documents` + +--- + +## 📁 Изменённые файлы + +``` +ticket_form/ +├── docs/ +│ └── NEW_FLOW_ARCHITECTURE.md # НОВЫЙ +├── frontend/src/components/form/ +│ ├── StepDocumentsNew.tsx # НОВЫЙ +│ ├── StepWaitingClaim.tsx # НОВЫЙ +│ └── StepDraftSelection.tsx # ОБНОВЛЁН +├── backend/app/ +│ ├── api/ +│ │ └── documents.py # НОВЫЙ +│ └── main.py # ОБНОВЛЁН +└── SESSION_LOG_2025-11-26_NEW_FLOW.md # НОВЫЙ +``` + +--- + +## ⏳ Что осталось сделать + +### Frontend +- [ ] Обновить `ClaimForm.tsx` — интегрировать новые компоненты в флоу +- [ ] Обновить `StepDescription.tsx` — после описания переходить к документам (не к визарду) + +### Backend +- [ ] Эндпоинт получения списка документов из черновика +- [ ] SSE события для прогресса OCR + +### n8n +- [ ] Воркфлоу: генерация списка документов (быстрый AI запрос) +- [ ] Воркфлоу: OCR документа → заполнение полей визарда +- [ ] Воркфлоу: формирование заявления после всех документов +- [ ] Webhook: `/webhook/document-upload` + +### Тестирование +- [ ] Полный цикл с реальными данными +- [ ] Обработка ошибок +- [ ] Legacy черновики + +--- + +## 🔧 Технические детали + +### Новые SSE события +```javascript +// Список документов готов +{ event_type: "documents_list_ready", documents_required: [...] } + +// Документ загружен (начало OCR) +{ event_type: "document_uploaded", document_type: "contract", status: "processing" } + +// OCR завершён +{ event_type: "document_ocr_completed", document_type: "contract", ocr_data: {...} } + +// Заявление готово +{ event_type: "claim_ready", claim_data: {...} } +``` + +### Статусы черновиков +| Статус | Описание | +|--------|----------| +| `draft_new` | Только описание проблемы | +| `draft_docs_progress` | Часть документов загружена | +| `draft_docs_complete` | Все документы, ждём заявление | +| `draft_claim_ready` | Заявление готово | +| `awaiting_sms` | Ждёт SMS подтверждения | + +### Legacy черновики +- Определяются по отсутствию `documents_required` в payload +- Показываются с пометкой "устаревший формат" +- Кнопка "Начать заново" копирует description в новый черновик + +--- + +## 📌 Примечания + +1. **Ветка backup:** `backup-wizard-ui-2025-11-26` содержит состояние до изменений +2. **n8n:** Webhook `/webhook/document-upload` нужно создать +3. **Redis каналы:** + - `ocr_events:{session_id}` — события для конкретного пользователя + - `ticket_form:documents_list` — запрос на генерацию списка документов + + diff --git a/ticket_form/backend/app/api/claims.py b/ticket_form/backend/app/api/claims.py index 18ae0f8d..4bbc25e0 100644 --- a/ticket_form/backend/app/api/claims.py +++ b/ticket_form/backend/app/api/claims.py @@ -400,6 +400,12 @@ async def get_draft(claim_id: str): logger.info(f"🔍 Загружен черновик: id={row.get('id')}, claim_id={final_claim_id}, channel={row.get('channel')}") + # 🔍 ОТЛАДКА: Логируем наличие documents_required + documents_required = payload.get('documents_required', []) if isinstance(payload, dict) else [] + logger.info(f"🔍 Черновик {final_claim_id}: status_code={row.get('status_code')}, documents_required count={len(documents_required) if isinstance(documents_required, list) else 0}") + if documents_required: + logger.info(f"🔍 documents_required: {documents_required[:2]}...") # Первые 2 для примера + return { "success": True, "claim": { @@ -426,14 +432,13 @@ async def delete_draft(claim_id: str): """ Удалить черновик по claim_id - Удаляет только черновики (status_code = 'draft') + Удаляет черновики с любым статусом (кроме submitted/completed) """ try: query = """ DELETE FROM clpr_claims - WHERE payload->>'claim_id' = $1 - AND status_code = 'draft' - AND channel = 'web_form' + WHERE (payload->>'claim_id' = $1 OR id::text = $1) + AND status_code NOT IN ('submitted', 'completed', 'rejected') RETURNING id """ diff --git a/ticket_form/backend/app/api/documents.py b/ticket_form/backend/app/api/documents.py index 332bc49a..daee5895 100644 --- a/ticket_form/backend/app/api/documents.py +++ b/ticket_form/backend/app/api/documents.py @@ -4,7 +4,7 @@ Documents API Routes - Загрузка и обработка документо Новый флоу: поэкранная загрузка документов """ from fastapi import APIRouter, HTTPException, UploadFile, File, Form, Request -from typing import Optional +from typing import Optional, List import httpx import json import uuid @@ -17,7 +17,7 @@ router = APIRouter(prefix="/api/v1/documents", tags=["Documents"]) logger = logging.getLogger(__name__) # n8n webhook для загрузки документов -N8N_DOCUMENT_UPLOAD_WEBHOOK = "https://n8n.clientright.pro/webhook/document-upload" +N8N_DOCUMENT_UPLOAD_WEBHOOK = "https://n8n.clientright.pro/webhook/webform_document_upload" @router.post("/upload") @@ -27,8 +27,12 @@ async def upload_document( claim_id: str = Form(...), session_id: str = Form(...), document_type: str = Form(...), + document_name: Optional[str] = Form(None), + document_description: Optional[str] = Form(None), + group_index: Optional[str] = Form(None), unified_id: Optional[str] = Form(None), contact_id: Optional[str] = Form(None), + phone: Optional[str] = Form(None), ): """ Загрузка одного документа. @@ -60,26 +64,50 @@ async def upload_document( file_content = await file.read() file_size = len(file_content) - # Формируем данные для отправки в n8n + # Получаем IP клиента + client_ip = request.client.host if request.client else "unknown" + forwarded_for = request.headers.get("x-forwarded-for", "").split(",")[0].strip() + if forwarded_for: + client_ip = forwarded_for + + # Формируем данные в формате совместимом с существующим n8n воркфлоу form_data = { - "claim_id": claim_id, + # Основные идентификаторы + "form_id": "ticket_form", + "stage": "document_upload", "session_id": session_id, + "claim_id": claim_id, + "client_ip": client_ip, + + # Идентификаторы пользователя + "unified_id": unified_id or "", + "contact_id": contact_id or "", + "phone": phone or "", + + # Информация о документе "document_type": document_type, "file_id": file_id, "original_filename": file.filename, "content_type": file.content_type or "application/octet-stream", "file_size": str(file_size), "upload_timestamp": datetime.utcnow().isoformat(), + + # Формат uploads_* для совместимости + # ✅ Используем group_index для правильной индексации (по умолчанию 0) + "uploads_field_names[{idx}]".format(idx=group_index or "0"): document_type, + "uploads_field_labels[{idx}]".format(idx=group_index or "0"): document_name or document_type, + "uploads_descriptions[{idx}]".format(idx=group_index or "0"): document_description or "", } - if unified_id: - form_data["unified_id"] = unified_id - if contact_id: - form_data["contact_id"] = contact_id + # ✅ Добавляем group_index в данные формы + if group_index: + form_data["group_index"] = group_index + logger.info(f"📋 group_index передан в n8n: {group_index}") - # Файл для multipart + # Файл для multipart (ключ uploads[group_index] для совместимости) + idx = group_index or "0" files = { - "file": (file.filename, file_content, file.content_type or "application/octet-stream") + f"uploads[{idx}]": (file.filename, file_content, file.content_type or "application/octet-stream") } # Отправляем в n8n @@ -163,6 +191,174 @@ async def upload_document( ) +@router.post("/upload-multiple") +async def upload_multiple_documents( + request: Request, + files: List[UploadFile] = File(...), + claim_id: str = Form(...), + session_id: str = Form(...), + document_type: str = Form(...), + document_name: Optional[str] = Form(None), + document_description: Optional[str] = Form(None), + unified_id: Optional[str] = Form(None), + contact_id: Optional[str] = Form(None), + phone: Optional[str] = Form(None), +): + """ + Загрузка нескольких файлов для одного документа (например, несколько страниц паспорта). + Все файлы отправляются одним запросом в n8n. + """ + try: + logger.info( + "📤 Multiple documents upload received", + extra={ + "claim_id": claim_id, + "session_id": session_id, + "document_type": document_type, + "files_count": len(files), + "file_names": [f.filename for f in files], + }, + ) + + # Получаем IP клиента + client_ip = request.client.host if request.client else "unknown" + forwarded_for = request.headers.get("x-forwarded-for", "").split(",")[0].strip() + if forwarded_for: + client_ip = forwarded_for + + # Генерируем ID для каждого файла и читаем контент + file_ids = [] + files_multipart = {} + + for i, file in enumerate(files): + file_id = f"doc_{uuid.uuid4().hex[:12]}" + file_ids.append(file_id) + + file_content = await file.read() + files_multipart[f"uploads[{i}]"] = ( + file.filename, + file_content, + file.content_type or "application/octet-stream" + ) + + # Формируем данные формы + form_data = { + # Основные идентификаторы + "form_id": "ticket_form", + "stage": "document_upload", + "session_id": session_id, + "claim_id": claim_id, + "client_ip": client_ip, + + # Идентификаторы пользователя + "unified_id": unified_id or "", + "contact_id": contact_id or "", + "phone": phone or "", + + # Информация о документе + "document_type": document_type, + "files_count": str(len(files)), + "upload_timestamp": datetime.utcnow().isoformat(), + } + + # ✅ Получаем group_index из Form (индекс документа в documents_required) + form_params = await request.form() + group_index = form_params.get("group_index") + if group_index: + form_data["group_index"] = group_index + logger.info(f"📋 group_index передан в n8n: {group_index}") + + # Добавляем информацию о каждом файле + for i, (file, file_id) in enumerate(zip(files, file_ids)): + form_data[f"file_ids[{i}]"] = file_id + form_data[f"uploads_field_names[{i}]"] = document_type + form_data[f"uploads_field_labels[{i}]"] = document_name or document_type + form_data[f"uploads_descriptions[{i}]"] = document_description or "" + form_data[f"original_filenames[{i}]"] = file.filename + + # Отправляем в n8n одним запросом + async with httpx.AsyncClient(timeout=180.0) as client: + response = await client.post( + N8N_DOCUMENT_UPLOAD_WEBHOOK, + data=form_data, + files=files_multipart, + ) + + response_text = response.text or "" + + if response.status_code == 200: + logger.info( + "✅ Multiple documents uploaded to n8n", + extra={ + "claim_id": claim_id, + "document_type": document_type, + "file_ids": file_ids, + "files_count": len(files), + }, + ) + + # Парсим ответ от n8n + try: + n8n_response = json.loads(response_text) + except json.JSONDecodeError: + n8n_response = {"raw": response_text} + + # Публикуем событие в Redis + event_data = { + "event_type": "documents_uploaded", + "status": "processing", + "claim_id": claim_id, + "session_id": session_id, + "document_type": document_type, + "file_ids": file_ids, + "files_count": len(files), + "original_filenames": [f.filename for f in files], + "timestamp": datetime.utcnow().isoformat(), + } + + await redis_service.publish( + f"ocr_events:{session_id}", + json.dumps(event_data, ensure_ascii=False) + ) + + return { + "success": True, + "file_ids": file_ids, + "files_count": len(files), + "document_type": document_type, + "ocr_status": "processing", + "message": f"Загружено {len(files)} файл(ов)", + "n8n_response": n8n_response, + } + + else: + logger.error( + "❌ n8n multiple upload error", + extra={ + "status_code": response.status_code, + "body": response_text[:500], + }, + ) + raise HTTPException( + status_code=response.status_code, + detail=f"Ошибка n8n: {response_text}", + ) + + except httpx.TimeoutException: + logger.error("⏱️ n8n multiple upload timeout") + raise HTTPException(status_code=504, detail="Таймаут загрузки документов") + + except HTTPException: + raise + + except Exception as e: + logger.exception("❌ Multiple upload error") + raise HTTPException( + status_code=500, + detail=f"Ошибка загрузки документов: {str(e)}", + ) + + @router.get("/status/{claim_id}") async def get_documents_status(claim_id: str): """ @@ -198,6 +394,150 @@ async def get_documents_status(claim_id: str): ) + +async def skip_document( + request: Request, + claim_id: str = Form(...), + session_id: str = Form(...), + document_type: str = Form(...), + document_name: Optional[str] = Form(None), + group_index: Optional[str] = Form(None), + unified_id: Optional[str] = Form(None), + contact_id: Optional[str] = Form(None), + phone: Optional[str] = Form(None), +): + """ + Пропуск документа (пользователь указал, что документа нет). + + Отправляет событие в n8n на тот же webhook, что и загрузка файлов, + но с флагом skipped=true для обработки пропуска. + """ + try: + logger.info( + "⏭️ Document skip received", + extra={ + "claim_id": claim_id, + "session_id": session_id, + "document_type": document_type, + "group_index": group_index, + }, + ) + + # Получаем IP клиента + client_ip = request.client.host if request.client else "unknown" + forwarded_for = request.headers.get("x-forwarded-for", "").split(",")[0].strip() + if forwarded_for: + client_ip = forwarded_for + + # Формируем данные в формате совместимом с существующим n8n воркфлоу + form_data = { + # Основные идентификаторы + "form_id": "ticket_form", + "stage": "document_skip", + "session_id": session_id, + "claim_id": claim_id, + "client_ip": client_ip, + + # Идентификаторы пользователя + "unified_id": unified_id or "", + "contact_id": contact_id or "", + "phone": phone or "", + + # Информация о документе + "document_type": document_type, + "document_name": document_name or document_type, + "skipped": "true", # ✅ Флаг пропуска документа + "action": "skip", # ✅ Действие: пропуск + "skip_timestamp": datetime.utcnow().isoformat(), + + # Формат uploads_* для совместимости (без файлов) + # ✅ Используем group_index для правильной индексации (по умолчанию 0) + "uploads_field_names[{idx}]".format(idx=group_index or "0"): document_type, + "uploads_field_labels[{idx}]".format(idx=group_index or "0"): document_name or document_type, + "uploads_descriptions[{idx}]".format(idx=group_index or "0"): "", + "files_count": "0", # ✅ Нет файлов + } + + # ✅ Добавляем group_index в данные формы + if group_index: + form_data["group_index"] = group_index + logger.info(f"📋 group_index передан в n8n: {group_index}") + + # Отправляем в n8n на тот же webhook (без файлов) + async with httpx.AsyncClient(timeout=60.0) as client: + response = await client.post( + N8N_DOCUMENT_UPLOAD_WEBHOOK, + data=form_data, + ) + + response_text = response.text or "" + + if response.status_code == 200: + logger.info( + "✅ Document skip sent to n8n", + extra={ + "claim_id": claim_id, + "document_type": document_type, + "response_preview": response_text[:200], + }, + ) + + # Парсим ответ от n8n + try: + n8n_response = json.loads(response_text) + except json.JSONDecodeError: + n8n_response = {"raw": response_text} + + # Публикуем событие в Redis для фронтенда + event_data = { + "event_type": "document_skipped", + "status": "skipped", + "claim_id": claim_id, + "session_id": session_id, + "document_type": document_type, + "document_name": document_name or document_type, + "timestamp": datetime.utcnow().isoformat(), + } + + await redis_service.publish( + f"ocr_events:{session_id}", + json.dumps(event_data, ensure_ascii=False) + ) + + return { + "success": True, + "document_type": document_type, + "status": "skipped", + "message": "Документ пропущен и сохранён", + "n8n_response": n8n_response, + } + + else: + logger.error( + "❌ n8n document skip error", + extra={ + "status_code": response.status_code, + "body": response_text[:500], + }, + ) + raise HTTPException( + status_code=response.status_code, + detail=f"Ошибка n8n: {response_text}", + ) + + except httpx.TimeoutException: + logger.error("⏱️ n8n document skip timeout") + raise HTTPException(status_code=504, detail="Таймаут отправки пропуска документа") + + except HTTPException: + raise + + except Exception as e: + logger.exception("❌ Document skip error") + raise HTTPException(status_code=500, detail=f"Ошибка пропуска документа: {str(e)}") + + + @router.post("/generate-list") async def generate_documents_list(request: Request): """ @@ -268,3 +608,7 @@ async def generate_documents_list(request: Request): detail=f"Ошибка генерации списка: {str(e)}", ) + + + +router.add_api_route("/skip", skip_document, methods=["POST"], tags=["Documents"]) diff --git a/ticket_form/backend/app/api/events.py b/ticket_form/backend/app/api/events.py index 3b9c9fe3..88240a43 100644 --- a/ticket_form/backend/app/api/events.py +++ b/ticket_form/backend/app/api/events.py @@ -123,10 +123,18 @@ async def stream_events(task_id: str): # Формат уже плоский (от backend API или старых источников) actual_event = event + # ✅ Логируем полученное событие + event_type = actual_event.get('event_type') + logger.info(f"🔍 Processing event: event_type={event_type}, has claim_id={bool(actual_event.get('claim_id'))}") + + # ✅ Обработка нового формата: documents_list_ready + if event_type == 'documents_list_ready': + logger.info(f"📋 Documents list received: {len(actual_event.get('documents_required', []))} documents") + # Просто пропускаем дальше к yield + # ✅ Обработка формата от n8n: если пришёл объект с claim_id, но без event_type # Это значит, что n8n пушит минимальный payload для wizard_ready - logger.info(f"🔍 Checking event: has event_type={bool(actual_event.get('event_type'))}, has claim_id={bool(actual_event.get('claim_id'))}") - if not actual_event.get('event_type') and actual_event.get('claim_id'): + elif not event_type and actual_event.get('claim_id'): logger.info(f"📦 Detected minimal wizard payload (no event_type), wrapping for claim_id={actual_event.get('claim_id')}") # Обёртываем в правильный формат actual_event = { @@ -209,13 +217,21 @@ async def stream_events(task_id: str): # Отправляем событие клиенту (плоский формат) event_json = json.dumps(actual_event, ensure_ascii=False) - logger.info(f"📤 Sending event to client: {actual_event.get('status', 'unknown')}") + event_type_sent = actual_event.get('event_type', 'unknown') + event_status = actual_event.get('status', 'unknown') + logger.info(f"📤 Sending event to client: type={event_type_sent}, status={event_status}") yield f"data: {event_json}\n\n" # Если обработка завершена - закрываем соединение - if actual_event.get('status') in ['completed', 'error', 'success']: + # НЕ закрываем для documents_list_ready и document_ocr_completed (ждём ещё события) + if event_status in ['completed', 'error'] and event_type_sent not in ['documents_list_ready', 'document_ocr_completed', 'document_uploaded']: logger.info(f"✅ Task {task_id} finished, closing SSE") break + + # Закрываем для финальных событий + if event_type_sent in ['claim_ready', 'claim_plan_ready']: + logger.info(f"✅ Final event {event_type_sent} sent, closing SSE") + break else: logger.info(f"⏰ Timeout waiting for message on {channel}") diff --git a/ticket_form/backend/app/main.py b/ticket_form/backend/app/main.py index 44d4f7d9..1a51a5db 100644 --- a/ticket_form/backend/app/main.py +++ b/ticket_form/backend/app/main.py @@ -229,3 +229,4 @@ async def info(): if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8200) + diff --git a/ticket_form/check_claim_documents_table.py b/ticket_form/check_claim_documents_table.py new file mode 100644 index 00000000..41479b04 --- /dev/null +++ b/ticket_form/check_claim_documents_table.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +""" +Проверка документов в таблице clpr_claim_documents +""" +import asyncio +import asyncpg + +POSTGRES_HOST = "147.45.189.234" +POSTGRES_PORT = 5432 +POSTGRES_DB = "default_db" +POSTGRES_USER = "gen_user" +POSTGRES_PASSWORD = "2~~9_^kVsU?2\\S" + +CLAIM_ID = "bddb6815-8e17-4d54-a721-5e94382942c7" + +async def check_documents_table(): + conn = await asyncpg.connect( + host=POSTGRES_HOST, + port=POSTGRES_PORT, + database=POSTGRES_DB, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD + ) + + try: + # Сначала находим UUID claim + claim_row = await conn.fetchrow(""" + SELECT id FROM clpr_claims + WHERE id::text = $1 OR payload->>'claim_id' = $1 + ORDER BY updated_at DESC + LIMIT 1 + """, CLAIM_ID) + + if not claim_row: + print(f"❌ Черновик {CLAIM_ID} не найден!") + return + + claim_uuid = claim_row['id'] + + # Ищем документы по UUID (claim_id в таблице - text) + rows = await conn.fetch(""" + SELECT + ccd.id, + ccd.claim_id, + ccd.field_name, + ccd.file_id, + ccd.file_name, + ccd.original_file_name, + ccd.uploaded_at + FROM clpr_claim_documents ccd + WHERE ccd.claim_id = $1 + ORDER BY ccd.uploaded_at DESC + """, str(claim_uuid)) + + print(f"📋 Найдено {len(rows)} документов в таблице clpr_claim_documents:") + for i, row in enumerate(rows): + print(f"\n {i+1}. field_name: {row['field_name']}") + print(f" file_id: {row['file_id']}") + print(f" file_name: {row['file_name']}") + print(f" original_file_name: {row['original_file_name']}") + print(f" uploaded_at: {row['uploaded_at']}") + + finally: + await conn.close() + +if __name__ == "__main__": + asyncio.run(check_documents_table()) + diff --git a/ticket_form/check_claim_status.py b/ticket_form/check_claim_status.py new file mode 100644 index 00000000..0a8776f8 --- /dev/null +++ b/ticket_form/check_claim_status.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +""" +Проверка статуса жалобы в базе данных +""" +import asyncio +import asyncpg +import json + +# Параметры подключения к БД +POSTGRES_HOST = "147.45.189.234" +POSTGRES_PORT = 5432 +POSTGRES_DB = "default_db" +POSTGRES_USER = "gen_user" +POSTGRES_PASSWORD = "2~~9_^kVsU?2\\S" + +CLAIM_ID = "bddb6815-8e17-4d54-a721-5e94382942c7" + +async def check_claim_status(): + """Проверяет статус жалобы""" + conn = None + try: + conn = await asyncpg.connect( + host=POSTGRES_HOST, + port=POSTGRES_PORT, + database=POSTGRES_DB, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD + ) + + # Запрос статуса жалобы + query = """ + SELECT + id, + status_code, + payload->>'claim_id' as claim_id, + payload->>'current_doc_index' as current_doc_index, + jsonb_array_length(COALESCE(payload->'documents_required', '[]'::jsonb)) as documents_required_count, + jsonb_array_length(COALESCE(payload->'documents_uploaded', '[]'::jsonb)) as documents_uploaded_count, + jsonb_array_length(COALESCE(payload->'documents_skipped', '[]'::jsonb)) as documents_skipped_count, + payload->'documents_required' as documents_required, + payload->'documents_uploaded' as documents_uploaded, + payload->'documents_skipped' as documents_skipped, + created_at, + updated_at + FROM clpr_claims + WHERE id::text = $1 + OR payload->>'claim_id' = $1 + ORDER BY updated_at DESC + LIMIT 1 + """ + + row = await conn.fetchrow(query, CLAIM_ID) + + if not row: + print(f"❌ Жалоба с claim_id '{CLAIM_ID}' не найдена") + return + + print("=" * 80) + print(f"📋 Статус жалобы: {CLAIM_ID}") + print("=" * 80) + print(f"ID в БД: {row['id']}") + print(f"Status Code: {row['status_code']}") + print(f"Claim ID: {row['claim_id']}") + print(f"Current Doc Index: {row['current_doc_index']}") + print(f"\n📊 Статистика документов:") + print(f" - Требуется документов: {row['documents_required_count']}") + print(f" - Загружено документов: {row['documents_uploaded_count']}") + print(f" - Пропущено документов: {row['documents_skipped_count']}") + print(f"\n📅 Даты:") + print(f" - Создано: {row['created_at']}") + print(f" - Обновлено: {row['updated_at']}") + + documents_required = row['documents_required'] if isinstance(row['documents_required'], list) else (json.loads(row['documents_required']) if isinstance(row['documents_required'], str) else []) + documents_uploaded = row['documents_uploaded'] if isinstance(row['documents_uploaded'], list) else (json.loads(row['documents_uploaded']) if isinstance(row['documents_uploaded'], str) else []) + documents_skipped = row['documents_skipped'] if isinstance(row['documents_skipped'], list) else (json.loads(row['documents_skipped']) if isinstance(row['documents_skipped'], str) else []) + + if documents_required: + print(f"\n📄 Требуемые документы:") + for idx, doc in enumerate(documents_required): + doc_obj = doc if isinstance(doc, dict) else json.loads(doc) if isinstance(doc, str) else {} + print(f" {idx}. {doc_obj.get('name', doc_obj.get('id', 'unknown'))} (id: {doc_obj.get('id', 'unknown')})") + + if documents_uploaded: + print(f"\n✅ Загруженные документы:") + for doc in documents_uploaded: + doc_obj = doc if isinstance(doc, dict) else json.loads(doc) if isinstance(doc, str) else {} + print(f" - {doc_obj.get('id', 'unknown')}: {doc_obj.get('file_name', 'N/A')}") + + if documents_skipped: + print(f"\n⏭️ Пропущенные документы:") + for doc in documents_skipped: + doc_obj = doc if isinstance(doc, dict) else json.loads(doc) if isinstance(doc, str) else {} + group_idx = doc_obj.get('group_index', 'N/A') + print(f" - {doc_obj.get('id', 'unknown')} (group_index: {group_idx}): {doc_obj.get('name', 'N/A')}") + + print("\n" + "=" * 80) + + # Определяем, что должно происходить дальше + status = row['status_code'] + uploaded = row['documents_uploaded_count'] or 0 + skipped = row['documents_skipped_count'] or 0 + required = row['documents_required_count'] or 0 + + print(f"\n🔍 Анализ статуса:") + if status == 'draft_docs_complete': + print(" ✅ Все документы обработаны (загружены или пропущены)") + print(" 📝 Должно происходить: формирование заявления (wizard generation)") + elif status == 'draft_docs_progress': + print(" ⏳ Документы загружаются") + remaining = required - uploaded - skipped + print(f" 📊 Осталось обработать: {remaining} документов") + elif status == 'draft_new': + print(" 🆕 Новая жалоба, только описание") + elif status == 'draft_claim_ready': + print(" ✅ Заявление готово к отправке") + else: + print(f" ⚠️ Неизвестный статус: {status}") + + except Exception as e: + print(f"❌ Ошибка: {e}") + import traceback + traceback.print_exc() + finally: + if conn: + await conn.close() + +if __name__ == "__main__": + asyncio.run(check_claim_status()) + diff --git a/ticket_form/check_documents_detailed.py b/ticket_form/check_documents_detailed.py new file mode 100644 index 00000000..abe86059 --- /dev/null +++ b/ticket_form/check_documents_detailed.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +Детальная проверка документов в черновике +""" +import asyncio +import asyncpg +import json + +POSTGRES_HOST = "147.45.189.234" +POSTGRES_PORT = 5432 +POSTGRES_DB = "default_db" +POSTGRES_USER = "gen_user" +POSTGRES_PASSWORD = "2~~9_^kVsU?2\\S" + +CLAIM_ID = "bddb6815-8e17-4d54-a721-5e94382942c7" + +async def check_documents_detailed(): + conn = await asyncpg.connect( + host=POSTGRES_HOST, + port=POSTGRES_PORT, + database=POSTGRES_DB, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD + ) + + try: + row = await conn.fetchrow(""" + SELECT id, status_code, payload, updated_at + FROM clpr_claims + WHERE id::text = $1 OR payload->>'claim_id' = $1 + ORDER BY updated_at DESC + LIMIT 1 + """, CLAIM_ID) + + if not row: + print(f"❌ Черновик {CLAIM_ID} не найден!") + return + + payload = row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']) + + print("=" * 80) + print(f"📋 Статус жалобы: {CLAIM_ID}") + print("=" * 80) + print(f"Status Code: {row['status_code']}") + print(f"Обновлён: {row['updated_at']}") + + # Статистика документов + documents_required = payload.get('documents_required', []) + documents_uploaded = payload.get('documents_uploaded', []) + documents_skipped = payload.get('documents_skipped', []) + current_doc_index = payload.get('current_doc_index', 0) + + print(f"\n📊 Статистика документов:") + print(f" - Требуется документов: {len(documents_required)}") + print(f" - Загружено документов: {len(documents_uploaded)}") + print(f" - Пропущено документов: {len(documents_skipped)}") + print(f" - Current Doc Index: {current_doc_index}") + + # Анализ статуса + print(f"\n🔍 Анализ статуса:") + status = row['status_code'] + uploaded = len(documents_uploaded) + skipped = len(documents_skipped) + required = len(documents_required) + + if status == 'draft_docs_complete': + print(" ✅ Все документы обработаны (загружены или пропущены)") + print(" 📝 Должно происходить: формирование заявления (wizard generation)") + elif status == 'draft_docs_progress': + print(" ⏳ Документы загружаются") + remaining = required - uploaded - skipped + print(f" 📊 Осталось обработать: {remaining} документов") + elif status == 'draft_new': + print(" 🆕 Новая жалоба, только описание") + elif status == 'draft_claim_ready': + print(" ✅ Заявление готово к отправке") + else: + print(f" ⚠️ Статус: {status}") + + print("=" * 80) + print(f"\n📋 documents_meta ({len(payload.get('documents_meta', []))} шт.):") + for i, doc in enumerate(payload.get('documents_meta', [])): + print(f" {i+1}. {doc.get('field_label', 'N/A')}") + print(f" file_id: {doc.get('file_id', 'N/A')[:80]}...") + print(f" field_name: {doc.get('field_name', 'N/A')}") + + print(f"\n📋 documents_uploaded ({len(payload.get('documents_uploaded', []))} шт.):") + for i, doc in enumerate(payload.get('documents_uploaded', [])): + print(f" {i+1}. Тип: {doc.get('type', 'N/A')} / {doc.get('id', 'N/A')}") + print(f" file_id: {doc.get('file_id', 'N/A')[:80]}...") + print(f" original_file_name: {doc.get('original_file_name', 'N/A')}") + + print(f"\n📋 documents_required ({len(payload.get('documents_required', []))} шт.):") + for i, doc in enumerate(payload.get('documents_required', [])): + print(f" {i+1}. {doc.get('name', 'N/A')} (id: {doc.get('id', 'N/A')})") + + print(f"\n📋 current_doc_index: {payload.get('current_doc_index', 'N/A')}") + + # Проверяем уникальность file_id + print(f"\n🔍 Проверка уникальности file_id:") + documents_meta = payload.get('documents_meta', []) + file_ids_meta = [doc.get('file_id') for doc in documents_meta if doc.get('file_id')] + unique_file_ids_meta = list(set(file_ids_meta)) + print(f" documents_meta: всего {len(file_ids_meta)}, уникальных {len(unique_file_ids_meta)}") + if len(file_ids_meta) != len(unique_file_ids_meta): + print(f" ⚠️ ЕСТЬ ДУБЛИКАТЫ!") + from collections import Counter + duplicates = [fid for fid, count in Counter(file_ids_meta).items() if count > 1] + for dup in duplicates: + print(f" - {dup[:80]}... (встречается {Counter(file_ids_meta)[dup]} раз)") + + documents_uploaded = payload.get('documents_uploaded', []) + file_ids_uploaded = [doc.get('file_id') for doc in documents_uploaded if doc.get('file_id')] + unique_file_ids_uploaded = list(set(file_ids_uploaded)) + print(f" documents_uploaded: всего {len(file_ids_uploaded)}, уникальных {len(unique_file_ids_uploaded)}") + if len(file_ids_uploaded) != len(unique_file_ids_uploaded): + print(f" ⚠️ ЕСТЬ ДУБЛИКАТЫ!") + + finally: + await conn.close() + +if __name__ == "__main__": + asyncio.run(check_documents_detailed()) + diff --git a/ticket_form/check_documents_mismatch.py b/ticket_form/check_documents_mismatch.py new file mode 100644 index 00000000..8724508a --- /dev/null +++ b/ticket_form/check_documents_mismatch.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +""" +Проверка несоответствия между documents_uploaded и clpr_claim_documents +""" +import asyncio +import asyncpg +import json + +POSTGRES_HOST = "147.45.189.234" +POSTGRES_PORT = 5432 +POSTGRES_DB = "default_db" +POSTGRES_USER = "gen_user" +POSTGRES_PASSWORD = "2~~9_^kVsU?2\\S" + +CLAIM_ID = "bddb6815-8e17-4d54-a721-5e94382942c7" + +async def check_mismatch(): + conn = await asyncpg.connect( + host=POSTGRES_HOST, + port=POSTGRES_PORT, + database=POSTGRES_DB, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD + ) + + try: + # Находим UUID claim + claim_row = await conn.fetchrow(""" + SELECT id FROM clpr_claims + WHERE id::text = $1 OR payload->>'claim_id' = $1 + ORDER BY updated_at DESC + LIMIT 1 + """, CLAIM_ID) + + if not claim_row: + print(f"❌ Черновик {CLAIM_ID} не найден!") + return + + claim_uuid = claim_row['id'] + + # Получаем payload + payload_row = await conn.fetchrow(""" + SELECT payload FROM clpr_claims WHERE id = $1 + """, claim_uuid) + + payload = payload_row['payload'] if isinstance(payload_row['payload'], dict) else json.loads(payload_row['payload']) + + # Получаем документы из таблицы + table_docs = await conn.fetch(""" + SELECT + ccd.id, + ccd.claim_id, + ccd.field_name, + ccd.file_id, + ccd.file_name, + ccd.original_file_name, + ccd.uploaded_at + FROM clpr_claim_documents ccd + WHERE ccd.claim_id = $1 + ORDER BY ccd.uploaded_at DESC + """, str(claim_uuid)) + + print(f"📋 Документы в таблице clpr_claim_documents ({len(table_docs)} шт.):") + for i, doc in enumerate(table_docs): + print(f" {i+1}. field_name: {doc['field_name']}") + print(f" file_id: {doc['file_id']}") + print(f" file_name: {doc['file_name']}") + print(f" original_file_name: {doc['original_file_name']}") + print(f" uploaded_at: {doc['uploaded_at']}") + + print(f"\n📋 Документы в documents_uploaded ({len(payload.get('documents_uploaded', []))} шт.):") + for i, doc in enumerate(payload.get('documents_uploaded', [])): + print(f" {i+1}. Тип: {doc.get('type', 'N/A')} / {doc.get('id', 'N/A')}") + print(f" file_id: {doc.get('file_id', 'N/A')}") + print(f" original_file_name: {doc.get('original_file_name', 'N/A')}") + + print(f"\n📋 Документы в documents_meta ({len(payload.get('documents_meta', []))} шт.):") + for i, doc in enumerate(payload.get('documents_meta', [])): + print(f" {i+1}. field_label: {doc.get('field_label', 'N/A')}") + print(f" field_name: {doc.get('field_name', 'N/A')}") + print(f" file_id: {doc.get('file_id', 'N/A')}") + + # Проверяем, какие документы из documents_uploaded отсутствуют в таблице + print(f"\n🔍 Проверка отсутствующих документов:") + table_file_ids = {doc['file_id'] for doc in table_docs} + uploaded_file_ids = {doc.get('file_id') for doc in payload.get('documents_uploaded', []) if doc.get('file_id')} + + missing_in_table = uploaded_file_ids - table_file_ids + if missing_in_table: + print(f" ⚠️ В documents_uploaded есть, но нет в таблице ({len(missing_in_table)} шт.):") + for file_id in missing_in_table: + doc = next((d for d in payload.get('documents_uploaded', []) if d.get('file_id') == file_id), None) + if doc: + print(f" - {doc.get('type', 'N/A')}: {file_id[:80]}...") + print(f" original_file_name: {doc.get('original_file_name', 'N/A')}") + else: + print(f" ✅ Все документы из documents_uploaded есть в таблице") + + # Проверяем field_name + print(f"\n🔍 Проверка field_name:") + table_field_names = {doc['field_name'] for doc in table_docs} + meta_field_names = {doc.get('field_name') for doc in payload.get('documents_meta', []) if doc.get('field_name')} + + print(f" В таблице: {sorted(table_field_names)}") + print(f" В documents_meta: {sorted(meta_field_names)}") + + # Проверяем, есть ли конфликты по field_name + if len(table_docs) < len(payload.get('documents_uploaded', [])): + print(f"\n ⚠️ Возможная причина: несколько документов с одинаковым field_name") + print(f" В таблице используется UNIQUE constraint на (claim_id, field_name)") + print(f" Если два документа имеют одинаковый field_name, второй перезапишет первый") + + finally: + await conn.close() + +if __name__ == "__main__": + asyncio.run(check_mismatch()) + diff --git a/ticket_form/check_draft_documents.py b/ticket_form/check_draft_documents.py new file mode 100644 index 00000000..1dc2ece6 --- /dev/null +++ b/ticket_form/check_draft_documents.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Проверка документов в черновике +""" +import asyncio +import asyncpg +import json + +POSTGRES_HOST = "147.45.189.234" +POSTGRES_PORT = 5432 +POSTGRES_DB = "default_db" +POSTGRES_USER = "gen_user" +POSTGRES_PASSWORD = "2~~9_^kVsU?2\\S" + +CLAIM_ID = "bddb6815-8e17-4d54-a721-5e94382942c7" + +async def check_documents(): + conn = await asyncpg.connect( + host=POSTGRES_HOST, + port=POSTGRES_PORT, + database=POSTGRES_DB, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD + ) + + try: + row = await conn.fetchrow(""" + SELECT id, status_code, payload + FROM clpr_claims + WHERE id::text = $1 OR payload->>'claim_id' = $1 + ORDER BY updated_at DESC + LIMIT 1 + """, CLAIM_ID) + + if not row: + print(f"❌ Черновик {CLAIM_ID} не найден!") + return + + payload = row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']) + + print("📋 documents_meta:") + for i, doc in enumerate(payload.get('documents_meta', [])): + print(f" {i+1}. {doc.get('field_label', 'N/A')} - {doc.get('file_id', 'N/A')}") + + print("\n📋 documents_uploaded:") + for i, doc in enumerate(payload.get('documents_uploaded', [])): + print(f" {i+1}. {doc.get('type', 'N/A')} / {doc.get('id', 'N/A')} - {doc.get('file_id', 'N/A')}") + + print("\n📋 Все file_id в payload:") + # Ищем все file_id в payload + payload_str = json.dumps(payload, ensure_ascii=False) + import re + file_ids = re.findall(r'file_id["\']?\s*:\s*["\']([^"\']+)', payload_str) + for file_id in set(file_ids): + print(f" - {file_id}") + + finally: + await conn.close() + +if __name__ == "__main__": + asyncio.run(check_documents()) + diff --git a/ticket_form/docs/N8N_CODE_PREPARE_DOCUMENT_SKIP_SQL.js b/ticket_form/docs/N8N_CODE_PREPARE_DOCUMENT_SKIP_SQL.js new file mode 100644 index 00000000..ef901b2e --- /dev/null +++ b/ticket_form/docs/N8N_CODE_PREPARE_DOCUMENT_SKIP_SQL.js @@ -0,0 +1,113 @@ +// ============================================================================ +// n8n Code Node: Подготовка параметров для SQL при пропуске документа +// ============================================================================ +// Входные данные: массив с объектом [{ propertyName: {...}, body: {...} }] +// Выходные данные: { $1: jsonb_payload, $2: claim_id_string } +// ============================================================================ + +// Получаем входные данные +const inputData = $input.all(); + +if (!inputData || inputData.length === 0) { + return [{ + json: { + error: "Нет входных данных", + $1: null, + $2: null + } + }]; +} + +// Берём первый элемент +// Если это массив - берём первый элемент массива +// Если это объект - используем его напрямую +let firstItem = inputData[0].json; + +if (Array.isArray(firstItem)) { + firstItem = firstItem[0]; +} + +// Извлекаем данные +const propertyName = firstItem.propertyName || {}; +const body = firstItem.body || {}; + +// Извлекаем claim_id (приоритет: body -> propertyName) +const claim_id = body.claim_id || propertyName.claim_id || null; + +if (!claim_id) { + return [{ + json: { + error: "claim_id не найден", + $1: null, + $2: null, + debug: { + body_keys: Object.keys(body), + propertyName_keys: Object.keys(propertyName) + } + } + }]; +} + +// Формируем payload для $1 (jsonb) +// SQL ищет данные в разных местах: p->>'document_type', p->'body'->>'document_type', p->'edit_fields_raw'->'body'->>'document_type' +const payload = { + // ✅ Основные идентификаторы (в корне для быстрого доступа) + session_id: body.session_id || propertyName.session_id, + claim_id: claim_id, + unified_id: body.unified_id || propertyName.unified_id, + contact_id: body.contact_id || propertyName.contact_id, + phone: body.phone || propertyName.phone, + + // ✅ Информация о пропущенном документе (в корне для быстрого доступа) + document_type: body.document_type, + document_name: body.document_name || body.document_type, + group_index: body.group_index ? parseInt(body.group_index) : (body.group_index || null), + + // ✅ Метаданные пропуска + skipped: body.skipped, + action: body.action, + skip_timestamp: body.skip_timestamp || new Date().toISOString(), + + // ✅ Данные из propertyName (для сохранения в payload) + problem_description: propertyName.description || propertyName.problem_description, + email: propertyName.email, + + // ✅ Данные из body (для совместимости) + form_id: body.form_id, + stage: body.stage, + client_ip: body.client_ip, + + // ✅ Поля для совместимости с существующим SQL (SQL ищет данные здесь) + body: { + document_type: body.document_type, + document_name: body.document_name || body.document_type, + group_index: body.group_index ? parseInt(body.group_index) : (body.group_index || null), + session_id: body.session_id, + claim_id: claim_id, + unified_id: body.unified_id, + contact_id: body.contact_id, + phone: body.phone + }, + edit_fields_raw: { + propertyName: propertyName, + body: body + }, + edit_fields_parsed: { + propertyName: propertyName, + body: body + } +}; + +// Возвращаем параметры для SQL +return [{ + json: { + $1: payload, // JSONB payload для SQL (будет передан как $1::jsonb) + $2: claim_id, // TEXT claim_id для SQL (будет передан как $2::text) + // Дополнительные поля для отладки + claim_id: claim_id, + document_type: body.document_type, + document_name: body.document_name, + group_index: body.group_index + } +}]; + diff --git a/ticket_form/docs/N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js b/ticket_form/docs/N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js new file mode 100644 index 00000000..3f371273 --- /dev/null +++ b/ticket_form/docs/N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js @@ -0,0 +1,160 @@ +// ============================================================================ +// n8n Code Node: Обработка загруженных файлов (ИСПРАВЛЕННАЯ ВЕРСИЯ) +// ============================================================================ +// OCR возвращает объединённые документы: один файл на группу (group_index) +// Структура: { data: [{ group_index_num: 0, files_count: 2, newfile: "...", ... }] } +// Решение: обрабатываем каждый элемент из data как объединённый документ +// ============================================================================ + +// ==== INPUT SHAPE SUPPORT ==== +// OCR возвращает: { data: [ ...объединённые документы... ] } +const raw = $json; +const items = Array.isArray(raw?.data) ? raw.data : (Array.isArray(raw) ? raw : []); + +if (!items.length) { + return [{ + json: { + claim_id: null, + payload_partial_json: { documents_meta: [], edit_fields_raw: null, edit_fields_parsed: null }, + filesRows: [] + } + }]; +} + +// ==== CLAIM_ID DISCOVERY ==== +let claim_id = $json.claim_id + || $items('Edit Fields6')?.[0]?.json?.propertyName?.case_id + || $('Edit Fields6').first().json.body.claim_id + || null; + +// ==== UTILS ==== +const safeStr = (v) => (v == null ? '' : String(v)); +const nowIso = new Date().toISOString(); +const tryParseJSON = (x) => { + if (x == null) return null; + if (typeof x === 'object') return x; + if (typeof x === 'string') { try { return JSON.parse(x); } catch { return null; } } + return null; +}; + +// ==== ПРЕДВАРИТЕЛЬНО СОБИРАЕМ uploads_field_labels ИЗ BODY ==== +const editRaw = $items('Edit Fields6')?.[0]?.json || null; +const body = editRaw?.body || null; + +let uploads_descriptions = []; +let uploads_field_names = []; +let uploads_field_labels = []; + +if (body && typeof body === 'object') { + const d = []; + const f = []; + const l = []; + for (const k of Object.keys(body)) { + const mD = k.match(/^uploads_descriptions\[(\d+)\]$/); + const mF = k.match(/^uploads_field_names\[(\d+)\]$/); + const mL = k.match(/^uploads_field_labels\[(\d+)\]$/); + if (mD) d[Number(mD[1])] = safeStr(body[k]); + if (mF) f[Number(mF[1])] = safeStr(body[k]); + if (mL) l[Number(mL[1])] = safeStr(body[k]); + } + uploads_descriptions = d.filter(v => v !== undefined); + uploads_field_names = f.filter(v => v !== undefined); + uploads_field_labels = l.filter(v => v !== undefined); +} + +// ==== BUILD documents_meta + filesRows ==== +// OCR возвращает объединённые документы: один файл на group_index +// Каждый элемент из data - это уже объединённый PDF (может содержать несколько страниц) +const documents_meta = []; +const filesRows = []; + +for (const it of items) { + // ✅ ПРИОРИТЕТ: Используем group_index из body (переданный с фронтенда) + // Если его нет - используем group_index_num из OCR + // Если и его нет - пытаемся определить по document_type из uploads_field_names + let grp = null; + + if (body && body.group_index !== undefined && body.group_index !== null) { + grp = Number(body.group_index); + } else if (it.group_index_num !== undefined && it.group_index_num !== null) { + grp = Number(it.group_index_num); + } else { + // Fallback: пытаемся определить по document_type + const doc_type = uploads_field_names[0] || uploads_field_labels[0] || ''; + // Ищем индекс в documents_required по типу документа + // Это не идеально, но лучше чем всегда 0 + grp = 0; // По умолчанию 0, если не можем определить + } + + grp = grp || 0; + const file_index = 0; // После объединения всегда один файл на группу + + const field_name = `uploads[${grp}][${file_index}]`; + + // ✅ ИСПРАВЛЕНО: uploads_field_labels содержит элементы с индексом 0 (текущий запрос), + // а grp - это позиция в documents_required. Используем индекс 0 для массивов текущего запроса. + const field_label = uploads_field_labels[0] || uploads_field_names[0] || uploads_descriptions[0] || `group-${grp}`; + + // OCR уже объединил файлы, используем newfile (путь к объединённому файлу) + const draft_key = safeStr(it.newfile || (it.folder && it.file_name ? `${it.folder}/${it.file_name}` : '')); + const original_name = safeStr(it.file_name || `group_${grp}.pdf`); + const description = safeStr(it.description || uploads_descriptions[0] || ''); + const prefix = safeStr(it.prefix || ''); + + // files_count показывает, сколько исходных файлов было объединено + const files_count = Number(it.files_count) || 1; + const pages = Number(it.pages) || null; + + documents_meta.push({ + field_name, + field_label, + file_id: draft_key, + file_name: original_name, + original_file_name: original_name, + uploaded_at: nowIso, + files_count, // Информация: сколько файлов было объединено + pages, // Информация: сколько страниц в объединённом PDF + }); + + filesRows.push({ + claim_id, + group_index: grp, + file_index, // Всегда 0 для объединённого документа + original_name, + draft_key, + mime: 'application/pdf', + size_bytes: null, + description, + prefix, + field_name, + field_label, + files_count, // Информация для отладки + pages, // Информация для отладки + }); +} + +// ==== ПОДТЯГИВАЕМ ВСЁ ИЗ "Edit Fields" ==== +const propertyName = editRaw?.propertyName || null; +const answers_parsed = body ? (tryParseJSON(body.answers) || null) : null; +const wizard_plan_parsed = body ? (tryParseJSON(body.wizard_plan) || null) : null; + +// ==== OUTPUT ==== +return [{ + json: { + claim_id, + payload_partial_json: { + documents_meta, + edit_fields_raw: editRaw || null, + edit_fields_parsed: { + propertyName, + body, + uploads_descriptions, + uploads_field_names, + uploads_field_labels, + answers_parsed, + wizard_plan_parsed, + } + }, + filesRows + } +}]; diff --git a/ticket_form/docs/N8N_CODE_PUSH_DOCUMENTS_LIST.js b/ticket_form/docs/N8N_CODE_PUSH_DOCUMENTS_LIST.js new file mode 100644 index 00000000..8abc475a --- /dev/null +++ b/ticket_form/docs/N8N_CODE_PUSH_DOCUMENTS_LIST.js @@ -0,0 +1,115 @@ +// ============================================================================ +// n8n Code Node: Пуш списка документов в Redis +// ============================================================================ +// Расположение в workflow: +// Redis Trigger (ticket_form:description) +// → AI Agent (анализ проблемы) +// → PostgreSQL (SQL_SAVE_DRAFT_NEW_FLOW.sql) +// → [ЭТОТ CODE NODE] +// → Redis Publish +// ============================================================================ + +// Получаем результат из PostgreSQL +const sqlResult = $input.first().json; + +// claim содержит результат SQL запроса +const claim = sqlResult.claim || sqlResult; + +// Валидация +if (!claim.session_token) { + throw new Error('Нет session_token в результате SQL'); +} + +if (!claim.documents_required || claim.documents_required.length === 0) { + console.log('⚠️ Список документов пуст, но продолжаем'); +} + +// Формируем событие для Redis +const event = { + event_type: 'documents_list_ready', + status: 'ready', + + // Идентификаторы + claim_id: claim.claim_id, + session_id: claim.session_token, + + // ✅ Список документов для фронтенда + documents_required: claim.documents_required || [], + documents_count: claim.documents_count || 0, + + // Метаданные + timestamp: new Date().toISOString(), + message: 'Список необходимых документов готов' +}; + +// Логируем для отладки +console.log('📤 Публикуем событие documents_list_ready:', { + channel: `ocr_events:${claim.session_token}`, + documents_count: event.documents_count, + claim_id: event.claim_id +}); + +// Возвращаем для Redis Publish node +return { + json: { + // Канал Redis (ocr_events:{session_id}) + channel: `ocr_events:${claim.session_token}`, + + // Данные события (будут JSON.stringify в Redis node) + message: JSON.stringify(event), + + // Дополнительно передаём для следующих нод + claim_id: claim.claim_id, + session_token: claim.session_token, + documents_required: claim.documents_required + } +}; + + +// ============================================================================ +// Пример структуры documents_required: +// ============================================================================ +// [ +// { +// "id": "contract", +// "name": "Договор или заказ", +// "required": false, +// "priority": 1, +// "accept": ["pdf", "jpg", "png"], +// "hints": "Поскольку договор не выслан, можно приложить публичную оферту" +// }, +// { +// "id": "payment", +// "name": "Чек или подтверждение оплаты", +// "required": false, +// "priority": 1, +// "accept": ["pdf", "jpg", "png"], +// "hints": "Копия квитанции, чека или банковской выписки" +// }, +// { +// "id": "correspondence", +// "name": "Переписка", +// "required": true, // ⚠️ КРИТИЧНЫЙ документ +// "priority": 2, +// "accept": ["pdf", "jpg", "png"], +// "hints": "Скриншоты переписки с организацией, претензии" +// } +// ] +// ============================================================================ + + +// ============================================================================ +// Настройка Redis Publish node (следующая нода): +// ============================================================================ +// +// Operation: Publish +// Channel: {{ $json.channel }} +// Message: {{ $json.message }} +// +// Или через Execute Command: +// Command: PUBLISH +// Arguments: +// - {{ $json.channel }} +// - {{ $json.message }} +// ============================================================================ + diff --git a/ticket_form/docs/N8N_SQL_PARAMETERS_DOCUMENT_SKIP.md b/ticket_form/docs/N8N_SQL_PARAMETERS_DOCUMENT_SKIP.md new file mode 100644 index 00000000..f9432aa4 --- /dev/null +++ b/ticket_form/docs/N8N_SQL_PARAMETERS_DOCUMENT_SKIP.md @@ -0,0 +1,112 @@ +# Параметры для SQL при пропуске документа + +## Входные данные n8n + +Массив с объектом: +```json +[ + { + "propertyName": { + "session_id": "sess_f47c9f47-a727-4176-bf3d-26a02bb2fe24", + "phone": "79262306381", + "unified_id": "usr_90599ff2-ac79-4236-b950-0df85395096c", + "contact_id": "320096", + "claim_id": "bddb6815-8e17-4d54-a721-5e94382942c7", + "description": "...", + "email": "help@clientright.ru", + ... + }, + "body": { + "form_id": "ticket_form", + "stage": "document_skip", + "session_id": "sess_f47c9f47-a727-4176-bf3d-26a02bb2fe24", + "claim_id": "bddb6815-8e17-4d54-a721-5e94382942c7", + "unified_id": "usr_90599ff2-ac79-4236-b950-0df85395096c", + "contact_id": "320096", + "phone": "79262306381", + "document_type": "correspondence", + "document_name": "Переписка", + "skipped": "true", + "action": "skip", + "skip_timestamp": "2025-11-27T12:35:46.915646", + "group_index": "2" + } + } +] +``` + +## Параметры для SQL + +### $1 (JSONB payload) + +Структура payload должна содержать данные в разных местах для совместимости с SQL: + +```json +{ + // В корне (для быстрого доступа) + "session_id": "sess_f47c9f47-a727-4176-bf3d-26a02bb2fe24", + "claim_id": "bddb6815-8e17-4d54-a721-5e94382942c7", + "unified_id": "usr_90599ff2-ac79-4236-b950-0df85395096c", + "contact_id": "320096", + "phone": "79262306381", + "document_type": "correspondence", + "document_name": "Переписка", + "group_index": 2, + + // В body (SQL ищет здесь: p->'body'->>'document_type') + "body": { + "document_type": "correspondence", + "document_name": "Переписка", + "group_index": 2, + "session_id": "sess_f47c9f47-a727-4176-bf3d-26a02bb2fe24", + "claim_id": "bddb6815-8e17-4d54-a721-5e94382942c7", + "unified_id": "usr_90599ff2-ac79-4236-b950-0df85395096c", + "contact_id": "320096", + "phone": "79262306381" + }, + + // В edit_fields_raw (SQL ищет здесь: p->'edit_fields_raw'->'body'->>'document_type') + "edit_fields_raw": { + "propertyName": { ... }, + "body": { ... } + }, + + // В edit_fields_parsed (SQL ищет здесь: p->'edit_fields_parsed'->'body'->>'document_type') + "edit_fields_parsed": { + "propertyName": { ... }, + "body": { ... } + }, + + // Дополнительные поля + "problem_description": "...", + "email": "help@clientright.ru", + "skipped": "true", + "action": "skip", + "skip_timestamp": "2025-11-27T12:35:46.915646" +} +``` + +### $2 (TEXT claim_id) + +Просто строка с claim_id: +``` +"bddb6815-8e17-4d54-a721-5e94382942c7" +``` + +## Использование в n8n + +1. **Code Node** (`N8N_CODE_PREPARE_DOCUMENT_SKIP_SQL.js`) - подготавливает параметры +2. **PostgreSQL Node** - выполняет SQL запрос `SQL_CLAIMSAVE_DOCUMENT_SKIP.sql` с параметрами: + - Parameter Name: `$1`, Value: `={{ $json.$1 }}` (JSON) + - Parameter Name: `$2`, Value: `={{ $json.$2 }}` (String) + +## Важно + +SQL запрос ищет данные в следующем порядке: +1. `partial.p->>'document_type'` - в корне payload +2. `partial.p->'body'->>'document_type'` - в body +3. `partial.p->'edit_fields_raw'->'body'->>'document_type'` - в edit_fields_raw.body +4. `partial.p->'edit_fields_parsed'->'body'->>'document_type'` - в edit_fields_parsed.body + +Поэтому payload должен содержать данные во всех этих местах для надёжности. + diff --git a/ticket_form/docs/NEW_FLOW_ARCHITECTURE.md b/ticket_form/docs/NEW_FLOW_ARCHITECTURE.md index 3c1f423c..2255d118 100644 --- a/ticket_form/docs/NEW_FLOW_ARCHITECTURE.md +++ b/ticket_form/docs/NEW_FLOW_ARCHITECTURE.md @@ -381,3 +381,387 @@ Redis Publish: claim_ready | Количество вопросов | 10-15 | 0-3 (только уточняющие) | | Конверсия | ? | ↑ (меньше отвала) | + + +**Дата создания:** 2025-11-26 +**Статус:** В разработке + +--- + +## 📋 Проблема + +Текущий флоу слишком медленный: +1. **2 минуты** — генерация визарда (RAG + AI анализ) +2. **Длинная анкета** — слишком много вопросов для пользователя + +--- + +## ✅ Новое решение + +### Концепция +1. После описания проблемы → сразу запрашиваем документы (без ожидания визарда) +2. Пока пользователь загружает документы → в бэке генерируется визард + OCR +3. После всех документов → показываем готовое заявление на апрув + +### Преимущества +- **Быстрый старт** — пользователь не ждёт 2 минуты +- **Параллельная работа** — OCR и визард генерируются пока пользователь ищет документы +- **Меньше вопросов** — большая часть данных извлекается из документов + +--- + +## 🔄 Новый флоу (шаги) + +``` +┌─────────────────┐ +│ 1. Телефон │ (уже есть) +│ SMS верификация +└────────┬────────┘ + ▼ +┌─────────────────┐ +│ 2. Черновики │ (уже есть, обновить UI) +│ - Новые статусы│ +│ - Legacy→"Начать заново" +└────────┬────────┘ + ▼ +┌─────────────────┐ +│ 3. Описание │ (уже есть) +│ Свободный текст│ +└────────┬────────┘ + │ + ▼ → n8n: быстрая генерация списка документов (5-10 сек) + │ → n8n: параллельно запускает генерацию визарда (в фоне) + ▼ +┌─────────────────┐ +│ 4. Документы │ 🆕 НОВЫЙ КОМПОНЕНТ +│ - Поэкранная загрузка +│ - Критичные помечены +│ - Можно пропустить +└────────┬────────┘ + │ + ▼ → n8n: OCR каждого документа → заполнение визарда (в фоне) + │ + ▼ +┌─────────────────┐ +│ 5. Ожидание │ 🆕 НОВЫЙ КОМПОНЕНТ +│ "Формируем заявление..." +│ Loader + прогресс +└────────┬────────┘ + │ + ▼ ← n8n: claim_ready event (SSE) + ▼ +┌─────────────────┐ +│ 6. Заявление │ (уже есть StepClaimConfirmation) +│ Просмотр + редактирование +└────────┬────────┘ + ▼ +┌─────────────────┐ +│ 7. SMS апрув │ (уже есть) +└─────────────────┘ +``` + +--- + +## 📊 Статусы черновика (status_code) + +| Статус | Описание | UI при открытии | +|--------|----------|-----------------| +| `draft_new` | Только описание | → Шаг документов | +| `draft_docs_progress` | Часть документов загружена | → Продолжить с текущего документа | +| `draft_docs_complete` | Все документы загружены | → Показать loader | +| `draft_claim_ready` | Заявление готово | → Показать заявление | +| `awaiting_sms` | Ждёт SMS | → Форма SMS | +| `approved` | Отправлено | Не показываем | + +### Legacy черновики (старый формат) +- Нет `documents_required` → показываем с пометкой "устаревший" +- Кнопка "Начать заново" → копирует description, создаёт новый черновик + +--- + +## 📦 Структура payload черновика + +```json +{ + // === Идентификаторы === + "claim_id": "CLM-2025-11-26-X7Y8Z9", + "session_token": "sess_abc123...", + "unified_id": "user_456...", + "phone": "+79991234567", + "email": "user@example.com", + + // === Описание проблемы === + "problem_description": "Купил курсы за 50000р, компания не отвечает...", + + // === Документы (новое!) === + "documents_required": [ + { + "type": "contract", + "name": "Договор или оферта", + "critical": true, + "hints": "Скриншот или PDF договора/оферты" + }, + { + "type": "payment", + "name": "Подтверждение оплаты", + "critical": true, + "hints": "Чек, выписка из банка, скриншот платежа" + }, + { + "type": "correspondence", + "name": "Переписка с продавцом", + "critical": false, + "hints": "Скриншоты переписки, email, чаты" + } + ], + "documents_uploaded": [ + { + "type": "contract", + "file_id": "s3://...", + "ocr_status": "completed", + "ocr_data": {...} + } + ], + "documents_skipped": ["correspondence"], + "current_doc_index": 1, + + // === Визард (генерируется в фоне) === + "wizard_plan": {...}, // AI-generated questions + "wizard_answers": {...}, // Auto-filled from OCR + "wizard_ready": true, // Флаг готовности + + // === Заявление === + "claim_ready": false, // Флаг готовности заявления + "claim_data": { // Готовое заявление для апрува + "applicant": {...}, + "case": {...}, + "contract_or_service": {...}, + "offenders": [...], + "claim": {...}, + "attachments": [...] + }, + + // === Метаданные === + "created_at": "2025-11-26T10:00:00Z", + "updated_at": "2025-11-26T10:05:00Z" +} +``` + +--- + +## 🔌 API Endpoints + +### Существующие (без изменений) +- `POST /api/v1/claims/description` — публикация описания в Redis +- `GET /api/v1/claims/drafts/list` — список черновиков +- `GET /api/v1/claims/drafts/{claim_id}` — полные данные черновика +- `POST /api/v1/claims/approve` — финальный апрув (SMS) + +### Новые/Изменённые + +#### 1. SSE: Получение списка документов +``` +GET /api/v1/events/{session_id} + +Event: documents_list_ready +Data: { + "event_type": "documents_list_ready", + "documents_required": [...] +} +``` + +#### 2. Загрузка документа +``` +POST /api/v1/documents/upload +Content-Type: multipart/form-data + +Body: +- claim_id: string +- document_type: string (contract, payment, etc.) +- file: binary + +Response: +{ + "success": true, + "file_id": "s3://...", + "ocr_status": "processing" +} +``` + +#### 3. SSE: Статус OCR и формирования заявления +``` +GET /api/v1/events/{session_id} + +Event: document_ocr_completed +Data: { + "event_type": "document_ocr_completed", + "document_type": "contract", + "ocr_data": {...} +} + +Event: claim_ready +Data: { + "event_type": "claim_ready", + "claim_data": {...} +} +``` + +#### 4. Получение статуса черновика +``` +GET /api/v1/claims/drafts/{claim_id}/status + +Response: +{ + "status_code": "draft_docs_progress", + "documents_total": 3, + "documents_uploaded": 1, + "documents_skipped": 0, + "wizard_ready": false, + "claim_ready": false +} +``` + +--- + +## 🖥️ Frontend компоненты + +### 1. StepDocumentsNew.tsx (НОВЫЙ) +```tsx +// Поэкранная загрузка документов +// Один документ на экран +// Критичные помечены алертом +// Кнопки: "Загрузить", "Пропустить", "Назад" + +interface Props { + documents: DocumentConfig[]; + currentIndex: number; + onUpload: (file: File) => void; + onSkip: () => void; + onNext: () => void; + onPrev: () => void; +} +``` + +### 2. StepWaitingClaim.tsx (НОВЫЙ) +```tsx +// Loader пока формируется заявление +// Прогресс: "OCR документов...", "Анализ данных...", "Формирование заявления..." +// SSE подписка на claim_ready + +interface Props { + sessionId: string; + onClaimReady: (claimData: any) => void; +} +``` + +### 3. StepDraftSelection.tsx (ОБНОВИТЬ) +```tsx +// Новые статусы черновиков +// Разные действия для разных статусов +// Legacy черновики → "Начать заново" +``` + +### 4. ClaimForm.tsx (ОБНОВИТЬ) +```tsx +// Новая логика шагов +// Убрать StepWizardPlan из основного флоу +// Добавить StepDocumentsNew и StepWaitingClaim +``` + +--- + +## ⚙️ n8n Воркфлоу + +### 1. Генерация списка документов (быстрая) +``` +Redis Trigger (ticket_form:description) + ↓ +AI: Быстрый анализ → список документов (5-10 сек) + ↓ +Redis Publish (ocr_events:{session_id}) + + event_type: documents_list_ready + ↓ +PostgreSQL: Сохранить documents_required в черновик + ↓ +Параллельно: Запустить генерацию визарда (отдельный воркфлоу) +``` + +### 2. Генерация визарда (фоновая) +``` +(Запускается из воркфлоу 1) + ↓ +AI Agent: RAG + генерация вопросов (2 мин) + ↓ +PostgreSQL: Сохранить wizard_plan в черновик + + wizard_ready = true +``` + +### 3. OCR документа +``` +Webhook (upload документа) + ↓ +S3 Upload + ↓ +AI Vision: OCR + извлечение данных + ↓ +PostgreSQL: Сохранить в documents_uploaded + ↓ +Redis Publish: document_ocr_completed + ↓ +Если все документы загружены: + ↓ (Запустить формирование заявления) +``` + +### 4. Формирование заявления +``` +(После всех документов) + ↓ +Собрать данные из: + - wizard_plan + - documents_uploaded (OCR данные) + - CRM контакт + ↓ +AI: Сформировать заявление + ↓ +PostgreSQL: Сохранить claim_data + + claim_ready = true + ↓ +Redis Publish: claim_ready +``` + +--- + +## 📝 План реализации + +### Фаза 1: Frontend (без n8n) +1. ✅ Создать `StepDocumentsNew.tsx` — заглушка с mock данными +2. ✅ Создать `StepWaitingClaim.tsx` — loader +3. ✅ Обновить `ClaimForm.tsx` — новый флоу шагов +4. ✅ Обновить `StepDraftSelection.tsx` — новые статусы + +### Фаза 2: Backend +1. ✅ Эндпоинт `POST /api/v1/documents/upload` +2. ✅ SSE events: `documents_list_ready`, `document_ocr_completed`, `claim_ready` +3. ✅ Эндпоинт `GET /api/v1/claims/drafts/{claim_id}/status` + +### Фаза 3: n8n +1. ✅ Воркфлоу: Генерация списка документов +2. ✅ Воркфлоу: OCR документа +3. ✅ Воркфлоу: Формирование заявления + +### Фаза 4: Интеграция и тестирование +1. ✅ Полный цикл с реальными данными +2. ✅ Обработка ошибок +3. ✅ Legacy черновики + +--- + +## 🎯 Ожидаемый результат + +| Метрика | Было | Стало | +|---------|------|-------| +| Время до первого действия | ~2 мин | ~10 сек | +| Количество вопросов | 10-15 | 0-3 (только уточняющие) | +| Конверсия | ? | ↑ (меньше отвала) | + + diff --git a/ticket_form/docs/SESSION_LOG_2025-11-28_documents_dedup.md b/ticket_form/docs/SESSION_LOG_2025-11-28_documents_dedup.md new file mode 100644 index 00000000..210470b9 --- /dev/null +++ b/ticket_form/docs/SESSION_LOG_2025-11-28_documents_dedup.md @@ -0,0 +1,96 @@ +# Лог сессии 28.11.2025 — Дедупликация документов и исправление field_label + +## Проблемы, которые были решены + +### 1. Неправильный `field_label` ("group-2" вместо "Переписка") + +**Причина:** В коде `N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js` использовался индекс `grp` (позиция в `documents_required`) для доступа к массиву `uploads_field_labels`, но этот массив содержит элементы с индексами от 0 (текущий запрос). + +**Исправление:** Изменён доступ к массивам на индекс `0`: +```javascript +// Было: +const field_label = uploads_field_labels[grp] || ... + +// Стало: +const field_label = uploads_field_labels[0] || uploads_field_names[0] || uploads_descriptions[0] || `group-${grp}`; +``` + +**Файл:** `ticket_form/docs/N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js` + +--- + +### 2. Дублирование записей в `documents_meta` + +**Причина:** SQL использовал простую конкатенацию `||` для объединения новых и существующих `documents_meta`, что приводило к накоплению дубликатов (было 28 записей вместо 2). + +**Исправление:** Создан новый SQL с дедупликацией — новые записи заменяют старые с тем же `field_name`: +```sql +SELECT DISTINCT ON (doc->>'field_name') doc +FROM ( + SELECT ... AS doc, 1 AS priority -- новые (приоритет) + UNION ALL + SELECT ... AS doc, 2 AS priority -- существующие +) all_docs +ORDER BY doc->>'field_name', priority, (doc->>'uploaded_at') DESC NULLS LAST +``` + +**Файл:** `ticket_form/docs/SQL_CLAIMSAVE_FIXED_NEW_FLOW_DEDUP.sql` + +--- + +### 3. Ошибка `ON CONFLICT` для `document_texts` + +**Причина:** Уникальный индекс на `file_hash` был частичным (`WHERE file_hash IS NOT NULL`), что не позволяло использовать `ON CONFLICT (file_hash)`. + +**Исправление:** Создан полный уникальный индекс: +```sql +DROP INDEX IF EXISTS idx_document_texts_hash_unique; +CREATE UNIQUE INDEX idx_document_texts_hash_unique ON document_texts(file_hash); +``` + +--- + +## Созданные/изменённые файлы + +| Файл | Описание | +|------|----------| +| `SQL_CLAIMSAVE_FIXED_NEW_FLOW_DEDUP.sql` | SQL с дедупликацией `documents_meta` | +| `SQL_CLEANUP_DOCUMENTS_META_DUPLICATES.sql` | SQL для очистки существующих дубликатов | +| `N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js` | Исправлен доступ к `uploads_field_labels[0]` | + +--- + +## SQL-запросы для n8n + +### Проверка дубликата по хешу +```sql +SELECT + EXISTS (SELECT 1 FROM document_texts WHERE file_hash = '{{ $json.file_hash }}') AS found, + (SELECT id FROM document_texts WHERE file_hash = '{{ $json.file_hash }}' LIMIT 1) AS existing_id; +``` + +### Вставка с дедупликацией +```sql +INSERT INTO document_texts +(file_id, file_url, path, title, filename_for_upload, "text", description, file_hash) +VALUES (...) +ON CONFLICT (file_hash) DO NOTHING +RETURNING id, file_id, title, file_hash; +``` + +--- + +## Изменения в БД + +1. Создан уникальный индекс `idx_document_texts_hash_unique` на `document_texts(file_hash)` +2. Очищены дубликаты в `documents_meta` для заявки `ef853bac-f54b-46aa-adf8-f0c9c0cd76bc` (было 28 → стало 2) +3. Исправлен `field_label` для `uploads[2][0]` на "Переписка" + +--- + +## Рекомендации + +1. **Обновить SQL в n8n** ноде `claimsave` на версию из `SQL_CLAIMSAVE_FIXED_NEW_FLOW_DEDUP.sql` +2. **Обновить код** в ноде `editfiletobd1` на версию из `N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js` +3. **Добавить проверку хеша** перед вставкой в `document_texts` для информирования о дубликатах + diff --git a/ticket_form/docs/SQL_DOCUMENTS_META_STRUCTURE.md b/ticket_form/docs/SQL_DOCUMENTS_META_STRUCTURE.md new file mode 100644 index 00000000..902088bd --- /dev/null +++ b/ticket_form/docs/SQL_DOCUMENTS_META_STRUCTURE.md @@ -0,0 +1,81 @@ +# Структура documents_meta в SQL запросах + +## Текущая структура после OCR объединения + +После обработки файлов OCR возвращает объединённые документы со следующей структурой: + +```json +{ + "documents_meta": [ + { + "field_name": "uploads[0][0]", + "field_label": "Договор или заказ", + "file_id": "clientright/0/1764167196926.pdf", + "file_name": "1764167196926.pdf", + "original_file_name": "1764167196926.pdf", + "uploaded_at": "2025-11-26T14:44:51.430Z", + "files_count": 2, // ✅ Новое поле: сколько файлов было объединено + "pages": 4 // ✅ Новое поле: сколько страниц в объединённом PDF + } + ] +} +``` + +## Как SQL обрабатывает эту структуру + +### 1. Сохранение в `clpr_claim_documents` + +SQL использует `jsonb_to_recordset` для извлечения только нужных полей: + +```sql +CROSS JOIN LATERAL jsonb_to_recordset( + COALESCE(partial.p->'documents_meta', '[]'::jsonb) +) AS doc( + field_name text, + file_id text, + file_name text, + original_file_name text, + uploaded_at text +) +``` + +**Важно:** `field_label`, `files_count`, `pages` не извлекаются, но это нормально - они не нужны в таблице `clpr_claim_documents`. + +### 2. Сохранение в `payload->'documents_meta'` + +Полный JSON сохраняется в `payload` через `jsonb_build_object`: + +```sql +jsonb_build_object( + 'claim_id', partial.claim_id_str, + 'documents_meta', COALESCE(partial.p->'documents_meta', '[]'::jsonb), + ... +) +``` + +**Результат:** Все поля (`field_label`, `files_count`, `pages`) сохраняются в `payload->'documents_meta'` в полном объёме. + +## Проверка сохранения + +После выполнения SQL запроса можно проверить: + +```sql +SELECT + payload->'documents_meta'->0->>'field_label' AS field_label, + payload->'documents_meta'->0->>'files_count' AS files_count, + payload->'documents_meta'->0->>'pages' AS pages +FROM clpr_claims +WHERE payload->>'claim_id' = 'bddb6815-8e17-4d54-a721-5e94382942c7'; +``` + +Должны вернуться: +- `field_label`: "Договор или заказ" +- `files_count`: "2" +- `pages`: "4" + +## Вывод + +✅ **SQL запрос работает правильно** - дополнительные поля сохраняются в `payload->'documents_meta'` и доступны для использования в дальнейших операциях. + +❌ **Не нужно менять SQL** - текущая структура достаточна для работы. + diff --git a/ticket_form/docs/SQL_DOCUMENT_ID_RETURN.md b/ticket_form/docs/SQL_DOCUMENT_ID_RETURN.md new file mode 100644 index 00000000..5a0637a1 --- /dev/null +++ b/ticket_form/docs/SQL_DOCUMENT_ID_RETURN.md @@ -0,0 +1,104 @@ +# Возврат claim_document_id при сохранении документов + +## Когда возникает claim_document_id? + +`claim_document_id` (поле `id` в таблице `clpr_claim_documents`) возникает в момент INSERT или UPDATE записи в таблицу `clpr_claim_documents`. + +## Где это происходит? + +### 1. При загрузке документа (SQL_CLAIMSAVE_FIXED_NEW_FLOW.sql) + +В CTE `docs_upsert` происходит INSERT/UPDATE: + +```sql +docs_upsert AS ( + INSERT INTO clpr_claim_documents ( + claim_id, + field_name, + file_id, + uploaded_at, + file_name, + original_file_name + ) + SELECT + partial.claim_id_str AS claim_id, + doc.field_name, + doc.file_id, + COALESCE((doc.uploaded_at)::timestamptz, now()), + doc.file_name, + doc.original_file_name + FROM partial + CROSS JOIN LATERAL jsonb_to_recordset( + COALESCE(partial.p->'documents_meta', '[]'::jsonb) + ) AS doc(...) + WHERE partial.p->'documents_meta' IS NOT NULL + AND jsonb_array_length(partial.p->'documents_meta') > 0 + ON CONFLICT (claim_id, field_name) DO UPDATE SET + file_id = EXCLUDED.file_id, + uploaded_at = EXCLUDED.uploaded_at, + file_name = EXCLUDED.file_name, + original_file_name = EXCLUDED.original_file_name + RETURNING id, claim_id, field_name, file_id, file_name, original_file_name -- ✅ Возвращаем id +) +``` + +### 2. В финальном SELECT + +```sql +SELECT + (SELECT jsonb_build_object(...) FROM claim_upsert cu) AS claim, + + (SELECT jsonb_agg(jsonb_build_object( + 'id', id, -- ✅ Это и есть claim_document_id + 'field_name', field_name, + 'file_id', file_id, + 'file_name', file_name, + 'original_file_name', original_file_name + )) FROM docs_upsert) AS documents; +``` + +## Структура ответа + +После выполнения SQL запроса возвращается: + +```json +{ + "claim": { + "claim_id": "...", + "status_code": "...", + ... + }, + "documents": [ + { + "id": "16fa625e-1da3-4097-895a-75a8904c702a", // ← Это claim_document_id + "field_name": "uploads[1][0]", + "file_id": "...", + "file_name": "...", + "original_file_name": "..." + }, + ... + ] +} +``` + +## Когда это нужно? + +`claim_document_id` нужен для: +1. **Связи с другими таблицами** - если нужно связать документ с другими сущностями +2. **Обновления документа** - для UPDATE конкретной записи по ID +3. **Удаления документа** - для DELETE конкретной записи по ID +4. **Отслеживания** - для логирования и аудита + +## Важно + +- `claim_document_id` генерируется автоматически PostgreSQL при INSERT (если `id` имеет тип UUID с DEFAULT) +- При UPDATE (ON CONFLICT) возвращается существующий `id` +- `RETURNING id` в SQL запросе обязательно должен быть, чтобы получить `id` обратно + +## Проверка + +Чтобы убедиться, что `claim_document_id` возвращается, проверьте: +1. SQL запрос содержит `RETURNING id` в INSERT/UPDATE для `clpr_claim_documents` +2. Финальный SELECT включает `'id', id` из CTE с документами +3. n8n получает массив `documents` с полем `id` в каждом элементе + diff --git a/ticket_form/fix_claim_documents_field_names.py b/ticket_form/fix_claim_documents_field_names.py new file mode 100644 index 00000000..dec1446a --- /dev/null +++ b/ticket_form/fix_claim_documents_field_names.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +""" +Исправление field_name в таблице clpr_claim_documents +Пересоздаёт записи с правильными field_name на основе documents_uploaded и documents_required +""" +import asyncio +import asyncpg +import json +from datetime import datetime + +POSTGRES_HOST = "147.45.189.234" +POSTGRES_PORT = 5432 +POSTGRES_DB = "default_db" +POSTGRES_USER = "gen_user" +POSTGRES_PASSWORD = "2~~9_^kVsU?2\\S" + +CLAIM_ID = "bddb6815-8e17-4d54-a721-5e94382942c7" + +async def fix_field_names(): + conn = await asyncpg.connect( + host=POSTGRES_HOST, + port=POSTGRES_PORT, + database=POSTGRES_DB, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD + ) + + try: + # Получаем данные черновика + row = await conn.fetchrow(""" + SELECT id, payload + FROM clpr_claims + WHERE id::text = $1 OR payload->>'claim_id' = $1 + ORDER BY updated_at DESC + LIMIT 1 + """, CLAIM_ID) + + if not row: + print(f"❌ Черновик {CLAIM_ID} не найден!") + return + + claim_uuid = row['id'] + payload = row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']) + + documents_required = payload.get('documents_required', []) + documents_uploaded = payload.get('documents_uploaded', []) + + print(f"📋 documents_required: {len(documents_required)} документов") + print(f"📋 documents_uploaded: {len(documents_uploaded)} документов") + + # Создаём мапу: doc_id -> group_index + doc_id_to_index = {} + for idx, doc_req in enumerate(documents_required): + doc_id = doc_req.get('id') + if doc_id: + doc_id_to_index[doc_id] = idx + + print(f"\n📋 Маппинг документов:") + for doc_id, idx in doc_id_to_index.items(): + print(f" {doc_id} -> group_index {idx}") + + # Удаляем старые записи + deleted_count = await conn.execute(""" + DELETE FROM clpr_claim_documents + WHERE claim_id = $1 + """, str(claim_uuid)) + + print(f"\n🗑️ Удалено старых записей: {deleted_count.split()[-1]}") + + # Вставляем новые записи с правильными field_name + inserted_count = 0 + for doc_up in documents_uploaded: + doc_type = doc_up.get('type') or doc_up.get('id') + file_id = doc_up.get('file_id') + + if not doc_type or not file_id: + print(f" ⚠️ Пропущен документ без type/id или file_id: {doc_up}") + continue + + group_index = doc_id_to_index.get(doc_type) + if group_index is None: + print(f" ⚠️ Не найден group_index для типа {doc_type}") + continue + + field_name = f"uploads[{group_index}][0]" + + # Парсим uploaded_at + uploaded_at_str = doc_up.get('uploaded_at') + uploaded_at = None + if uploaded_at_str: + try: + # Пробуем разные форматы даты + if isinstance(uploaded_at_str, str): + if 'T' in uploaded_at_str: + uploaded_at = datetime.fromisoformat(uploaded_at_str.replace('Z', '+00:00')) + else: + uploaded_at = datetime.fromisoformat(uploaded_at_str) + elif isinstance(uploaded_at_str, datetime): + uploaded_at = uploaded_at_str + except Exception as e: + print(f" ⚠️ Ошибка парсинга даты {uploaded_at_str}: {e}") + uploaded_at = None + + await conn.execute(""" + INSERT INTO clpr_claim_documents ( + claim_id, + field_name, + file_id, + file_name, + original_file_name, + uploaded_at + ) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (claim_id, field_name) DO UPDATE SET + file_id = EXCLUDED.file_id, + file_name = EXCLUDED.file_name, + original_file_name = EXCLUDED.original_file_name, + uploaded_at = EXCLUDED.uploaded_at + """, + str(claim_uuid), + field_name, + file_id, + doc_up.get('file_name', ''), + doc_up.get('original_file_name', ''), + uploaded_at + ) + + inserted_count += 1 + print(f" ✅ Вставлен: {field_name} -> {doc_type} ({file_id[:50]}...)") + + print(f"\n✅ Вставлено новых записей: {inserted_count}") + + # Проверяем результат + result_rows = await conn.fetch(""" + SELECT + field_name, + file_id, + file_name, + original_file_name + FROM clpr_claim_documents + WHERE claim_id = $1 + ORDER BY field_name + """, str(claim_uuid)) + + print(f"\n📊 Результат в таблице ({len(result_rows)} записей):") + for row in result_rows: + print(f" {row['field_name']}: {row['file_name']} ({row['file_id'][:50]}...)") + + finally: + await conn.close() + +if __name__ == "__main__": + asyncio.run(fix_field_names()) + diff --git a/ticket_form/fix_documents_meta_duplicates.py b/ticket_form/fix_documents_meta_duplicates.py new file mode 100644 index 00000000..40550718 --- /dev/null +++ b/ticket_form/fix_documents_meta_duplicates.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +""" +Очистка дубликатов в documents_meta +""" +import asyncio +import asyncpg +import json + +POSTGRES_HOST = "147.45.189.234" +POSTGRES_PORT = 5432 +POSTGRES_DB = "default_db" +POSTGRES_USER = "gen_user" +POSTGRES_PASSWORD = "2~~9_^kVsU?2\\S" + +CLAIM_ID = "bddb6815-8e17-4d54-a721-5e94382942c7" + +async def fix_duplicates(): + conn = await asyncpg.connect( + host=POSTGRES_HOST, + port=POSTGRES_PORT, + database=POSTGRES_DB, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD + ) + + try: + row = await conn.fetchrow(""" + SELECT id, payload + FROM clpr_claims + WHERE id::text = $1 OR payload->>'claim_id' = $1 + ORDER BY updated_at DESC + LIMIT 1 + """, CLAIM_ID) + + if not row: + print(f"❌ Черновик {CLAIM_ID} не найден!") + return + + payload = row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']) + documents_meta = payload.get('documents_meta', []) + + print(f"📋 Было документов в documents_meta: {len(documents_meta)}") + + # Убираем дубликаты по file_id (оставляем первый) + seen_file_ids = set() + unique_documents_meta = [] + + for doc in documents_meta: + file_id = doc.get('file_id') + if file_id and file_id not in seen_file_ids: + seen_file_ids.add(file_id) + unique_documents_meta.append(doc) + elif file_id: + print(f" ⚠️ Пропущен дубликат: {file_id[:80]}...") + + print(f"📋 Стало документов в documents_meta: {len(unique_documents_meta)}") + + # Обновляем payload + payload['documents_meta'] = unique_documents_meta + + await conn.execute(""" + UPDATE clpr_claims + SET + payload = $1::jsonb, + updated_at = now() + WHERE id::text = $2 OR payload->>'claim_id' = $2 + """, json.dumps(payload, ensure_ascii=False), CLAIM_ID) + + print(f"\n✅ Дубликаты удалены!") + + # Проверяем результат + row_after = await conn.fetchrow(""" + SELECT jsonb_array_length(payload->'documents_meta') as docs_count + FROM clpr_claims + WHERE id::text = $1 OR payload->>'claim_id' = $1 + ORDER BY updated_at DESC + LIMIT 1 + """, CLAIM_ID) + + print(f"📊 Результат: {row_after['docs_count']} документов в documents_meta") + + finally: + await conn.close() + +if __name__ == "__main__": + asyncio.run(fix_duplicates()) + diff --git a/ticket_form/fix_draft_bddb6815.py b/ticket_form/fix_draft_bddb6815.py new file mode 100644 index 00000000..6fe66ea2 --- /dev/null +++ b/ticket_form/fix_draft_bddb6815.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +Скрипт для исправления черновика bddb6815-8e17-4d54-a721-5e94382942c7 +Добавляет documents_required и исправляет статус +""" +import asyncio +import asyncpg +import json +from pathlib import Path + +# Параметры подключения к БД (из config.py) +POSTGRES_HOST = "147.45.189.234" +POSTGRES_PORT = 5432 +POSTGRES_DB = "default_db" +POSTGRES_USER = "gen_user" +POSTGRES_PASSWORD = "2~~9_^kVsU?2\\S" + +CLAIM_ID = "bddb6815-8e17-4d54-a721-5e94382942c7" + +DOCUMENTS_REQUIRED = [ + { + "id": "contract", + "name": "Договор или заказ", + "hints": "Фото или скан подписанного договора или квитанции", + "accept": ["pdf", "jpg", "png"], + "priority": 1, + "required": True + }, + { + "id": "payment", + "name": "Чек или подтверждение оплаты", + "hints": "Копия кассового чека, онлайн-платежа или квитанции", + "accept": ["pdf", "jpg", "png"], + "priority": 1, + "required": True + }, + { + "id": "correspondence", + "name": "Переписка", + "hints": "Скриншоты сообщений, писем, жалоб", + "accept": ["pdf", "jpg", "png"], + "priority": 2, + "required": False + }, + { + "id": "evidence_photo", + "name": "Фото доказательства", + "hints": "Фото дефектов товара, видео процесса ремонта или передачи", + "accept": ["jpg", "png", "pdf"], + "priority": 2, + "required": False + } +] + + +async def fix_draft(): + """Исправляет черновик: добавляет documents_required и обновляет статус""" + conn = await asyncpg.connect( + host=POSTGRES_HOST, + port=POSTGRES_PORT, + database=POSTGRES_DB, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD + ) + + try: + # Получаем текущее состояние черновика + row = await conn.fetchrow(""" + SELECT id, status_code, payload + FROM clpr_claims + WHERE id::text = $1 OR payload->>'claim_id' = $1 + LIMIT 1 + """, CLAIM_ID) + + if not row: + print(f"❌ Черновик {CLAIM_ID} не найден!") + return + + payload = row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']) + current_status = row['status_code'] + documents_uploaded = payload.get('documents_uploaded', []) + uploaded_count = len(documents_uploaded) if isinstance(documents_uploaded, list) else 0 + + print(f"📋 Текущее состояние черновика:") + print(f" - status_code: {current_status}") + print(f" - documents_required: {len(payload.get('documents_required', []))} шт.") + print(f" - documents_uploaded: {uploaded_count} шт.") + + # Определяем новый статус + if uploaded_count > 0: + if uploaded_count >= len(DOCUMENTS_REQUIRED): + new_status = 'draft_docs_complete' + else: + new_status = 'draft_docs_progress' + else: + new_status = 'draft_new' + + # Обновляем payload + payload['documents_required'] = DOCUMENTS_REQUIRED + + # Обновляем черновик + await conn.execute(""" + UPDATE clpr_claims + SET + status_code = $1, + payload = $2::jsonb, + updated_at = now() + WHERE id::text = $3 OR payload->>'claim_id' = $3 + """, new_status, json.dumps(payload, ensure_ascii=False), CLAIM_ID) + + print(f"\n✅ Черновик исправлен!") + print(f" - Новый status_code: {new_status}") + print(f" - documents_required: {len(DOCUMENTS_REQUIRED)} документов добавлено") + + # Проверяем результат + row_after = await conn.fetchrow(""" + SELECT + id::text, + status_code, + jsonb_array_length(COALESCE(payload->'documents_required', '[]'::jsonb)) as docs_count + FROM clpr_claims + WHERE id::text = $1 OR payload->>'claim_id' = $1 + LIMIT 1 + """, CLAIM_ID) + + print(f"\n📊 Результат:") + print(f" - status_code: {row_after['status_code']}") + print(f" - documents_required count: {row_after['docs_count']}") + + finally: + await conn.close() + + +if __name__ == "__main__": + asyncio.run(fix_draft()) + diff --git a/ticket_form/fix_draft_bddb6815_with_contract.py b/ticket_form/fix_draft_bddb6815_with_contract.py new file mode 100644 index 00000000..d17533af --- /dev/null +++ b/ticket_form/fix_draft_bddb6815_with_contract.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +""" +Скрипт для исправления черновика bddb6815-8e17-4d54-a721-5e94382942c7 +Добавляет documents_required и обновляет статус с учётом уже загруженного договора +""" +import asyncio +import asyncpg +import json +from datetime import datetime + +# Параметры подключения к БД +POSTGRES_HOST = "147.45.189.234" +POSTGRES_PORT = 5432 +POSTGRES_DB = "default_db" +POSTGRES_USER = "gen_user" +POSTGRES_PASSWORD = "2~~9_^kVsU?2\\S" + +CLAIM_ID = "bddb6815-8e17-4d54-a721-5e94382942c7" + +DOCUMENTS_REQUIRED = [ + { + "id": "contract", + "name": "Договор или заказ", + "hints": "Фото или скан подписанного договора или квитанции", + "accept": ["pdf", "jpg", "png"], + "priority": 1, + "required": True + }, + { + "id": "payment", + "name": "Чек или подтверждение оплаты", + "hints": "Копия кассового чека, онлайн-платежа или квитанции", + "accept": ["pdf", "jpg", "png"], + "priority": 1, + "required": True + }, + { + "id": "correspondence", + "name": "Переписка", + "hints": "Скриншоты сообщений, писем, жалоб", + "accept": ["pdf", "jpg", "png"], + "priority": 2, + "required": False + }, + { + "id": "evidence_photo", + "name": "Фото доказательства", + "hints": "Фото дефектов товара, видео процесса ремонта или передачи", + "accept": ["jpg", "png", "pdf"], + "priority": 2, + "required": False + } +] + + +async def fix_draft(): + """Исправляет черновик: добавляет documents_required и обновляет статус""" + conn = await asyncpg.connect( + host=POSTGRES_HOST, + port=POSTGRES_PORT, + database=POSTGRES_DB, + user=POSTGRES_USER, + password=POSTGRES_PASSWORD + ) + + try: + # Получаем текущее состояние черновика + row = await conn.fetchrow(""" + SELECT id, status_code, payload + FROM clpr_claims + WHERE id::text = $1 OR payload->>'claim_id' = $1 + ORDER BY updated_at DESC + LIMIT 1 + """, CLAIM_ID) + + if not row: + print(f"❌ Черновик {CLAIM_ID} не найден!") + return + + payload = row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']) + current_status = row['status_code'] + + print(f"📋 Текущее состояние черновика:") + print(f" - status_code: {current_status}") + print(f" - documents_required: {len(payload.get('documents_required', []))} шт.") + print(f" - documents_uploaded: {len(payload.get('documents_uploaded', []))} шт.") + print(f" - documents_meta: {len(payload.get('documents_meta', []))} шт.") + + # Проверяем documents_meta на наличие загруженных документов + documents_meta = payload.get('documents_meta', []) + existing_documents_uploaded = payload.get('documents_uploaded', []) + + # Функция для определения типа документа (сначала по field_label, потом по field_name) + def get_document_type(field_label: str, field_name: str) -> str: + field_label_lower = field_label.lower() + # ✅ СНАЧАЛА проверяем field_label (более точный способ) + if 'договор' in field_label_lower or 'заказ' in field_label_lower: + return 'contract' + elif 'чек' in field_label_lower or 'оплат' in field_label_lower: + return 'payment' + elif 'переписк' in field_label_lower: + return 'correspondence' + elif 'доказательств' in field_label_lower or 'фото' in field_label_lower: + return 'evidence_photo' + # ✅ ПОТОМ проверяем field_name (fallback) + elif 'uploads[0]' in field_name: + return 'contract' + elif 'uploads[1]' in field_name: + return 'payment' + elif 'uploads[2]' in field_name: + return 'correspondence' + elif 'uploads[3]' in field_name: + return 'evidence_photo' + else: + return 'unknown' + + # ✅ Объединяем существующие documents_uploaded с documents_meta + # Создаём мапу file_id -> doc_meta для быстрого поиска + meta_by_file_id = {} + if documents_meta: + print(f"\n🔍 Найдено {len(documents_meta)} документов в documents_meta") + for doc_meta in documents_meta: + file_id = doc_meta.get('file_id', '') + if file_id: + meta_by_file_id[file_id] = doc_meta + + # ✅ Пересоздаём documents_uploaded: объединяем существующие с данными из documents_meta + documents_uploaded = [] + seen_file_ids = set() + + # Сначала обрабатываем documents_meta (приоритет) + for doc_meta in documents_meta: + file_id = doc_meta.get('file_id', '') + if not file_id or file_id in seen_file_ids: + continue + + field_label = doc_meta.get('field_label', '') + field_name = doc_meta.get('field_name', '') + doc_type = get_document_type(field_label, field_name) + + if doc_type != 'unknown': + seen_file_ids.add(file_id) + documents_uploaded.append({ + "id": doc_type, + "type": doc_type, + "file_id": file_id, + "file_name": doc_meta.get('file_name', ''), + "original_file_name": doc_meta.get('original_file_name', ''), + "uploaded_at": doc_meta.get('uploaded_at', datetime.utcnow().isoformat()), + "ocr_status": "completed", + "files_count": doc_meta.get('files_count', 1), + "pages": doc_meta.get('pages', None) + }) + print(f" ✅ Из documents_meta: {doc_type} ({field_label}) - {doc_meta.get('original_file_name', 'N/A')}") + + # Затем добавляем существующие documents_uploaded, которых нет в documents_meta + for existing_doc in existing_documents_uploaded: + file_id = existing_doc.get('file_id', '') + if not file_id or file_id in seen_file_ids: + continue + + # Если есть в documents_meta - пропускаем (уже обработали) + if file_id in meta_by_file_id: + continue + + # Используем существующий тип или пытаемся определить по file_name + doc_type = existing_doc.get('type') or existing_doc.get('id') or 'unknown' + + # Если тип неправильный (например, contract вместо payment), пытаемся определить по file_name + if doc_type == 'contract' and 'chek' in file_id.lower(): + doc_type = 'payment' + elif doc_type == 'contract' and 'dogovor' in file_id.lower(): + doc_type = 'contract' + + seen_file_ids.add(file_id) + documents_uploaded.append({ + "id": doc_type, + "type": doc_type, + "file_id": file_id, + "file_name": existing_doc.get('file_name', ''), + "original_file_name": existing_doc.get('original_file_name', ''), + "uploaded_at": existing_doc.get('uploaded_at', datetime.utcnow().isoformat()), + "ocr_status": existing_doc.get('ocr_status', 'completed'), + "files_count": existing_doc.get('files_count', 1), + "pages": existing_doc.get('pages', None) + }) + print(f" ✅ Из существующих: {doc_type} - {existing_doc.get('original_file_name', 'N/A')}") + + # Определяем current_doc_index (индекс следующего документа для загрузки) + # Убираем дубликаты по типу документа + uploaded_types = list(set([doc.get('id') or doc.get('type') for doc in documents_uploaded])) + current_doc_index = 0 + + # Находим первый незагруженный документ + for idx, doc_req in enumerate(DOCUMENTS_REQUIRED): + if doc_req['id'] not in uploaded_types: + current_doc_index = idx + break + else: + # Все документы загружены + current_doc_index = len(DOCUMENTS_REQUIRED) + + # Определяем новый статус (учитываем уникальные типы документов) + uploaded_unique_types = len(uploaded_types) + if uploaded_unique_types >= len(DOCUMENTS_REQUIRED): + new_status = 'draft_docs_complete' + elif uploaded_unique_types > 0: + new_status = 'draft_docs_progress' + else: + new_status = 'draft_new' + + # Обновляем payload + payload['documents_required'] = DOCUMENTS_REQUIRED + payload['documents_uploaded'] = documents_uploaded + payload['current_doc_index'] = current_doc_index + + print(f"\n📝 Обновление черновика:") + print(f" - documents_required: {len(DOCUMENTS_REQUIRED)} документов") + print(f" - documents_uploaded: {len(documents_uploaded)} документов") + print(f" - current_doc_index: {current_doc_index} (следующий документ: {DOCUMENTS_REQUIRED[current_doc_index]['name'] if current_doc_index < len(DOCUMENTS_REQUIRED) else 'все загружены'})") + print(f" - status_code: {current_status} → {new_status}") + + # Обновляем черновик + await conn.execute(""" + UPDATE clpr_claims + SET + status_code = $1, + payload = $2::jsonb, + updated_at = now() + WHERE id::text = $3 OR payload->>'claim_id' = $3 + """, new_status, json.dumps(payload, ensure_ascii=False), CLAIM_ID) + + print(f"\n✅ Черновик исправлен!") + + # Проверяем результат + row_after = await conn.fetchrow(""" + SELECT + id::text, + status_code, + jsonb_array_length(COALESCE(payload->'documents_required', '[]'::jsonb)) as docs_required_count, + jsonb_array_length(COALESCE(payload->'documents_uploaded', '[]'::jsonb)) as docs_uploaded_count, + (payload->>'current_doc_index')::int as current_doc_index + FROM clpr_claims + WHERE id::text = $1 OR payload->>'claim_id' = $1 + ORDER BY updated_at DESC + LIMIT 1 + """, CLAIM_ID) + + print(f"\n📊 Результат:") + print(f" - status_code: {row_after['status_code']}") + print(f" - documents_required count: {row_after['docs_required_count']}") + print(f" - documents_uploaded count: {row_after['docs_uploaded_count']}") + print(f" - current_doc_index: {row_after['current_doc_index']}") + + finally: + await conn.close() + + +if __name__ == "__main__": + asyncio.run(fix_draft()) + diff --git a/ticket_form/frontend/src/components/form/StepDocumentsNew.tsx b/ticket_form/frontend/src/components/form/StepDocumentsNew.tsx index f17f94ba..00f429ff 100644 --- a/ticket_form/frontend/src/components/form/StepDocumentsNew.tsx +++ b/ticket_form/frontend/src/components/form/StepDocumentsNew.tsx @@ -360,3 +360,366 @@ export default function StepDocumentsNew({ ); } + + * StepDocumentsNew.tsx + * + * Поэкранная загрузка документов. + * Один документ на экран с возможностью пропуска. + * + * @version 1.0 + * @date 2025-11-26 + */ + +import { useState, useCallback, useEffect, useRef } from 'react'; +import { + Button, + Card, + Upload, + Progress, + Alert, + Typography, + Space, + Spin, + message, + Result +} from 'antd'; +import { + UploadOutlined, + FileTextOutlined, + ExclamationCircleOutlined, + CheckCircleOutlined, + LoadingOutlined, + InboxOutlined +} from '@ant-design/icons'; +import type { UploadFile, UploadProps } from 'antd/es/upload/interface'; + +const { Title, Text, Paragraph } = Typography; +const { Dragger } = Upload; + +// === Типы === +export interface DocumentConfig { + type: string; // Идентификатор: contract, payment, correspondence + name: string; // Название: "Договор или оферта" + critical: boolean; // Обязательный документ? + hints?: string; // Подсказка: "Скриншот или PDF договора" + accept?: string[]; // Допустимые форматы: ['pdf', 'jpg', 'png'] +} + +interface Props { + formData: any; + updateFormData: (data: any) => void; + documents: DocumentConfig[]; + currentIndex: number; + onDocumentUploaded: (docType: string, fileData: any) => void; + onDocumentSkipped: (docType: string) => void; + onAllDocumentsComplete: () => void; + onPrev: () => void; + addDebugEvent?: (type: string, status: string, message: string, data?: any) => void; +} + +// === Компонент === +export default function StepDocumentsNew({ + formData, + updateFormData, + documents, + currentIndex, + onDocumentUploaded, + onDocumentSkipped, + onAllDocumentsComplete, + onPrev, + addDebugEvent, +}: Props) { + const [fileList, setFileList] = useState([]); + const [uploading, setUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + + // Текущий документ + const currentDoc = documents[currentIndex]; + const isLastDocument = currentIndex === documents.length - 1; + const totalDocs = documents.length; + + // Сбрасываем файлы при смене документа + useEffect(() => { + setFileList([]); + setUploadProgress(0); + }, [currentIndex]); + + // === Handlers === + + const handleUpload = useCallback(async () => { + if (fileList.length === 0) { + message.error('Выберите файл для загрузки'); + return; + } + + const file = fileList[0]; + if (!file.originFileObj) { + message.error('Ошибка: файл не найден'); + return; + } + + setUploading(true); + setUploadProgress(0); + + try { + addDebugEvent?.('documents', 'info', `📤 Загрузка документа: ${currentDoc.name}`, { + document_type: currentDoc.type, + file_name: file.name, + file_size: file.size, + }); + + const formDataToSend = new FormData(); + formDataToSend.append('claim_id', formData.claim_id || ''); + formDataToSend.append('session_id', formData.session_id || ''); + formDataToSend.append('document_type', currentDoc.type); + formDataToSend.append('file', file.originFileObj, file.name); + + // Симуляция прогресса (реальный прогресс будет через XHR) + const progressInterval = setInterval(() => { + setUploadProgress(prev => Math.min(prev + 10, 90)); + }, 200); + + const response = await fetch('/api/v1/documents/upload', { + method: 'POST', + body: formDataToSend, + }); + + clearInterval(progressInterval); + setUploadProgress(100); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Ошибка загрузки: ${response.status} ${errorText}`); + } + + const result = await response.json(); + + addDebugEvent?.('documents', 'success', `✅ Документ загружен: ${currentDoc.name}`, { + document_type: currentDoc.type, + file_id: result.file_id, + }); + + message.success(`${currentDoc.name} загружен!`); + + // Сохраняем в formData + const uploadedDocs = formData.documents_uploaded || []; + uploadedDocs.push({ + type: currentDoc.type, + file_id: result.file_id, + file_name: file.name, + ocr_status: 'processing', + }); + + updateFormData({ + documents_uploaded: uploadedDocs, + current_doc_index: currentIndex + 1, + }); + + // Callback + onDocumentUploaded(currentDoc.type, result); + + // Переходим к следующему или завершаем + if (isLastDocument) { + onAllDocumentsComplete(); + } + + } catch (error) { + console.error('❌ Upload error:', error); + message.error('Ошибка загрузки файла. Попробуйте ещё раз.'); + addDebugEvent?.('documents', 'error', `❌ Ошибка загрузки: ${currentDoc.name}`, { + error: String(error), + }); + } finally { + setUploading(false); + } + }, [fileList, currentDoc, formData, updateFormData, currentIndex, isLastDocument, onDocumentUploaded, onAllDocumentsComplete, addDebugEvent]); + + const handleSkip = useCallback(() => { + if (currentDoc.critical) { + // Показываем предупреждение, но всё равно разрешаем пропустить + message.warning(`⚠️ Документ "${currentDoc.name}" важен для рассмотрения заявки`); + } + + addDebugEvent?.('documents', 'info', `⏭️ Документ пропущен: ${currentDoc.name}`, { + document_type: currentDoc.type, + was_critical: currentDoc.critical, + }); + + // Сохраняем в список пропущенных + const skippedDocs = formData.documents_skipped || []; + if (!skippedDocs.includes(currentDoc.type)) { + skippedDocs.push(currentDoc.type); + } + + updateFormData({ + documents_skipped: skippedDocs, + current_doc_index: currentIndex + 1, + }); + + // Callback + onDocumentSkipped(currentDoc.type); + + // Переходим к следующему или завершаем + if (isLastDocument) { + onAllDocumentsComplete(); + } + }, [currentDoc, formData, updateFormData, currentIndex, isLastDocument, onDocumentSkipped, onAllDocumentsComplete, addDebugEvent]); + + // === Upload Props === + const uploadProps: UploadProps = { + fileList, + onChange: ({ fileList: newFileList }) => setFileList(newFileList.slice(-1)), // Только один файл + beforeUpload: () => false, // Не загружаем автоматически + maxCount: 1, + accept: currentDoc?.accept + ? currentDoc.accept.map(ext => `.${ext}`).join(',') + : '.pdf,.jpg,.jpeg,.png,.heic,.doc,.docx', + disabled: uploading, + }; + + // === Render === + + if (!currentDoc) { + return ( + } + /> + ); + } + + return ( +
+ + {/* === Прогресс === */} +
+
+ + Документ {currentIndex + 1} из {totalDocs} + + + {Math.round((currentIndex / totalDocs) * 100)}% завершено + +
+ +
+ + {/* === Заголовок === */} +
+ + <FileTextOutlined style={{ color: '#595959' }} /> + {currentDoc.name} + {currentDoc.critical && ( + <ExclamationCircleOutlined + style={{ color: '#fa8c16', fontSize: 20 }} + title="Важный документ" + /> + )} + + + {currentDoc.hints && ( + + {currentDoc.hints} + + )} +
+ + {/* === Алерт для критичных документов === */} + {currentDoc.critical && ( + } + style={{ marginBottom: 24 }} + /> + )} + + {/* === Загрузка файла === */} + +

+ {uploading ? ( + + ) : ( + + )} +

+

+ {uploading + ? 'Загружаем документ...' + : 'Перетащите файл сюда или нажмите для выбора' + } +

+

+ Поддерживаются: PDF, JPG, PNG, HEIC, DOC (до 20 МБ) +

+
+ + {/* === Прогресс загрузки === */} + {uploading && ( + + )} + + {/* === Кнопки === */} + + + + + + + + + + + {/* === Уже загруженные документы === */} + {formData.documents_uploaded && formData.documents_uploaded.length > 0 && ( +
+ Загруженные документы: +
    + {formData.documents_uploaded.map((doc: any, idx: number) => ( +
  • + + {documents.find(d => d.type === doc.type)?.name || doc.type} +
  • + ))} +
+
+ )} +
+
+ ); +} + + diff --git a/ticket_form/frontend/src/components/form/StepDraftSelection.tsx b/ticket_form/frontend/src/components/form/StepDraftSelection.tsx index d8187d17..df09a6a1 100644 --- a/ticket_form/frontend/src/components/form/StepDraftSelection.tsx +++ b/ticket_form/frontend/src/components/form/StepDraftSelection.tsx @@ -203,8 +203,12 @@ export default function StepDraftSelection({ // Определяем legacy черновики (без documents_required в payload) const processedDrafts = (data.drafts || []).map((draft: Draft) => { - // Legacy если нет новых полей и есть старый wizard формат - const isLegacy = draft.wizard_plan && !draft.documents_total && draft.status_code === 'draft'; + // Legacy только если: + // 1. Статус 'draft' (старый формат) ИЛИ + // 2. Нет новых статусов (draft_new, draft_docs_progress, draft_docs_complete, draft_claim_ready) + // И есть wizard_plan (старый формат) + const isNewFlowStatus = ['draft_new', 'draft_docs_progress', 'draft_docs_complete', 'draft_claim_ready'].includes(draft.status_code || ''); + const isLegacy = !isNewFlowStatus && draft.wizard_plan && draft.status_code === 'draft'; return { ...draft, is_legacy: isLegacy, @@ -316,19 +320,26 @@ export default function StepDraftSelection({
+ {/* Кнопка создания новой заявки - всегда вверху */} + + {loading ? (
) : drafts.length === 0 ? ( - - + /> ) : ( <> {config.icon}
} title={ - - - {draft.problem_description - ? draft.problem_description.substring(0, 50) + (draft.problem_description.length > 50 ? '...' : '') - : 'Заявка' - } - - {config.label} - +
+ {config.label} +
} description={ + {/* Описание проблемы */} + {draft.problem_description && ( + + {draft.problem_description.length > 60 + ? draft.problem_description.substring(0, 60) + '...' + : draft.problem_description + } + + )} + {/* Время обновления */} @@ -409,7 +436,7 @@ export default function StepDraftSelection({ {/* Legacy предупреждение */} {draft.is_legacy && ( -
- -
- -
+
+ } + /> + ); + } + + if (state.currentStep === 'ready') { + return ( + } + extra={} + /> + ); + } + + return ( +
+ + {/* === Иллюстрация === */} + AI работает + + {/* === Заголовок === */} + {state.message} + + + Наш AI-ассистент обрабатывает ваши документы и формирует заявление. + Это займёт 1-2 минуты. + + + {/* === Прогресс === */} + + + {/* === Шаги обработки === */} + + 0 ? `${state.ocrCompleted}/${state.ocrTotal}` : ''} + icon={state.currentStep === 'ocr' ? : } + /> + : } + /> + : } + /> + } + /> + + + {/* === Таймер === */} + + + + Время ожидания: {formatTime(elapsedTime)} + + + + {/* === Подсказка === */} + + Не закрывайте эту страницу. Обработка происходит на сервере. + + +
+ ); +} + + diff --git a/ticket_form/frontend/src/components/form/StepWizardPlan.tsx b/ticket_form/frontend/src/components/form/StepWizardPlan.tsx index 67eca32e..75d64ac3 100644 --- a/ticket_form/frontend/src/components/form/StepWizardPlan.tsx +++ b/ticket_form/frontend/src/components/form/StepWizardPlan.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Button, Card, Checkbox, Form, Input, Radio, Result, Select, Skeleton, Space, Tag, Typography, Upload, message, Progress } from 'antd'; -import { LoadingOutlined, PlusOutlined, ThunderboltOutlined } from '@ant-design/icons'; +import { LoadingOutlined, PlusOutlined, ThunderboltOutlined, InboxOutlined, FileTextOutlined } from '@ant-design/icons'; import AiWorkingIllustration from '../../assets/ai-working.svg'; import type { UploadFile } from 'antd/es/upload/interface'; @@ -249,15 +249,15 @@ export default function StepWizardPlan({ : ''; return [ - ...blocks, - { - id: generateBlockId(docId), - fieldName: docId, + ...blocks, + { + id: generateBlockId(docId), + fieldName: docId, description: autoDescription, category: category, - docLabel: docLabel, - files: [], - }, + docLabel: docLabel, + files: [], + }, ]; }); }; @@ -341,7 +341,7 @@ export default function StepWizardPlan({ // Автоматически создаём блоки для ВСЕХ документов из плана при загрузке // Используем ref чтобы отслеживать какие блоки уже созданы const createdDocBlocksRef = useRef>(new Set()); - + useEffect(() => { if (!plan || !documents || documents.length === 0) return; @@ -459,6 +459,43 @@ export default function StepWizardPlan({ payload_preview: JSON.stringify(payload).substring(0, 200), }); + // ✅ НОВЫЙ ФЛОУ: Обработка списка документов + if (eventType === 'documents_list_ready') { + const documentsRequired = payload.documents_required || []; + + debugLoggerRef.current?.('wizard', 'success', '📋 Получен список документов!', { + session_id: sessionId, + documents_count: documentsRequired.length, + documents: documentsRequired.map((d: any) => d.name), + }); + + console.log('📋 documents_list_ready:', { + claim_id: payload.claim_id, + documents_required: documentsRequired, + }); + + // Сохраняем в formData для нового флоу + updateFormData({ + documents_required: documentsRequired, + claim_id: payload.claim_id, + wizardPlanStatus: 'documents_ready', // Новый статус + }); + + setIsWaiting(false); + setConnectionError(null); + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + + // Пока показываем alert для теста, потом переход к StepDocumentsNew + message.success(`Получен список документов: ${documentsRequired.length} шт.`); + + // TODO: onNext() для перехода к StepDocumentsNew + return; + } + const wizardPayload = extractWizardPayload(payload); const hasWizardPlan = Boolean(wizardPayload); @@ -565,14 +602,14 @@ export default function StepWizardPlan({ // Для обязательных документов описание не требуется // Для предопределённых документов описание не требуется if (!doc.required && !isPredefinedDoc) { - const missingDescription = blocks.some( - (block) => block.files.length > 0 && !block.description?.trim() - ); - if (missingDescription) { + const missingDescription = blocks.some( + (block) => block.files.length > 0 && !block.description?.trim() + ); + if (missingDescription) { return `Заполните описание для документа "${doc.name}"`; - } } } + } const customMissingDescription = customFileBlocks.some( (block) => block.files.length > 0 && !block.description?.trim() @@ -700,8 +737,8 @@ export default function StepWizardPlan({ } if (cat.includes('payment') || cat.includes('cheque') || cat.includes('receipt') || cat.includes('подтверждение') || cat === 'payment_proof') { - return 'upload_payment'; - } + return 'upload_payment'; + } if (cat.includes('correspondence') || cat.includes('chat') || cat.includes('переписка')) { return 'upload_correspondence'; } @@ -869,7 +906,7 @@ export default function StepWizardPlan({ eventSourceRef.current = null; // Переходим к следующему шагу (форма подтверждения) - onNext(); + onNext(); } else if (data.event_type === 'claim_plan_error' || data.status === 'error') { message.destroy(); message.error(data.message || 'Ошибка получения данных заявления'); @@ -1012,14 +1049,14 @@ export default function StepWizardPlan({ // Кнопка "Удалить" только если это дополнительный блок (idx > 0) // Первый блок предустановленного документа удалять нельзя (currentBlocks.length > 1 && idx > 0) && ( - + ) } > @@ -1027,28 +1064,30 @@ export default function StepWizardPlan({ {/* Поле описания показываем только для дополнительных блоков (idx > 0) или для общих документов (docs_exist) */} {(idx > 0 || !isPredefinedDoc) && ( - - updateDocumentBlock(docId, block.id, { description: e.target.value }) - } - /> + value={block.description} + onChange={(e) => + updateDocumentBlock(docId, block.id, { description: e.target.value }) + } + maxLength={500} + showCount + /> )} {/* Выпадашка категорий только для общих вопросов (docs_exist, correspondence_exist) */} {!isPredefinedDoc && ( - + )} } + + )} ); @@ -1109,27 +1148,61 @@ export default function StepWizardPlan({ const renderCustomUploads = () => ( }> - Добавить блок - + style={{ + marginTop: 24, + borderRadius: 8, + border: '2px dashed #d9d9d9', + background: '#fafafa' + }} + title={ +
+ + Дополнительные документы +
} > {customFileBlocks.length === 0 && ( - - Можно добавить произвольные группы документов — например, переписку, дополнительные акты - или фото. - +
+ + Есть ещё документы, которые могут помочь? + + + Если у вас есть дополнительные документы, которые не указаны в списке выше, + вы можете загрузить их здесь. Например: + +
    +
  • Дополнительная переписка
  • +
  • Скриншоты переговоров
  • +
  • Дополнительные чеки или акты
  • +
  • Любые другие документы, которые могут быть полезны
  • +
+ +
)} {customFileBlocks.map((block, idx) => ( + + Дополнительный документ #{idx + 1} +
+ } extra={