Actual prod

This commit is contained in:
mac 2026-06-28 10:56:29 +03:00
parent f593915c05
commit a6ab73246e
6 changed files with 13004 additions and 11232 deletions

View File

@ -6,6 +6,25 @@ use Complex\Exception;
class AdvertsController extends ApiController
{
// Найденный листинг парсера read-only: рекламные действия недоступны
// (паритет с десктопом, где switchAdvert*/addPromotion и т.п. отбивают листинг). Вернёт true + 403, если листинг.
// Offset-free: после снятия смещения id листинга (= реальный external_listings.id) коллизит с objects.id,
// поэтому различить листинг по id нельзя. Признак приходит явным флагом is_external из запроса
// (фронт знает, что действие над найденным листингом). Сервер по id отличить листинг уже не может.
private function _rejectIfExternalListing($app, $is_external)
{
if (!empty($is_external)) {
$app->response->header('Content-Type', 'application/json');
$app->response->setStatus(403);
$app->response->setBody(json_encode([
'success' => false,
'errors' => ['Действие недоступно для найденного листинга. Скопируйте объект в свои, чтобы управлять им.'],
], JSON_UNESCAPED_UNICODE));
return true;
}
return false;
}
public function getPublishedPackages($app, $get, $post) {
$error = false;
@ -90,6 +109,9 @@ class AdvertsController extends ApiController
if (isset($data['object_id']))
$object_id = $data['object_id'];
// Признак найденного листинга — явный флаг из запроса (offset-free: id листинга коллизит с objects.id)
if ($this->_rejectIfExternalListing($app, isset($data['is_external']) ? $data['is_external'] : null)) return;
$service = null;
if (isset($data['service']))
$service = $data['service'];
@ -1814,6 +1836,9 @@ class AdvertsController extends ApiController
if (isset($get['object_id']))
$object_id = $get['object_id'];
// Признак найденного листинга — явный флаг из запроса (offset-free: id листинга коллизит с objects.id)
if ($this->_rejectIfExternalListing($app, isset($get['is_external']) ? $get['is_external'] : null)) return;
if (($user_id = $_SESSION['id']) && !is_null($object_id)) {
// Открываем соединение с БД
@ -1867,6 +1892,9 @@ class AdvertsController extends ApiController
if (isset($data['object_id']))
$object_id = $data['object_id'];
// Признак найденного листинга — явный флаг из запроса (offset-free: id листинга коллизит с objects.id)
if ($this->_rejectIfExternalListing($app, isset($data['is_external']) ? $data['is_external'] : null)) return;
$packages = null;
if (isset($data['packages']))
$packages = $data['packages'];
@ -2036,6 +2064,9 @@ class AdvertsController extends ApiController
if (isset($data['object_id']))
$object_id = $data['object_id'];
// Признак найденного листинга — явный флаг из запроса (offset-free: id листинга коллизит с objects.id)
if ($this->_rejectIfExternalListing($app, isset($data['is_external']) ? $data['is_external'] : null)) return;
$price = null;
if (isset($data['price']))
$price = $data['price'];
@ -2131,6 +2162,9 @@ class AdvertsController extends ApiController
if (isset($data['object_id']))
$object_id = $data['object_id'];
// Признак найденного листинга — явный флаг из запроса (offset-free: id листинга коллизит с objects.id)
if ($this->_rejectIfExternalListing($app, isset($data['is_external']) ? $data['is_external'] : null)) return;
$price = null;
if (isset($data['price']))
$price = $data['price'];
@ -2597,6 +2631,9 @@ class AdvertsController extends ApiController
if (isset($data['object_id']))
$object_id = $data['object_id'];
// Признак найденного листинга — явный флаг из запроса (offset-free: id листинга коллизит с objects.id)
if ($this->_rejectIfExternalListing($app, isset($data['is_external']) ? $data['is_external'] : null)) return;
$services = null;
if (isset($data['services']))
$services = $data['services'];
@ -2647,6 +2684,9 @@ class AdvertsController extends ApiController
if (isset($data['object_id']))
$object_id = $data['object_id'];
// Признак найденного листинга — явный флаг из запроса (offset-free: id листинга коллизит с objects.id)
if ($this->_rejectIfExternalListing($app, isset($data['is_external']) ? $data['is_external'] : null)) return;
$employee_ids = null;
if (isset($data['employee_ids']))
$employee_ids = $data['employee_ids'];

View File

@ -25,6 +25,16 @@ class ClientsController extends ApiController
$menuPermissions = $user->checkMenuPermissions();
$agency_id = $_SESSION['agency_id'];
// Наполняем new_transfers для is_new здесь, не полагаясь на getFunnels
$nt_agency_id = \User::getUserAgencyID($user_id);
$nt_ids = [];
$nt_sql = "SELECT DISTINCT `clients`.`id` FROM `events_clients` AS `events` LEFT JOIN `clients` AS `clients` ON `clients`.`id` = `events`.`client_id` LEFT JOIN `funnel` AS `funnel` ON `funnel`.`id` = `clients`.`funnel_id` WHERE (`events`.`user_id` = '$user_id' AND `events`.`from_id` = '$user_id') AND (`events`.`event` = 'accepted' OR `events`.`event` = 'accepted_doer') AND `events`.`viewed` = 0 AND `events`.`read` = 1 AND (`clients`.`confirm` = 1 OR `clients`.`doers_confirm` = 1) AND `events`.`req_id` = 0 AND `clients`.`deleted` <> '1' AND (`clients`.`who_work` = '$user_id' OR `clients`.`doers` LIKE '%&quot;{$user_id}&quot;:1%') AND (`clients`.`funnel_id` = 0 OR (`funnel`.`agency_id` = '$nt_agency_id' AND `funnel`.`deleted` = 0))";
if ($nt_q = mysql_query($nt_sql)) {
while ($nt_row = mysql_fetch_assoc($nt_q))
$nt_ids[] = $nt_row['id'];
}
$_SESSION['new_transfers']['clients'] = $nt_ids;
$clients = [];
$pagination = null;
@ -1092,7 +1102,7 @@ class ClientsController extends ApiController
if (isset($users[$client['id_agent']])) {
if ($user_who = $users[$client['id_agent']]) {
if ($user_who['agency']) {
$who_added['name'] = "Агентство {$user_who['agency_name']}";
$who_added['name'] = trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']) ?: "Агентство {$user_who['agency_name']}";
} else if ($user_who['manager']) {
$who_added['name'] = "Менеджер " . trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']);
if ($user_who['role_id'] > 0)
@ -1117,7 +1127,7 @@ class ClientsController extends ApiController
if ($user_who = $users[$client['who_delete']]) {
if ($user_who['agency']) {
$who_delete['name'] = "Агентство {$user_who['agency_name']}";
$who_delete['name'] = trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']) ?: "Агентство {$user_who['agency_name']}";
} else if ($user_who['manager']) {
$who_delete['name'] = "Менеджер " . trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']);
} else {
@ -1151,7 +1161,7 @@ class ClientsController extends ApiController
if ($user_who = $users[$client['who_work']]) {
if ($user_who['agency']) {
$who_work['name'] = "Агентство {$user_who['agency_name']}";
$who_work['name'] = trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']) ?: "Агентство {$user_who['agency_name']}";
} else if ($user_who['manager']) {
if ($user_who['role_id'] > 0) {
$who_work['name'] = $departments[$user_who['role_id']]['role_name'] . ' ' . trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']);
@ -1197,7 +1207,7 @@ class ClientsController extends ApiController
$user_who = $users[$id];
if ($status == 0) {
if ($user_who['agency']) {
$doers_request['name'] = "Агентство {$user_who['agency_name']}";
$doers_request['name'] = trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']) ?: "Агентство {$user_who['agency_name']}";
} else if ($user_who['manager']) {
$doers_request['name'] = "Менеджер " . trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']);
} else {
@ -1208,7 +1218,7 @@ class ClientsController extends ApiController
}
if ($status == 1) {
if ($user_who['agency']) {
$doers_accept['name'] = "Агентство {$user_who['agency_name']}";
$doers_accept['name'] = trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']) ?: "Агентство {$user_who['agency_name']}";
} else if ($user_who['manager']) {
$doers_accept['name'] = "Менеджер " . trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']);
} else {
@ -1886,7 +1896,7 @@ class ClientsController extends ApiController
if (isset($users[$data['id_agent']])) {
if ($user_who = $users[$data['id_agent']]) {
if ($user_who['agency']) {
$who_added['name'] = "Агентство {$user_who['agency_name']}";
$who_added['name'] = trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']) ?: "Агентство {$user_who['agency_name']}";
} else if ($user_who['manager']) {
$who_added['name'] = "Менеджер " . trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']);
if ($user_who['role_id'] > 0)
@ -1911,7 +1921,7 @@ class ClientsController extends ApiController
if ($user_who = $users[$data['who_delete']]) {
if ($user_who['agency']) {
$who_delete['name'] = "Агентство {$user_who['agency_name']}";
$who_delete['name'] = trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']) ?: "Агентство {$user_who['agency_name']}";
} else if ($user_who['manager']) {
$who_delete['name'] = "Менеджер " . trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']);
} else {
@ -1945,7 +1955,7 @@ class ClientsController extends ApiController
if ($user_who = $users[$data['who_work']]) {
if ($user_who['agency']) {
$who_work['name'] = "Агентство {$user_who['agency_name']}";
$who_work['name'] = trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']) ?: "Агентство {$user_who['agency_name']}";
} else if ($user_who['manager']) {
if ($user_who['role_id'] > 0) {
$who_work['name'] = $departments[$user_who['role_id']]['role_name'] . ' ' . trim($user_who['last_name'] . ' ' . $user_who['first_name'] . ' ' . $user_who['middle_name']);
@ -2446,33 +2456,64 @@ class ClientsController extends ApiController
$variants = [];
$sql = "SELECT `sended_pdf`.*,
0 AS `close_contact`,
0 AS `new_price`,
DATE_FORMAT(`sended_pdf`.`date_send`, '%d.%m.%Y') AS `date_f`,
DATE_FORMAT(`sended_pdf`.`date_send`, '%H:%i') AS `time_f`,
`archive`.`phone`,
`archive`.`sobstv`,
`archive`.`id` as `object_id`,
`archive`.`nazv` as `object_title`,
`archive`.`adres` as `object_address`,
0 as `newbild`
FROM `sended_pdf`, `archive`
WHERE `sended_pdf`.`id_client` = '$client_id' AND
`archive`.`id_obj`=`sended_pdf`.`id_object`
UNION ALL SELECT `sended_pdf_newbuildings`.*,
0 AS `close_contact`,
0 AS `new_price`,
DATE_FORMAT(`sended_pdf_newbuildings`.`date_send`, '%d.%m.%Y') AS `date_f`,
DATE_FORMAT(`sended_pdf_newbuildings`.`date_send`, '%H:%i') AS `time_f`,
null as `phone`,
null as `sobstv`,
null as `object_id`,
null as `object_title`,
null as `object_address`,
1 as `newbild`
FROM `sended_pdf_newbuildings`
WHERE `sended_pdf_newbuildings`.`id_client` = '$client_id'
// Распознавание найденных листингов парсера и витринный EXT-id.
// Offset-free: id_object для листинга = реальный external_listings.id,
// он коллизит с objects.id. Листинг различаем по флагу sended_pdf.is_external;
// витринный id собираем как 'EXT-{el.id}' напрямую (listing_guard.php тут больше не нужен).
$ext_variant_rows = []; // позиция в $variants => реальный el.id листинга
$ext_title_fallback = []; // позиция в $variants => запасной заголовок из снапшота отправки
// Три ветки UNION:
// 1) свои объекты (is_external=0) — INNER JOIN archive по id_object (даёт телефон/собственника/адрес снапшота);
// 2) найденные листинги (is_external=1) — БЕЗ join к archive: id_object = реальный el.id коллизит с
// objects.id, join к archive подцепил бы чужой одноимённый снапшот; адрес/заголовок добиваются батчем
// из external_listings после цикла;
// 3) новостройки (sended_pdf_newbuildings) — листингами никогда не бывают.
$sql = "SELECT `sended_pdf`.`id`, `sended_pdf`.`id_agent`, `sended_pdf`.`id_object`, `sended_pdf`.`date_send`, `sended_pdf`.`id_client`, `sended_pdf`.`nazv`, `sended_pdf`.`path`, `sended_pdf`.`send_type`, `sended_pdf`.`close_contact`, `sended_pdf`.`hide_address`, `sended_pdf`.`new_price`, `sended_pdf`.`description`, `sended_pdf`.`token`, `sended_pdf`.`id_request`, `sended_pdf`.`sent_catalog_id`, `sended_pdf`.`is_favorite`,
0 AS `close_contact`,
0 AS `new_price`,
DATE_FORMAT(`sended_pdf`.`date_send`, '%d.%m.%Y') AS `date_f`,
DATE_FORMAT(`sended_pdf`.`date_send`, '%H:%i') AS `time_f`,
`archive`.`phone`,
`archive`.`sobstv`,
`archive`.`id` as `object_id`,
`archive`.`nazv` as `object_title`,
`archive`.`adres` as `object_address`,
0 as `newbild`,
0 as `is_external`
FROM `sended_pdf`, `archive`
WHERE `sended_pdf`.`id_client` = '$client_id' AND
`sended_pdf`.`is_external` = 0 AND
`archive`.`id_obj`=`sended_pdf`.`id_object`
UNION ALL SELECT `sended_pdf`.`id`, `sended_pdf`.`id_agent`, `sended_pdf`.`id_object`, `sended_pdf`.`date_send`, `sended_pdf`.`id_client`, `sended_pdf`.`nazv`, `sended_pdf`.`path`, `sended_pdf`.`send_type`, `sended_pdf`.`close_contact`, `sended_pdf`.`hide_address`, `sended_pdf`.`new_price`, `sended_pdf`.`description`, `sended_pdf`.`token`, `sended_pdf`.`id_request`, `sended_pdf`.`sent_catalog_id`, `sended_pdf`.`is_favorite`,
0 AS `close_contact`,
0 AS `new_price`,
DATE_FORMAT(`sended_pdf`.`date_send`, '%d.%m.%Y') AS `date_f`,
DATE_FORMAT(`sended_pdf`.`date_send`, '%H:%i') AS `time_f`,
null as `phone`,
null as `sobstv`,
null as `object_id`,
null as `object_title`,
null as `object_address`,
0 as `newbild`,
1 as `is_external`
FROM `sended_pdf`
WHERE `sended_pdf`.`id_client` = '$client_id' AND
`sended_pdf`.`is_external` = 1
UNION ALL SELECT `sended_pdf_newbuildings`.`id`, `sended_pdf_newbuildings`.`id_agent`, `sended_pdf_newbuildings`.`id_object`, `sended_pdf_newbuildings`.`date_send`, `sended_pdf_newbuildings`.`id_client`, `sended_pdf_newbuildings`.`nazv`, `sended_pdf_newbuildings`.`path`, `sended_pdf_newbuildings`.`send_type`, `sended_pdf_newbuildings`.`close_contact`, `sended_pdf_newbuildings`.`hide_address`, `sended_pdf_newbuildings`.`new_price`, null as `description`, `sended_pdf_newbuildings`.`token`, `sended_pdf_newbuildings`.`id_request`, `sended_pdf_newbuildings`.`sent_catalog_id`, `sended_pdf_newbuildings`.`is_favorite`,
0 AS `close_contact`,
0 AS `new_price`,
DATE_FORMAT(`sended_pdf_newbuildings`.`date_send`, '%d.%m.%Y') AS `date_f`,
DATE_FORMAT(`sended_pdf_newbuildings`.`date_send`, '%H:%i') AS `time_f`,
null as `phone`,
null as `sobstv`,
null as `object_id`,
null as `object_title`,
null as `object_address`,
1 as `newbild`,
0 as `is_external`
FROM `sended_pdf_newbuildings`
WHERE `sended_pdf_newbuildings`.`id_client` = '$client_id'
ORDER BY `date_send` DESC";
if ($query = mysql_query($sql)) {
@ -2513,6 +2554,58 @@ class ClientsController extends ApiController
'is_sended' => in_array($pdf['id_object'], $toSend),
'is_new' => $new_date,
];
// Найденный листинг парсера: строки в objects нет, помечаем элемент и
// запоминаем позицию — дообогащение из external_listings батчем после цикла.
// Offset-free: листинг различаем по флагу is_external (Sphinx/БД-атрибут), id_object —
// уже реальный external_listings.id, его и кладём как ключ дообогащения.
if ((int)$pdf['is_external'] === 1) {
$ext_idx = count($variants) - 1;
$ext_real_id = (int)$pdf['id_object'];
$variants[$ext_idx]['is_external'] = true;
$variants[$ext_idx]['display_id'] = 'EXT-' . $ext_real_id;
$variants[$ext_idx]['is_removed'] = false;
$ext_variant_rows[$ext_idx] = $ext_real_id;
$ext_title_fallback[$ext_idx] = trim($pdf['nazv']);
}
}
// Дообогащение строк-листингов: заголовок/адрес берём из external_listings одним батчем.
// Offset-free: ключ — реальный external_listings.id (без смещения), запрашиваем и индексируем по нему.
if (!empty($ext_variant_rows)) {
// мягкая деградация: проблема с external_listings не должна валить историю отправок
try {
$pdo = $app->container->get('pdo');
$ext_real_ids = array_map('intval', array_values($ext_variant_rows));
$ext_sth = $pdo->prepare("SELECT id, address_text, price FROM external_listings WHERE id IN (" . implode(',', array_unique($ext_real_ids)) . ")");
if ($ext_sth->execute()) {
// Мапа листингов ключуется на реальный el.id и держится ОТДЕЛЬНО от objects —
// одноимённый objects.id сюда не попадает (запрос только по external_listings).
$ext_by_id = [];
while ($ext_row = $ext_sth->fetch(\PDO::FETCH_ASSOC))
$ext_by_id[(int)$ext_row['id']] = $ext_row;
foreach ($ext_variant_rows as $ext_idx => $ext_real_id) {
if (isset($ext_by_id[$ext_real_id])) {
// живой листинг: пустые заголовок/адрес добиваем адресом первоисточника
$ext_address = trim($ext_by_id[$ext_real_id]['address_text']);
if ($variants[$ext_idx]['object_title'] === '')
$variants[$ext_idx]['object_title'] = $ext_address;
if ($variants[$ext_idx]['object_address'] === '')
$variants[$ext_idx]['object_address'] = $ext_address;
} else {
// объявление снято с публикации: остаётся nazv-снапшот из истории отправки
$variants[$ext_idx]['is_removed'] = true;
if ($variants[$ext_idx]['object_title'] === '')
$variants[$ext_idx]['object_title'] = $ext_title_fallback[$ext_idx];
}
}
}
} catch (\Exception $e) {
// данных по листингам нет — строки остаются с пометкой is_external без дообогащения
}
}
} else {
$error = true;
@ -2579,6 +2672,7 @@ class ClientsController extends ApiController
$history = [];
// copy_created / copy_from — лейбл пустой: текст идёт в dop_text, иначе в истории двоится.
$array_ev = [
'add'=>'Добавлен',
'transmitted' => 'Передан',
@ -2590,7 +2684,10 @@ class ClientsController extends ApiController
'notadopted_doer' => 'Приглашение не принято',
'stageedit' => 'Изменен этап',
'delete' => 'Закрыт',
'double_client' => 'Повторное обращение'
'double_client' => 'Повторное обращение',
// copy_* — плейсхолдер {N} подменяется ниже на кликабельный ID из dop_text.
'copy_created' => 'Создана копия → ID {N}',
'copy_from' => 'Создана как копия заявки ID {N}'
];
$sql = "(SELECT id, client_id, user_id, from_id, `event`, `date`, `dop_text`, 0 as `of_objects` FROM events_clients WHERE client_id = $client_id)
@ -2614,7 +2711,7 @@ class ClientsController extends ApiController
$mstr = mysql_fetch_assoc($q_m);
if ($mstr['agency'])
$master = "Агентство $mstr[agency_name]";
$master = trim($mstr['last_name'] . ' ' . $mstr['first_name'] . ' ' . $mstr['middle_name']) ?: "Агентство {$mstr['agency_name']}";
else if ($mstr['manager'])
$master = "Менеджер " . trim($mstr['last_name'] . ' ' . $mstr['first_name'] . ' ' . $mstr['middle_name']);
else
@ -2634,7 +2731,7 @@ class ClientsController extends ApiController
$mstr = mysql_fetch_assoc($q_m);
if ($mstr['agency'])
$from_master = "агентство $mstr[agency_name]";
$from_master = trim($mstr['last_name'] . ' ' . $mstr['first_name'] . ' ' . $mstr['middle_name']) ?: "агентство {$mstr['agency_name']}";
else if($mstr['manager'])
$from_master = "менеджер " . trim($mstr['last_name'] . ' ' . $mstr['first_name'] . ' ' . $mstr['middle_name']);
else
@ -2676,13 +2773,23 @@ class ClientsController extends ApiController
$dop_text .= ' по звонку по объекту '.$name;
}
// copy_* — рендерим label-шаблон, подставляя int-id из dop_text.
$event_label = $array_ev[$r_ev['event']];
if (in_array($r_ev['event'], ['copy_from', 'copy_created'], true)) {
$linkId = (int)$r_ev['dop_text'];
if ($linkId > 0) {
$event_label = str_replace('{N}', (string)$linkId, $event_label);
$r_ev['dop_text'] = ''; // весь текст уже в label
}
}
$history[] = [
'datetime' => date('Y-m-d H:i:s', $udate),
'date_string' => $date_row,
'time_string' => date('H:i:s', $udate),
'to_user' => $master,
'from_user' => $from_master,
'event' => $array_ev[$r_ev['event']] ." ". (function_exists('mask_text_for_hide_user') ? mask_text_for_hide_user($dop_text . $r_ev['dop_text']) : ($dop_text . $r_ev['dop_text'])) ." ". $from_master,
'event' => $event_label ." ". (function_exists('mask_text_for_hide_user') ? mask_text_for_hide_user($dop_text . $r_ev['dop_text']) : ($dop_text . $r_ev['dop_text'])) ." ". $from_master,
];
}
} else {

22000
api/v2/app/src/controllers/CommonController.php Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@ -153,6 +153,23 @@ class ContractsController extends ApiController
else if ($section == "objects")
$parent_type = 'object';
// Договор на найденном листинге парсера запрещён — листинг read-only,
// сначала копирование объекта в свои.
// Offset-free: после снятия смещения id листинга (= реальный external_listings.id) коллизит
// с objects.id, поэтому различить листинг по id нельзя. Признак — явный флаг is_external
// из запроса (фронт знает, что договор оформляют на найденном листинге).
if ($parent_type == 'object' && !empty($data['is_external'])) {
if ($need_return)
return false;
$app->response->header('Content-Type', 'application/json');
$app->response->setStatus(403);
$app->response->setBody(json_encode([
'success' => false,
'errors' => ['Действие недоступно для найденного листинга. Скопируйте объект в свои, чтобы управлять им.'],
], JSON_UNESCAPED_UNICODE));
$app->stop();
}
if (($user_id = $_SESSION['id']) && $parent_type) {
// Открываем соединение с БД

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,27 @@ namespace App\v2\Controller;
class RequisitionsController extends ApiController
{
// Гейтинг найденных листингов парсера: заявку нельзя привязать к чужому объявлению —
// оно read-only, сначала копирование объекта в свои.
// Offset-free: после снятия смещения id листинга (= реальный external_listings.id) коллизит
// с objects.id, поэтому различить листинг по id нельзя. Признак — явный флаг is_external
// из запроса (фронт знает, что заявку привязывают к найденному листингу).
// Возвращает true, если запрос отклонён (вызывающий метод делает return).
private function _rejectIfExternalListing($app, $is_external)
{
if (!empty($is_external)) {
$app->response->header('Content-Type', 'application/json');
$app->response->setStatus(403);
$app->response->setBody(json_encode([
'success' => false,
'errors' => ['Действие недоступно для найденного листинга. Скопируйте объект в свои, чтобы управлять им.'],
], JSON_UNESCAPED_UNICODE));
return true;
}
return false;
}
public function getRequisitions($app, $get = null, $post = null, $need_return = false) {
$start_time = microtime(true);
$error = false;
@ -385,6 +406,17 @@ class RequisitionsController extends ApiController
}
$req_filter['stages'] = (object)$req_filter['stages'];
// Наполняем new_transfers для is_new здесь, не полагаясь на getFunnels
$nt_agency_id = \User::getUserAgencyID($user_id);
$nt_ids = [];
$nt_sql = "SELECT DISTINCT `req`.`id` FROM `events_clients` AS `events` LEFT JOIN `requisitions` AS `req` ON `req`.`id` = `events`.`req_id` LEFT JOIN `funnel` AS `funnel` ON `funnel`.`id` = `req`.`funnel_id` WHERE (`events`.`user_id` = '$user_id' AND `events`.`from_id` = '$user_id') AND (`events`.`event` = 'accepted' OR `events`.`event` = 'accepted_doer') AND `events`.`viewed` = 0 AND `events`.`read` = 1 AND (`req`.`confirm` = 1 OR `req`.`doers_confirm` = 1) AND `events`.`client_id` = 0 AND `req`.`deleted` = 0 AND (`req`.`who_work` = '$user_id' OR `req`.`doers` LIKE '%&quot;{$user_id}&quot;:1%') AND (`req`.`funnel_id` = 0 OR (`funnel`.`agency_id` = '$nt_agency_id' AND `funnel`.`deleted` = 0))";
if ($nt_q = mysql_query($nt_sql)) {
while ($nt_row = mysql_fetch_assoc($nt_q))
$nt_ids[] = $nt_row['id'];
}
$_SESSION['new_transfers']['requisitions'] = $nt_ids;
$results = $req_inst->get_all($search_user_id, (object)$req_filter, (string)$funnel_id, $client_id, $object_id, $need_autosearch);
$pagination = [
@ -1193,6 +1225,11 @@ class RequisitionsController extends ApiController
$fields_result = null;
// Заявку нельзя привязать к найденному листингу парсера — листинг read-only.
// Признак найденного листинга — явный флаг из запроса (offset-free: id листинга коллизит с objects.id).
if ($this->_rejectIfExternalListing($app, isset($data['is_external']) ? $data['is_external'] : null))
return;
$req = new \Requisitions($pdo);
$result = $req->add_edit($params);
if (isset($result['errors'])) {
@ -2583,9 +2620,293 @@ ini_set('display_startup_errors', 1);*/
$app->response->setStatus(200);
$app->response->setBody(json_encode([
'success' => true,
'funnel_id ' => $funnel_id
'funnel_id ' => $funnel_id
], JSON_UNESCAPED_UNICODE));
$app->stop();
}
// POST /api/v2/requisitions/copy-preview — дефолты модалки + embed funnels/employees (1 round-trip для mJW).
public function copyPreview($app, $get = null, $post = null, $need_return = false) {
$body = $app->request->getBody();
$data = json_decode($body, true);
$user_id = isset($_SESSION['id']) ? (int)$_SESSION['id'] : 0;
if ($user_id <= 0) {
$app->response->setStatus(401);
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(['success' => false, 'error' => 'unauthorized'], JSON_UNESCAPED_UNICODE));
$app->stop();
}
$srcId = isset($data['req_id']) ? (int)$data['req_id'] : 0;
if ($srcId <= 0) {
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(['success' => false, 'error' => 'Заявка обязательна'], JSON_UNESCAPED_UNICODE));
$app->stop();
}
// mysql-сервис нужен: User::getAll* используют mysql_*.
$mdb = $app->container->get('mysql');
$pdo = $app->container->get('mypdo');
$src = mysql_fetch_assoc(mysql_query("SELECT * FROM requisitions WHERE id = {$srcId} LIMIT 1"));
if (!$src) {
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(['success' => false, 'error' => 'Заявка не найдена'], JSON_UNESCAPED_UNICODE));
$app->stop();
}
if ((int)$src['deleted'] === 1) {
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(['success' => false, 'error' => 'Заявка удалена'], JSON_UNESCAPED_UNICODE));
$app->stop();
}
// set_user — для checkMenuPermissions внутри user_can_edit_req.
$req = new \Requisitions($pdo);
$userForCheck = new \User();
$userForCheck->checkPermissions();
$req->set_user($userForCheck);
if (!$req->user_can_edit_req($src, $user_id)) {
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(['success' => false, 'error' => 'Нет прав на копирование'], JSON_UNESCAPED_UNICODE));
$app->stop();
}
$srcTypeId = (int)$src['type_id'];
$typeRow = mysql_fetch_assoc(mysql_query("SELECT id, heir FROM requisitions_type WHERE id = {$srcTypeId} LIMIT 1"));
$baseTypeId = $typeRow ? requisition_base_type_id($typeRow) : 0;
// warning contract_without_object — object_id не копируется, но у исходника есть договор продажи.
$warnings = array();
if (!requisition_should_copy_object($baseTypeId) && (int)$src['object_id'] > 0) {
$hasSaleContract = mysql_fetch_assoc(
mysql_query("SELECT id FROM contracts_req WHERE req_id = {$srcId} AND type = 2 LIMIT 1")
);
if ($hasSaleContract) {
$warnings[] = 'contract_without_object';
}
}
// Канонический источник воронок: Funnel::get_funnels (включает дефолт id=0 и visibility).
$userAgency = (int)$_SESSION['agency_id'];
$funnelClass = new \Funnel($pdo);
$funnelClass->set_agency_id($userAgency);
$funnelClass->set_user_id($user_id);
$funnelsRaw = $funnelClass->get_funnels('requisitions');
$funnels = array();
if (is_array($funnelsRaw)) {
foreach ($funnelsRaw as $rF) {
$funnels[] = array(
'id' => (int)$rF['id'],
'name' => (isset($rF['name']) && $rF['name'] !== null && $rF['name'] !== '')
? (string)$rF['name']
: 'Обычные',
);
}
}
// Сотрудники: Я + «Всё агентство» (если разрешено) + менеджеры + агенты.
$employees = array(array('id' => $user_id, 'name' => 'Я'));
$r_perm = mysql_fetch_assoc(
mysql_query("SELECT menu_can_transfer_to_common FROM user_permissions WHERE user_id = {$user_id} LIMIT 1")
);
if (
!empty($_SESSION['agency'])
|| !empty($_SESSION['users_admin'])
|| ($r_perm && (int)$r_perm['menu_can_transfer_to_common'] === 1)
) {
$employees[] = array('id' => 0, 'name' => 'Всё агентство');
}
$managers = \User::getAllManagers($userAgency);
if ($managers) {
foreach ($managers as $m) {
$employees[] = array(
'id' => (int)$m['id'],
'name' => trim($m['last_name'] . ' ' . $m['first_name'] . ' ' . $m['middle_name']),
'group' => 'Менеджеры',
);
}
}
$agents = \User::getAllAgents($userAgency);
if ($agents) {
foreach ($agents as $a) {
$employees[] = array(
'id' => (int)$a['id'],
'name' => trim($a['last_name'] . ' ' . $a['first_name'] . ' ' . $a['middle_name']),
'group' => 'Агенты',
);
}
}
$app->response->header('Content-Type', 'application/json');
$app->response->setStatus(200);
$app->response->setBody(json_encode(array(
'success' => true,
'default_funnel_id' => (int)$src['funnel_id'],
'default_who_work_id' => (int)$src['who_work'],
'base_type_id' => $baseTypeId,
'warnings' => $warnings,
'funnels' => $funnels,
'employees' => $employees,
), JSON_UNESCAPED_UNICODE));
$app->stop();
}
// POST /api/v2/requisitions/copy — создание копии заявки.
public function copy($app, $get = null, $post = null, $need_return = false) {
$body = $app->request->getBody();
$data = json_decode($body, true);
$currentId = isset($_SESSION['id']) ? (int)$_SESSION['id'] : 0;
if ($currentId <= 0) {
$app->response->setStatus(401);
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(['success' => false, 'error' => 'unauthorized'], JSON_UNESCAPED_UNICODE));
$app->stop();
}
$srcId = isset($data['req_id']) ? (int)$data['req_id'] : 0;
$funnelId = isset($data['funnel_id']) ? (int)$data['funnel_id'] : 0;
$whoWorkId = isset($data['who_work_id']) ? (int)$data['who_work_id'] : 0;
$userAgency = (int)$_SESSION['agency_id'];
// funnel_id = 0 — дефолтная воронка (funnel_default).
if ($srcId <= 0 || $funnelId < 0 || $whoWorkId <= 0) {
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(array(
'success' => false,
'error' => 'req_id, funnel_id, who_work_id обязательны',
'code' => 'bad_payload',
), JSON_UNESCAPED_UNICODE));
$app->stop();
}
// mysql-сервис нужен: legacy-SQL ниже + User::* внутри валидаций.
$mdb = $app->container->get('mysql');
$pdo = $app->container->get('mypdo');
$src = mysql_fetch_assoc(mysql_query("SELECT * FROM requisitions WHERE id = {$srcId} LIMIT 1"));
if (!$src) {
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(array(
'success' => false,
'error' => 'Заявка не найдена',
'code' => 'src_not_found',
), JSON_UNESCAPED_UNICODE));
$app->stop();
}
if ((int)$src['deleted'] === 1) {
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(array(
'success' => false,
'error' => 'Заявка удалена',
'code' => 'src_not_found',
), JSON_UNESCAPED_UNICODE));
$app->stop();
}
// set_user — для checkMenuPermissions внутри user_can_edit_req.
$req = new \Requisitions($pdo);
$userForCheck = new \User();
$userForCheck->checkPermissions();
$req->set_user($userForCheck);
// Право на копирование == право на редактирование исходника.
if (!$req->user_can_edit_req($src, $currentId)) {
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(array(
'success' => false,
'error' => 'Нет прав на копирование',
'code' => 'no_edit_right',
), JSON_UNESCAPED_UNICODE));
$app->stop();
}
// Доступ к воронке. funnel_id=0 — проверяем funnel_default.is_req.
if ($funnelId === 0) {
$funnelDef = mysql_fetch_assoc(mysql_query(
"SELECT `is_req` FROM `funnel_default`
WHERE `agency_id` = {$userAgency}
LIMIT 1"
));
if ($funnelDef && (int)$funnelDef['is_req'] === 0) {
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(array(
'success' => false,
'error' => 'Воронка недоступна',
'code' => 'funnel_forbidden',
), JSON_UNESCAPED_UNICODE));
$app->stop();
}
} else {
// Проверка доступности — через канонический Funnel::get_funnels() (учитывает is_no_see + раскрытие 'all_X').
$funnelCheckClass = new \Funnel($pdo);
$funnelCheckClass->set_agency_id($userAgency);
$funnelCheckClass->set_user_id($currentId);
$availableFunnels = $funnelCheckClass->get_funnels('requisitions');
$isFunnelAvailable = false;
if (is_array($availableFunnels)) {
foreach ($availableFunnels as $af) {
if ((int)$af['id'] === $funnelId) { $isFunnelAvailable = true; break; }
}
}
if (!$isFunnelAvailable) {
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(array(
'success' => false,
'error' => 'Воронка недоступна',
'code' => 'funnel_forbidden',
), JSON_UNESCAPED_UNICODE));
$app->stop();
}
}
// Ответственный. Себя всегда можно назначить.
if ($whoWorkId !== $currentId) {
if ($funnelId === 0) {
// user_can_be_responsible требует funnel_id > 0 — для дефолтной проверяем вручную (agency + blocked).
$candidateAgency = (int)\User::getUserAgencyID($whoWorkId);
if ($candidateAgency !== $userAgency) {
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(array(
'success' => false,
'error' => 'Выбранный ответственный недоступен',
'code' => 'who_work_forbidden',
), JSON_UNESCAPED_UNICODE));
$app->stop();
}
$blockedRow = mysql_fetch_assoc(mysql_query(
"SELECT `blocked` FROM `users` WHERE `id` = {$whoWorkId} LIMIT 1"
));
if (!$blockedRow || (int)$blockedRow['blocked'] === 1) {
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(array(
'success' => false,
'error' => 'Выбранный ответственный недоступен',
'code' => 'who_work_forbidden',
), JSON_UNESCAPED_UNICODE));
$app->stop();
}
} else if (!$req->user_can_be_responsible($whoWorkId, $userAgency, $funnelId)) {
$app->response->header('Content-Type', 'application/json');
$app->response->setBody(json_encode(array(
'success' => false,
'error' => 'Выбранный ответственный недоступен',
'code' => 'who_work_forbidden',
), JSON_UNESCAPED_UNICODE));
$app->stop();
}
}
$result = $req->copy($srcId, $funnelId, $whoWorkId, $currentId);
$app->response->header('Content-Type', 'application/json');
$app->response->setStatus(200);
$app->response->setBody(json_encode($result, JSON_UNESCAPED_UNICODE));
$app->stop();
}
}