actual prod

This commit is contained in:
mac 2026-07-06 21:13:55 +03:00
parent 11cdbeb6fd
commit b67a39532a
3 changed files with 1916 additions and 1308 deletions

View File

@ -179,6 +179,12 @@ if($request == 'add_edit') {
$params['complex_room_id'] = $data->complex_room_id; $params['complex_room_id'] = $data->complex_room_id;
} }
// Заявку нельзя привязать к найденному листингу парсера — листинг read-only, сначала копирование объекта в свои.
// Offset-free: признак листинга — явный флаг is_external из JSON-тела (el.id коллизит с objects.id).
require_once $_SERVER['DOCUMENT_ROOT'] . '/engine/helpers/listing_guard.php';
$objIsExternal = (isset($data->is_external) && $data->is_external) || (isset($data->src) && $data->src === 'ext');
reject_if_external_listing($params['object_id'], $objIsExternal);
$req = new Requisitions($pdo); $req = new Requisitions($pdo);
$result = $req->add_edit($params); $result = $req->add_edit($params);
if(isset($result['error'])){ if(isset($result['error'])){
@ -226,6 +232,11 @@ if($request == 'update_req'){
if(!empty($objects)){ if(!empty($objects)){
$object = $objects[0]; $object = $objects[0];
} }
// Привязать к заявке найденный листинг парсера нельзя — листинг read-only.
// Offset-free: признак листинга — явный флаг is_external из JSON-тела (el.id коллизит с objects.id).
require_once $_SERVER['DOCUMENT_ROOT'] . '/engine/helpers/listing_guard.php';
$objIsExternal = (isset($data->is_external) && $data->is_external) || (isset($data->src) && $data->src === 'ext');
reject_if_external_listing($object, $objIsExternal);
$req_id_arr = explode(',',$req_id); $req_id_arr = explode(',',$req_id);
foreach($req_id_arr as $req){ foreach($req_id_arr as $req){
$object_id_req = 0; $object_id_req = 0;
@ -825,10 +836,10 @@ if($request == 'get_type'){
echo json_encode($res); echo json_encode($res);
exit(); exit();
} }
//Смена очереди //Смена очереди
if($request == 'set_type_order'){ if($request == 'set_type_order'){
/*error_reporting(E_ALL | E_STRICT); /*error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', 1);*/ ini_set('display_errors', 1);*/
$id = (int)$data->id; $id = (int)$data->id;
@ -842,11 +853,11 @@ if($request == 'get_type'){
echo json_encode($res); echo json_encode($res);
exit(); exit();
} }
//Редактирование/добавление типа //Редактирование/добавление типа
if($request == 'edit_add_type') if($request == 'edit_add_type')
{ {
/* error_reporting(E_ALL | E_STRICT); /* error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', 1);*/ ini_set('display_errors', 1);*/
$result = array(); $result = array();
@ -865,11 +876,11 @@ if($request == 'get_type'){
echo json_encode($result, JSON_UNESCAPED_UNICODE); echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit; exit;
} }
//Удаление типа заявки //Удаление типа заявки
if($request == 'del_type_req') if($request == 'del_type_req')
{ {
$result = array(); $result = array();
$agency_id = (int)$data->agency_id; $agency_id = (int)$data->agency_id;
$user_id = (int)$data->user_id; $user_id = (int)$data->user_id;
@ -883,11 +894,11 @@ if($request == 'get_type'){
} }
echo json_encode($result, JSON_UNESCAPED_UNICODE); echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit; exit;
} }
//Архивирование типа заявки //Архивирование типа заявки
if($request == 'archive_type_req') if($request == 'archive_type_req')
{ {
$result = array(); $result = array();
$agency_id = (int)$data->agency_id; $agency_id = (int)$data->agency_id;
$user_id = (int)$data->user_id; $user_id = (int)$data->user_id;
@ -902,11 +913,11 @@ if($request == 'get_type'){
} }
echo json_encode($result, JSON_UNESCAPED_UNICODE); echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit; exit;
} }
//Архивирование типа заявки //Архивирование типа заявки
if($request == 'recover_type_req') if($request == 'recover_type_req')
{ {
$result = array(); $result = array();
$agency_id = (int)$data->agency_id; $agency_id = (int)$data->agency_id;
$user_id = (int)$data->user_id; $user_id = (int)$data->user_id;
@ -921,10 +932,10 @@ if($request == 'get_type'){
} }
echo json_encode($result, JSON_UNESCAPED_UNICODE); echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit; exit;
} }
//Установка причин закрытия //Установка причин закрытия
if($request == 'set_type_denial'){ if($request == 'set_type_denial'){
$type_id = (int)$data->type_id; $type_id = (int)$data->type_id;
$agency_id = (int)$data->agency_id; $agency_id = (int)$data->agency_id;
$denial_id = (int)$data->denial_id; $denial_id = (int)$data->denial_id;
@ -933,10 +944,10 @@ if($request == 'get_type'){
// echo $agency_id; // echo $agency_id;
$req->set_agencyId($agency_id); $req->set_agencyId($agency_id);
$req->set_type_denial($type_id, $denial_id, $check); $req->set_type_denial($type_id, $denial_id, $check);
} }
//Список причин //Список причин
if($request == 'get_denials'){ if($request == 'get_denials'){
$result = array(); $result = array();
$agency_id = (int)$data->agency_id; $agency_id = (int)$data->agency_id;
$req = new Requisitions($pdo); $req = new Requisitions($pdo);
@ -944,11 +955,11 @@ if($request == 'get_type'){
$result = $req->get_denial_work(1); $result = $req->get_denial_work(1);
echo json_encode($result, JSON_UNESCAPED_UNICODE); echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit; exit;
} }
//Редактирование причин закрытия //Редактирование причин закрытия
if($request == 'edit_denial'){ if($request == 'edit_denial'){
/*error_reporting(E_ALL | E_STRICT); /*error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', 1);*/ ini_set('display_errors', 1);*/
$result = array(); $result = array();
@ -963,10 +974,10 @@ if($request == 'get_type'){
echo json_encode($result, JSON_UNESCAPED_UNICODE); echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit; exit;
} }
//Удаление причины //Удаление причины
if($request == 'del_denial'){ if($request == 'del_denial'){
$result = array(); $result = array();
$agency_id = (int)$data->agency_id; $agency_id = (int)$data->agency_id;
// $user_id = (int)$data->user_id; // $user_id = (int)$data->user_id;
@ -980,7 +991,7 @@ if($request == 'get_type'){
echo json_encode($result, JSON_UNESCAPED_UNICODE); echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit; exit;
} }
//Подсчет количества заявок или клиентов //Подсчет количества заявок или клиентов
if($request == 'confirmation_total_item'){ if($request == 'confirmation_total_item'){
@ -1880,8 +1891,8 @@ if($request == 'get_clients_list'){
exit; exit;
} }
//Типы недвижимости //Типы недвижимости
if($request == 'get_property_types'){ if($request == 'get_property_types'){
$result = array(); $result = array();
$sql = "SELECT * FROM `property_types`"; $sql = "SELECT * FROM `property_types`";
$q = $pdo->query($sql); $q = $pdo->query($sql);
@ -1890,10 +1901,10 @@ if($request == 'get_clients_list'){
} }
echo json_encode($result, JSON_UNESCAPED_UNICODE); echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit; exit;
} }
//Заявки для объединения //Заявки для объединения
if($request == 'get_union_reqs') { if($request == 'get_union_reqs') {
$result = array(); $result = array();
$reqs = $data->reqs; $reqs = $data->reqs;
$agency_id = $data->agency_id; $agency_id = $data->agency_id;
@ -1908,10 +1919,10 @@ if($request == 'get_clients_list'){
echo json_encode($result, JSON_UNESCAPED_UNICODE); echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit; exit;
} }
//Сохранение объединения заявок //Сохранение объединения заявок
if($request == 'save_union_req'){ if($request == 'save_union_req'){
$main_id = $data->main_id; $main_id = $data->main_id;
$reqs = $data->reqs; $reqs = $data->reqs;
$user_id = (int)$data->user_id; $user_id = (int)$data->user_id;
@ -1926,10 +1937,10 @@ if($request == 'get_clients_list'){
echo json_encode($result, JSON_UNESCAPED_UNICODE); echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit; exit;
} }
//Список присоединенных заявок //Список присоединенных заявок
if($request == 'get_reqs_joint'){ if($request == 'get_reqs_joint'){
$result = array(); $result = array();
$req_id = (int)$data->req_id; $req_id = (int)$data->req_id;
if($req_id > 0){ if($req_id > 0){
@ -1941,7 +1952,7 @@ if($request == 'get_clients_list'){
} }
echo json_encode($result, JSON_UNESCAPED_UNICODE); echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit; exit;
} }
if ($request == 'mass_take_from_closed') { if ($request == 'mass_take_from_closed') {
$reqs = $data->reqs_id; $reqs = $data->reqs_id;
@ -2010,11 +2021,11 @@ if($request == 'get_view_page'){
exit; exit;
} }
/** /**
* Сохранение настроек вида страницы * Сохранение настроек вида страницы
* *
*/ */
if($request == 'set_view_page'){ if($request == 'set_view_page'){
$user_id = (int)$data->user_id; $user_id = (int)$data->user_id;
$view = $data->view; $view = $data->view;
@ -2028,12 +2039,12 @@ if($request == 'get_view_page'){
$sql_in = "INSERT INTO user_views_page (user_id, view_req) values ({$user_id}, '{$view}')"; $sql_in = "INSERT INTO user_views_page (user_id, view_req) values ({$user_id}, '{$view}')";
$pdo->query($sql_in); $pdo->query($sql_in);
} }
} }
/** /**
* Финансы * Финансы
*/ */
if($request == 'req_finance_info'){ if($request == 'req_finance_info'){
$is_block = false; $is_block = false;
$html = ''; $html = '';
$req_id = (int)$data->req_id; $req_id = (int)$data->req_id;
@ -2171,12 +2182,12 @@ if($request == 'get_view_page'){
$result = array('is_block'=>$is_block, 'html'=>$html); $result = array('is_block'=>$is_block, 'html'=>$html);
echo json_encode($result, JSON_UNESCAPED_UNICODE); echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit; exit;
} }
/** /**
* Редактирование финансов * Редактирование финансов
*/ */
if($request == 'edit_req_finance_info'){ if($request == 'edit_req_finance_info'){
$reqId = (int)$data->req_id; $reqId = (int)$data->req_id;
$userId = (int)$data->user_id; $userId = (int)$data->user_id;
$depositId = 0; $depositId = 0;
@ -2295,8 +2306,233 @@ if($request == 'get_view_page'){
} }
}
// copy_preview — дефолты для модалки копирования (funnel/who_work/base_type_id/warnings).
if($request == 'copy_preview') {
$srcId = isset($data->req_id) ? (int)$data->req_id : 0;
if ($srcId <= 0) {
echo json_encode(array('success' => false, 'error' => 'req_id обязателен'));
exit();
} }
// ВАЖНО: $pdo здесь — MySQL, не Sphinx.
$qSrc = $pdo->query("SELECT * FROM requisitions WHERE id = {$srcId} LIMIT 1");
$src = $pdo->fetch_assoc($qSrc);
if (!$src) {
echo json_encode(array('success' => false, 'error' => 'Заявка не найдена'));
exit();
}
if ((int)$src['deleted'] === 1) {
echo json_encode(array('success' => false, 'error' => 'Заявка удалена'));
exit();
}
// set_user — для checkMenuPermissions внутри user_can_edit_req.
$reqCheck = new Requisitions($pdo);
$userForCheck = new User();
$userForCheck->checkPermissions();
$reqCheck->set_user($userForCheck);
if (!$reqCheck->user_can_edit_req($src, $_SESSION['id'])) {
echo json_encode(array('success' => false, 'error' => 'Нет прав на копирование этой заявки'));
exit();
}
$srcTypeId = (int)$src['type_id'];
$qType = $pdo->query("SELECT id, heir FROM requisitions_type WHERE id = {$srcTypeId} LIMIT 1");
$typeRow = $pdo->fetch_assoc($qType);
$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) {
$qContract = $pdo->query(
"SELECT id FROM contracts_req WHERE req_id = {$srcId} AND type = 2 LIMIT 1"
);
$hasSaleContract = $pdo->fetch_assoc($qContract);
if ($hasSaleContract) {
$warnings[] = 'contract_without_object';
}
}
echo 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,
));
exit();
}
// copy — валидация payload/прав/воронки/ответственного → Requisitions::copy().
if($request == 'copy') {
$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;
$currentId = (int)$_SESSION['id'];
$userAgency = (int)$_SESSION['agency_id'];
// funnel_id = 0 — дефолтная воронка (funnel_default).
if ($srcId <= 0 || $funnelId < 0 || $whoWorkId <= 0) {
echo json_encode(array(
'success' => false,
'error' => 'req_id, funnel_id, who_work_id обязательны',
'code' => 'bad_payload',
));
exit();
}
$qSrc = $pdo->query("SELECT * FROM requisitions WHERE id = {$srcId} LIMIT 1");
$src = $pdo->fetch_assoc($qSrc);
if (!$src) {
echo json_encode(array(
'success' => false,
'error' => 'Заявка не найдена',
'code' => 'src_not_found',
));
exit();
}
if ((int)$src['deleted'] === 1) {
echo json_encode(array(
'success' => false,
'error' => 'Заявка удалена',
'code' => 'src_not_found',
));
exit();
}
$reqCheck = new Requisitions($pdo);
$userForCheck = new User();
$userForCheck->checkPermissions();
$reqCheck->set_user($userForCheck);
// Право на копирование == право на редактирование исходника.
if (!$reqCheck->user_can_edit_req($src, $currentId)) {
echo json_encode(array(
'success' => false,
'error' => 'Нет прав на копирование этой заявки',
'code' => 'no_edit_right',
));
exit();
}
// Доступ к воронке. funnel_id=0 — проверяем funnel_default.is_req.
if ($funnelId === 0) {
$qFunnel = $pdo->query(
"SELECT `is_req` FROM `funnel_default`
WHERE `agency_id` = {$userAgency}
LIMIT 1"
);
$funnelDef = $pdo->fetch_assoc($qFunnel);
if ($funnelDef && (int)$funnelDef['is_req'] === 0) {
echo json_encode(array(
'success' => false,
'error' => 'Воронка недоступна',
'code' => 'funnel_forbidden',
));
exit();
}
} else {
// Проверка доступности воронки — через канонический Funnel::get_funnels(): там корректно учитывается
// is_no_see + раскрытие 'all_X' (подчинённые), users_no_see — это whitelist при is_no_see=1, имя обманчиво.
$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) {
echo json_encode(array(
'success' => false,
'error' => 'Воронка недоступна',
'code' => 'funnel_forbidden',
));
exit();
}
}
// Ответственный. Себя всегда можно назначить.
if ($whoWorkId !== $currentId) {
if ($funnelId === 0) {
// user_can_be_responsible требует funnel_id > 0 — для дефолтной проверяем вручную (agency + blocked).
$candidateAgency = (int)User::getUserAgencyID($whoWorkId);
if ($candidateAgency !== $userAgency) {
echo json_encode(array(
'success' => false,
'error' => 'Выбранный ответственный недоступен',
'code' => 'who_work_forbidden',
));
exit();
}
$blockedQ = mysql_query("SELECT `blocked` FROM `users` WHERE `id` = {$whoWorkId} LIMIT 1");
$blockedRow = mysql_fetch_assoc($blockedQ);
if (!$blockedRow || (int)$blockedRow['blocked'] === 1) {
echo json_encode(array(
'success' => false,
'error' => 'Выбранный ответственный недоступен',
'code' => 'who_work_forbidden',
));
exit();
}
} else if (!$reqCheck->user_can_be_responsible($whoWorkId, $userAgency, $funnelId)) {
echo json_encode(array(
'success' => false,
'error' => 'Выбранный ответственный недоступен',
'code' => 'who_work_forbidden',
));
exit();
}
}
$result = $reqCheck->copy($srcId, $funnelId, $whoWorkId, $currentId);
echo json_encode($result);
exit();
}
if ($request == 'get_requisitions_by_stage') {
$stageId = (int)(isset($data->stage_id) ? $data->stage_id : 0);
$funnelId = (int)(isset($data->funnel_id) ? $data->funnel_id : 0);
$page = max(1, (int)(isset($data->page) ? $data->page : 1));
$search = trim(isset($data->search) ? $data->search : '');
$period = isset($data->period) ? $data->period : '';
$responsible = is_array($data->responsible) ? $data->responsible : [];
$limit = 20;
$offset = ($page - 1) * $limit;
$userId = (int)$_SESSION['id'];
$user = new User($pdo);
$user->getUserById($userId);
$req = new Requisitions($pdo);
$req->set_userId($userId);
$req->set_agencyId($user->agencyId);
$result = $req->getRequisitionsByStage($stageId, $funnelId, $search, $period, $responsible, $offset, $limit);
$html = '';
foreach ($result['items'] as $item) {
$html .= '<li id="kanban_item_li_' . $item['id'] . '" class="kanban__item p' . (isset($item['priority']) ? $item['priority'] : 0) . '">';
$html .= '<div id="client_kanban_' . $item['id'] . '" class="kanban__item-inner">';
$html .= '<div title="' . htmlspecialchars(isset($item['fio']) ? $item['fio'] : 'Без имени') . '" class="title-kanban">';
$html .= '<span>' . htmlspecialchars(isset($item['fio']) ? $item['fio'] : 'Без имени') . '</span>';
$html .= '</div>';
$html .= '</div></li>';
}
echo json_encode([
'html' => $html,
'has_more' => count($result['items']) == $limit,
'page' => $page
]);
exit();
}
if($request == 'get_deal_by_req'){ if($request == 'get_deal_by_req'){
$req_id = intval($data->req_id); $req_id = intval($data->req_id);
$sql = "SELECT id FROM deals WHERE status != 2 AND (side_one_requisition_id = $req_id OR side_two_requisition_id = $req_id) LIMIT 1"; $sql = "SELECT id FROM deals WHERE status != 2 AND (side_one_requisition_id = $req_id OR side_two_requisition_id = $req_id) LIMIT 1";

View File

@ -2,6 +2,31 @@ Vue.config.devtools = true;
Vue.component('v-select', VueSelect.VueSelect); Vue.component('v-select', VueSelect.VueSelect);
// JW-style toast (стили — crm.css). Дубликат из requisitions_vue.js с idempotent-гардом.
if (typeof window.jwShowCopyToast !== 'function') {
window.jwShowCopyToast = function (text) {
var container = document.getElementById('jw-toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'jw-toast-container';
container.className = 'jw-toast-container';
document.body.appendChild(container);
}
var toast = document.createElement('div');
toast.className = 'jw-toast';
toast.textContent = String(text || '');
container.appendChild(toast);
void toast.offsetWidth; // reflow для transition
toast.classList.add('jw-toast-show');
setTimeout(function () {
toast.classList.remove('jw-toast-show');
setTimeout(function () {
if (toast.parentNode) toast.parentNode.removeChild(toast);
}, 250);
}, 3500);
};
}
var requisitions_client = new Vue({ var requisitions_client = new Vue({
el: '#requisitions_list_client', el: '#requisitions_list_client',
name: 'requisitions_list_client', name: 'requisitions_list_client',
@ -564,7 +589,34 @@ var room_requisitions = new Vue({
ot: 0, ot: 0,
do: 0, do: 0,
pagePagin: [], pagePagin: [],
complexBus: null complexBus: null,
// Состояние модалки копирования заявки. employees — серверный пререндер window.__copyEmployeesList; фильтр в Vue (без Select2).
copyModal: {
open: false,
srcId: null,
funnels: [],
funnelId: null,
employees: [],
whoWorkId: null,
whoWorkSearch: '',
whoWorkOpen: false,
whoWorkLabel: '',
defaultWhoWorkId: null,
warnings: [],
submitting: false,
error: null,
},
}
},
computed: {
// Фильтр сотрудников по whoWorkSearch (case-insensitive).
copyFilteredEmployees: function() {
var q = (this.copyModal.whoWorkSearch || '').toLowerCase().trim();
var emps = this.copyModal.employees || [];
if (!q) return emps;
return emps.filter(function(e) {
return (e.name || '').toLowerCase().indexOf(q) !== -1;
});
} }
}, },
methods: methods:
@ -830,6 +882,131 @@ var room_requisitions = new Vue({
this.complexBus = window.complexBus this.complexBus = window.complexBus
} }
}, 200) }, 200)
},
// Открыть модалку копирования. Funnels/employees — серверный пререндер из room_requisitions.php / footer.php.
openCopyRequisitionModal: function(reqId) {
var self = this;
var preFunnels = Array.isArray(window.__copyFunnelsList) ? window.__copyFunnelsList : [];
var preEmployees = Array.isArray(window.__copyEmployeesList) ? window.__copyEmployeesList : [];
var employeesNormalized = preEmployees.map(function (e) {
return {
id: parseInt(e.id, 10),
name: e.name,
group: e.group || null,
};
});
self.copyModal = {
open: true,
srcId: reqId,
funnels: preFunnels.map(function (f) {
return { id: parseInt(f.id, 10), name: f.name };
}),
funnelId: null,
employees: employeesNormalized,
whoWorkId: null,
whoWorkSearch: '',
whoWorkOpen: false,
whoWorkLabel: '',
defaultWhoWorkId: null,
warnings: [],
submitting: false,
error: null,
};
axios.post('/ajax/ajax_vue_requisitions.php', {request: 'copy_preview', req_id: reqId})
.then(function (res) {
if (!res.data.success) {
self.copyModal.error = res.data.error || 'Не удалось загрузить данные';
return;
}
self.copyModal.warnings = res.data.warnings || [];
// Дефолтная воронка — из исходника, иначе первая доступная.
var defaultFunnelId = res.data.default_funnel_id;
var funnelMatch = self.copyModal.funnels.find(function (f) {
return parseInt(f.id, 10) === parseInt(defaultFunnelId, 10);
});
if (funnelMatch) {
self.copyModal.funnelId = parseInt(defaultFunnelId, 10);
} else if (self.copyModal.funnels.length > 0) {
self.copyModal.funnelId = self.copyModal.funnels[0].id;
} else {
self.copyModal.funnelId = null;
}
self.copyModal.defaultWhoWorkId = res.data.default_who_work_id;
var defaultId = res.data.default_who_work_id;
var hasInList = employeesNormalized.some(function (e) { return e.id === parseInt(defaultId, 10); });
var chosenId = hasInList
? parseInt(defaultId, 10)
: (employeesNormalized.length > 0 ? employeesNormalized[0].id : null);
self.copyModal.whoWorkId = chosenId;
// whoWorkLabel — имя в свернутом поле; whoWorkSearch держим пустым (полный список при открытии).
var chosenEmp = employeesNormalized.find(function (e) { return e.id === chosenId; });
self.copyModal.whoWorkLabel = chosenEmp ? chosenEmp.name : '';
self.copyModal.whoWorkSearch = '';
self.copyModal.whoWorkOpen = false;
})
.catch(function () {
self.copyModal.error = 'Сетевая ошибка';
});
},
// Выбор сотрудника. Сбрасываем whoWorkSearch, чтобы следующий раз показать полный список.
selectCopyWhoWork: function(emp) {
this.copyModal.whoWorkId = emp.id;
this.copyModal.whoWorkLabel = emp.name;
this.copyModal.whoWorkSearch = '';
this.copyModal.whoWorkOpen = false;
},
// Отправка копии: создаёт заявку и обновляет список комнаты.
submitCopy: function() {
var self = this;
if (self.copyModal.submitting) return;
var whoWorkId = parseInt(self.copyModal.whoWorkId, 10);
if (isNaN(whoWorkId) || self.copyModal.funnelId === null || self.copyModal.funnelId === undefined) {
self.copyModal.error = 'Заполните воронку и ответственного';
return;
}
self.copyModal.submitting = true;
self.copyModal.error = null;
axios.post('/ajax/ajax_vue_requisitions.php', {
request: 'copy',
req_id: self.copyModal.srcId,
funnel_id: self.copyModal.funnelId,
who_work_id: whoWorkId,
}).then(function (res) {
self.copyModal.submitting = false;
if (!res.data.success) {
self.copyModal.error = res.data.error || 'Не удалось создать копию';
return;
}
self.copyModal.open = false;
if (typeof window.jwShowCopyToast === 'function') {
window.jwShowCopyToast('Копия заявки создана');
}
if (typeof self.getList === 'function') self.getList();
}).catch(function () {
self.copyModal.submitting = false;
self.copyModal.error = 'Сетевая ошибка';
});
},
// Закрыть модалку копирования.
closeCopyModal: function() {
this.copyModal.open = false;
},
// Закрытие picker сотрудников по клику вне него (mousedown, чтобы опередить click внутри).
onCopyModalDocumentClick: function(e) {
if (!this.copyModal.whoWorkOpen) return;
var picker = this.$refs && this.$refs.copyWhoWorkPicker;
if (picker && (picker === e.target || picker.contains(e.target))) return;
this.copyModal.whoWorkOpen = false;
} }
}, },
mounted() mounted()
@ -837,6 +1014,11 @@ var room_requisitions = new Vue({
this.$nextTick(() => { this.$nextTick(() => {
this.observeRefs(); this.observeRefs();
}); });
document.addEventListener('mousedown', this.onCopyModalDocumentClick);
},
beforeDestroy: function()
{
document.removeEventListener('mousedown', this.onCopyModalDocumentClick);
}, },
watch: watch:
{ {
@ -853,7 +1035,8 @@ var room_requisitions = new Vue({
} }
}); });
} }
} },
// Watch 'copyModal.funnelId' удалён: список сотрудников не зависит от воронки, заполняется один раз из window.__copyEmployeesList.
}, },
created: function() created: function()
{ {

View File

@ -3,6 +3,31 @@ Vue.config.devtools = true;
Vue.component('v-select', VueSelect.VueSelect) Vue.component('v-select', VueSelect.VueSelect)
Vue.directive('mask', VueMask.VueMaskDirective); Vue.directive('mask', VueMask.VueMaskDirective);
// JW-style toast (стили — crm.css). Idempotent guard на случай повторной загрузки файла.
if (typeof window.jwShowCopyToast !== 'function') {
window.jwShowCopyToast = function (text) {
var container = document.getElementById('jw-toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'jw-toast-container';
container.className = 'jw-toast-container';
document.body.appendChild(container);
}
var toast = document.createElement('div');
toast.className = 'jw-toast';
toast.textContent = String(text || '');
container.appendChild(toast);
void toast.offsetWidth; // reflow для transition
toast.classList.add('jw-toast-show');
setTimeout(function () {
toast.classList.remove('jw-toast-show');
setTimeout(function () {
if (toast.parentNode) toast.parentNode.removeChild(toast);
}, 250);
}, 3500);
};
}
if ($('#requisitions_list').length > 0) { if ($('#requisitions_list').length > 0) {
var requisitions = new Vue({ var requisitions = new Vue({
el: '#requisitions_list', el: '#requisitions_list',
@ -47,6 +72,33 @@ if ($('#requisitions_list').length > 0) {
add_tag_contract: null, add_tag_contract: null,
req_id_contract_tag: 0, req_id_contract_tag: 0,
prev_selectors: { 'li': null, 'ul': null }, prev_selectors: { 'li': null, 'ul': null },
// Состояние модалки копирования заявки. employees — серверный пререндер window.__copyEmployeesList (см. requisitions.php); фильтр в Vue (без Select2).
copyModal: {
open: false,
srcId: null,
funnels: [],
funnelId: null,
employees: [],
whoWorkId: null,
whoWorkSearch: '',
whoWorkOpen: false,
whoWorkLabel: '',
defaultWhoWorkId: null,
warnings: [],
submitting: false,
error: null,
},
}
},
computed: {
// Фильтр сотрудников по whoWorkSearch (case-insensitive).
copyFilteredEmployees: function() {
var q = (this.copyModal.whoWorkSearch || '').toLowerCase().trim();
var emps = this.copyModal.employees || [];
if (!q) return emps;
return emps.filter(function(e) {
return (e.name || '').toLowerCase().indexOf(q) !== -1;
});
} }
}, },
watch: { watch: {
@ -83,7 +135,8 @@ if ($('#requisitions_list').length > 0) {
this.totalReqs = totalFromSt this.totalReqs = totalFromSt
} }
} },
// Watch 'copyModal.funnelId' удалён: список сотрудников не зависит от воронки, заполняется один раз из window.__copyEmployeesList.
}, },
methods: { methods: {
setAutosearch(reqs) { setAutosearch(reqs) {
@ -1172,11 +1225,146 @@ if ($('#requisitions_list').length > 0) {
// } // }
}, },
// Открыть модалку копирования: preview-запрос → defaults + warnings.
// Списки воронок/сотрудников приходят из window.__copyFunnelsList / window.__copyEmployeesList (server-embed в requisitions.php).
openCopyRequisitionModal: function(reqId) {
var self = this;
var preFunnels = Array.isArray(window.__copyFunnelsList) ? window.__copyFunnelsList : [];
var preEmployees = Array.isArray(window.__copyEmployeesList) ? window.__copyEmployeesList : [];
var employeesNormalized = preEmployees.map(function (e) {
return {
id: parseInt(e.id, 10),
name: e.name,
group: e.group || null,
};
});
// Полное переприсваивание copyModal — чтобы Vue точно зарегистрировал изменения всех полей.
self.copyModal = {
open: true,
srcId: reqId,
funnels: preFunnels.map(function (f) {
return { id: parseInt(f.id, 10), name: f.name };
}),
funnelId: null,
employees: employeesNormalized,
whoWorkId: null,
whoWorkSearch: '',
whoWorkOpen: false,
whoWorkLabel: '',
defaultWhoWorkId: null,
warnings: [],
submitting: false,
error: null,
};
axios.post('/ajax/ajax_vue_requisitions.php', {request: 'copy_preview', req_id: reqId})
.then(function (res) {
if (!res.data.success) {
self.copyModal.error = res.data.error || 'Не удалось загрузить данные';
return;
}
self.copyModal.warnings = res.data.warnings || [];
// Дефолтная воронка — из исходной заявки; если недоступна юзеру — fallback на первую.
var defaultFunnelId = res.data.default_funnel_id;
var funnelMatch = self.copyModal.funnels.find(function (f) {
return parseInt(f.id, 10) === parseInt(defaultFunnelId, 10);
});
if (funnelMatch) {
self.copyModal.funnelId = parseInt(defaultFunnelId, 10);
} else if (self.copyModal.funnels.length > 0) {
self.copyModal.funnelId = self.copyModal.funnels[0].id;
} else {
self.copyModal.funnelId = null;
}
self.copyModal.defaultWhoWorkId = res.data.default_who_work_id;
// Дефолтный ответственный — из preview; иначе первая опция списка.
var defaultId = res.data.default_who_work_id;
var hasInList = employeesNormalized.some(function (e) { return e.id === parseInt(defaultId, 10); });
var chosenId = hasInList
? parseInt(defaultId, 10)
: (employeesNormalized.length > 0 ? employeesNormalized[0].id : null);
self.copyModal.whoWorkId = chosenId;
// whoWorkLabel — отображаемое имя в свернутом поле; whoWorkSearch держим пустым.
var chosenEmp = employeesNormalized.find(function (e) { return e.id === chosenId; });
self.copyModal.whoWorkLabel = chosenEmp ? chosenEmp.name : '';
self.copyModal.whoWorkSearch = '';
self.copyModal.whoWorkOpen = false;
})
.catch(function () {
self.copyModal.error = 'Сетевая ошибка';
});
},
// Выбор сотрудника. Сбрасываем whoWorkSearch — следующий раз показать полный список.
selectCopyWhoWork: function(emp) {
this.copyModal.whoWorkId = emp.id;
this.copyModal.whoWorkLabel = emp.name;
this.copyModal.whoWorkSearch = '';
this.copyModal.whoWorkOpen = false;
},
// Отправка копии: создаёт новую заявку и обновляет список.
submitCopy: function() {
var self = this;
if (self.copyModal.submitting) return;
var whoWorkId = parseInt(self.copyModal.whoWorkId, 10);
// funnel_id может быть 0 («Обычные») — проверяем именно на null/undefined.
// who_work_id может быть 0 («Всё агентство») — проверяем только NaN.
if (isNaN(whoWorkId) || self.copyModal.funnelId === null || self.copyModal.funnelId === undefined) {
self.copyModal.error = 'Заполните воронку и ответственного';
return;
}
self.copyModal.submitting = true;
self.copyModal.error = null;
axios.post('/ajax/ajax_vue_requisitions.php', {
request: 'copy',
req_id: self.copyModal.srcId,
funnel_id: self.copyModal.funnelId,
who_work_id: whoWorkId,
}).then(function (res) {
self.copyModal.submitting = false;
if (!res.data.success) {
self.copyModal.error = res.data.error || 'Не удалось создать копию';
return;
}
self.copyModal.open = false;
if (typeof window.jwShowCopyToast === 'function') {
window.jwShowCopyToast('Копия заявки создана');
}
if (typeof self.getList === 'function') self.getList();
}).catch(function () {
self.copyModal.submitting = false;
self.copyModal.error = 'Сетевая ошибка';
});
},
// Закрыть модалку копирования — никаких Select2 destroy, просто скрываем.
closeCopyModal: function() {
this.copyModal.open = false;
},
// Закрытие picker сотрудников по клику вне него (mousedown, чтобы опередить click внутри).
onCopyModalDocumentClick: function(e) {
if (!this.copyModal.whoWorkOpen) return;
var picker = this.$refs && this.$refs.copyWhoWorkPicker;
if (picker && (picker === e.target || picker.contains(e.target))) return;
this.copyModal.whoWorkOpen = false;
},
}, },
mounted() { mounted() {
if (document.location.pathname == '/requisitions.php') { if (document.location.pathname == '/requisitions.php') {
} }
document.addEventListener('mousedown', this.onCopyModalDocumentClick);
},
beforeDestroy: function() {
// Снимаем document-level слушатель, чтобы не утекал при пересборке инстанса.
document.removeEventListener('mousedown', this.onCopyModalDocumentClick);
}, },
created: function() { created: function() {
//this.getList(); //this.getList();
@ -2763,6 +2951,7 @@ var req_form = new Vue({
} }
}, },
}, },
mounted() { mounted() {