Add is_deal icon

This commit is contained in:
mac 2026-06-20 11:19:54 +03:00
parent e579dd82d7
commit 0e9fb83cc2
2 changed files with 437 additions and 11 deletions

View File

@ -543,7 +543,7 @@
} else {
if ($mstr['agency']) {
$master_work = "агентство $mstr[agency_name]";
$master_work = trim($mstr['last_name'] . ' ' . $mstr['first_name'] . ' ' . $mstr['middle_name']) ?: "агентство {$mstr['agency_name']}";
}
elseif ($mstr['manager']) {
@ -602,7 +602,7 @@
$master_delete = trim($mstr['last_name'] . ' ' . $mstr['first_name'] . ' ' . $mstr['middle_name']);
} else {
if ($mstr['agency']) {
$master_delete = "агентство $mstr[agency_name]";
$master_delete = trim($mstr['last_name'] . ' ' . $mstr['first_name'] . ' ' . $mstr['middle_name']) ?: "агентство {$mstr['agency_name']}";
}
elseif ($mstr['manager']) {
@ -642,9 +642,8 @@
//Общее по одной заявке для канбана
private function get_req_details_kanban($r, $kanban = false, $usersToUse = [], $departments = [], $typesReq = array(), $events_res = array()){
$dateAdd = date("d.m.Y",strtotime($r['created_at']));
list($d, $m, $y) = explode(".", $dateAdd);
$date_add = $d." ".getRusMonth($m)." ".$y;
// Унифицировано с форматом даты задачи: DD.MM.YYYY.
$date_add = date("d.m.Y", strtotime($r['created_at']));
if(!isset($r['funnel_type'])){
$r['funnel_type'] = 'main';
}
@ -848,7 +847,8 @@
];
$type = $row['type'];
$scheduleDate = date('d-m-Y H:i', strtotime($row['schedule_date']));
// Унифицировано с датой заявки: DD.MM.YYYY HH:MM.
$scheduleDate = date('d.m.Y H:i', strtotime($row['schedule_date']));
$taskName = $row['name'];
$bgColor = '#e2e9f5';
$textColor = '#76a7fa';
@ -870,6 +870,7 @@
}
}
$r['is_deal'] = !empty($r['deal_id']) && $r['deal_id'] > 0 ? 1 : 0;
$result = $r;
return $result;
}
@ -935,6 +936,8 @@
$r['no_conf_doer'] = $noConfDoer;
$r['can_see'] =$canSee;
$r['is_deal'] = !empty($r['deal_id']) && $r['deal_id'] > 0 ? 1 : 0;
$r['can_see_other'] = false;
if ($r['can_edit'] && ((int)$r['who_work'] == (int)$this->user->id || (int)$r['user_id'] == (int)$this->user->id))
$r['can_see_other'] = true;
@ -1118,7 +1121,7 @@
$r['no_can_edit_hot'] = 0;
$r['see_modal_client'] = 1;
if ($r_m['agency']) {
$master = "Агентство $r_m[agency_name]";
$master = trim($r_m['last_name'] . ' ' . $r_m['first_name'] . ' ' . $r_m['middle_name']) ?: "Агентство {$r_m['agency_name']}";
} else if ($r_m['manager']) {
$master = "Менеджер " . trim($r_m['last_name'] . ' ' . $r_m['first_name'] . ' ' . $r_m['middle_name']);
if ($r_m['role_id'] > 0) {
@ -1536,7 +1539,7 @@
req.stage, req.deleted, req.cancel, req.priority, req.doer_clients,
req.hot, req.reason, req.denial, req.summa, req.expected_commission, req.activities, req.asfilter_id,
req.source, req.doers, req.doers_confirm, req.employee_id, req.property_type_id,
req.integration, req.call_id, req.friend_client_id, req.confirm_recommendation, req.id as reqid,
req.integration, req.call_id, req.friend_client_id, req.confirm_recommendation, req.deal_id, req.id as reqid,
'main' as funnel_type,
req.complex_room_id $priceSelect
FROM requisitions as req $priceJoin $propertyTypeJoin $join WHERE funnel_id in ($funnelId)";
@ -1547,7 +1550,7 @@
req.stage, req.deleted, req.cancel, req.priority, req.doer_clients,
req.hot, req.reason, req.denial, req.summa, req.expected_commission, req.activities, req.asfilter_id,
req.source, req.doers, req.doers_confirm, req.employee_id, req.property_type_id,
req.integration, req.call_id, 0 as friend_client_id, 0 as confirm_recommendation, req.id as reqid,
req.integration, req.call_id, 0 as friend_client_id, 0 as confirm_recommendation, req.deal_id, req.id as reqid,
'adjacent' as funnel_type,
req.complex_room_id $priceSelect
FROM adjacent_funnels as a
@ -2709,7 +2712,7 @@
req.stage, req.deleted, req.cancel, req.priority, req.doer_clients,
req.hot, req.reason, req.denial, req.summa, req.expected_commission, req.activities, req.asfilter_id,
req.source, req.doers, req.doers_confirm, req.employee_id, req.property_type_id,
req.integration, req.call_id, req.friend_client_id, req.confirm_recommendation, req.id as reqid, 'main' as funnel_type $priceSelect FROM requisitions as req $priceJoin $propertyTypeJoin $join WHERE req.funnel_id=$funnelId $wherePart1 and req.step_id = $step_id";
req.integration, req.call_id, req.friend_client_id, req.confirm_recommendation, req.deal_id, req.id as reqid, 'main' as funnel_type $priceSelect FROM requisitions as req $priceJoin $propertyTypeJoin $join WHERE req.funnel_id=$funnelId $wherePart1 and req.step_id = $step_id";
$sql_2 .= " UNION ";
$sql_2 .= "SELECT req.id, req.name, req.user_id, req.created_at, req.client_id,
req.object_id, req.type_id, a.funnel_id, a.step_id_active as step_id,
@ -2717,7 +2720,7 @@
req.stage, req.deleted, req.cancel, req.priority, req.doer_clients,
req.hot, req.reason, req.denial, req.summa, req.expected_commission, req.activities, req.asfilter_id,
req.source, req.doers, req.doers_confirm, req.employee_id, req.property_type_id,
req.integration, req.call_id, 0 as friend_client_id, 0 as confirm_recommendation, req.id as reqid, 'adjacent' as funnel_type $priceSelect
req.integration, req.call_id, 0 as friend_client_id, 0 as confirm_recommendation, req.deal_id, req.id as reqid, 'adjacent' as funnel_type $priceSelect
FROM adjacent_funnels as a
LEFT JOIN requisitions as req on a.req_id = req.id $priceJoin $propertyTypeJoin $join WHERE a.funnel_id=$funnelId $wherePart2 and a.step_id_active = $step_id";
$sql_2 .= " ORDER BY created_at $order $limitSql";
@ -4316,6 +4319,362 @@
}
}
// Копирование заявки в одной транзакции. Возвращает [success, id, error, code].
public function copy($srcId, $funnelId, $whoWorkId, $currentUserId) {
$srcId = (int)$srcId;
$funnelId = (int)$funnelId;
$whoWorkId = (int)$whoWorkId;
$currentUserId = (int)$currentUserId;
$qSrc = $this->db->query("SELECT * FROM requisitions WHERE id = {$srcId} LIMIT 1");
$src = $this->db->fetch_assoc($qSrc);
if (!$src) {
return array(
'success' => false,
'error' => 'Исходная заявка не найдена',
'code' => 'src_not_found',
'id' => null,
);
}
if ((int)$src['deleted'] === 1) {
return array(
'success' => false,
'error' => 'Нельзя копировать закрытую заявку',
'code' => 'src_deleted',
'id' => null,
);
}
$srcTypeId = (int)$src['type_id'];
$qType = $this->db->query("SELECT id, heir FROM requisitions_type WHERE id = {$srcTypeId} LIMIT 1");
$typeRow = $this->db->fetch_assoc($qType);
$baseTypeId = $typeRow ? requisition_base_type_id($typeRow) : 0;
// funnel_id=0 — дефолтная воронка без funnel_steps; step_id=1 («Новый»).
if ($funnelId === 0) {
$newStepId = 1;
} else {
$qStep = $this->db->query(
"SELECT id FROM funnel_steps WHERE funnel_id = {$funnelId} AND deleted = 0 ORDER BY order_number, branch LIMIT 1"
);
$firstStep = $this->db->fetch_assoc($qStep);
if (!$firstStep) {
return array(
'success' => false,
'error' => 'В выбранной воронке нет этапов',
'code' => 'no_steps',
'id' => null,
);
}
$newStepId = (int)$firstStep['id'];
}
$srcName = isset($src['name']) ? (string)$src['name'] : '';
$newName = (trim($srcName) === '') ? '' : 'Копия: ' . $srcName;
$copyObject = requisition_should_copy_object($baseTypeId);
$newObjectId = $copyObject ? (int)$src['object_id'] : 0;
$copyFilters = requisition_should_copy_search_filters($baseTypeId);
// ВАЖНО: нативный PDO begin/commit (query("BEGIN") глотает PDOException). Даты через NOW() — TZ-смещение.
try {
$this->db->beginTransaction();
$now = date('Y-m-d H:i:s');
$nameSql = mysql_real_escape_string($newName);
$sourceVal = (int)$src['source'];
$hot = (int)$src['hot'];
$priority = (int)$src['priority'];
$clientId = (int)$src['client_id'];
$descriptionSql = mysql_real_escape_string((string)$src['description']);
// Теги читаются из requisitions_activities (junction-таблица), а не из колонки activities.
$insSql = "INSERT INTO requisitions
(user_id, who_work, funnel_id, step_id, stage, name, type_id, source,
priority, hot, client_id, object_id, description, created_at, confirm, deleted, cancel)
VALUES
({$currentUserId}, {$whoWorkId}, {$funnelId}, {$newStepId}, 1,
'{$nameSql}', {$srcTypeId}, {$sourceVal},
{$priority}, {$hot}, {$clientId}, {$newObjectId}, '{$descriptionSql}', NOW(), 1, 0, 0)";
if (!$this->db->query($insSql)) {
throw new Exception('INSERT requisitions завершился ошибкой');
}
$newId = (int)$this->db->insert_id();
if ($newId <= 0) {
throw new Exception('INSERT requisitions не вернул id');
}
// autosearch_filters — копируем для всех типов кроме «Подбор объекта» (base=1).
$srcAsfilterId = isset($src['asfilter_id']) ? (int)$src['asfilter_id'] : 0;
if ($copyFilters && $srcAsfilterId > 0) {
$qFilter = $this->db->query("SELECT * FROM autosearch_filters WHERE id = {$srcAsfilterId} LIMIT 1");
$filterRow = $this->db->fetch_assoc($qFilter);
if ($filterRow) {
unset($filterRow['id']);
if (array_key_exists('deleted', $filterRow)) $filterRow['deleted'] = 0;
if (array_key_exists('archived', $filterRow)) $filterRow['archived'] = 0;
$filterRow['req_id'] = $newId;
$filterRow['created_at'] = $now;
$filterRow['created_by'] = $currentUserId;
$filterRow['updated_at'] = $now;
$filterRow['updated_by'] = $currentUserId;
$cols = array();
$vals = array();
foreach ($filterRow as $col => $val) {
$cols[] = "`{$col}`";
if ($col === 'created_at' || $col === 'updated_at') {
$vals[] = 'NOW()';
} elseif ($val === null) {
$vals[] = 'NULL';
} else {
$vals[] = "'" . mysql_real_escape_string((string)$val) . "'";
}
}
$filterInsSql = "INSERT INTO autosearch_filters (" . implode(',', $cols) . ")"
. " VALUES (" . implode(',', $vals) . ")";
if (!$this->db->query($filterInsSql)) {
throw new Exception('INSERT autosearch_filters завершился ошибкой');
}
$newAsfilterId = (int)$this->db->insert_id();
if ($newAsfilterId <= 0) {
throw new Exception('INSERT autosearch_filters не вернул id');
}
if (!$this->db->query("UPDATE requisitions SET asfilter_id = {$newAsfilterId} WHERE id = {$newId}")) {
throw new Exception('UPDATE requisitions.asfilter_id завершился ошибкой');
}
}
}
$qTags = $this->db->query("SELECT activity_id FROM requisitions_activities WHERE req_id = {$srcId}");
while ($tag = $this->db->fetch_assoc($qTags)) {
$aid = (int)$tag['activity_id'];
if (!$this->db->query("INSERT INTO requisitions_activities SET req_id = {$newId}, activity_id = {$aid}")) {
throw new Exception('INSERT requisitions_activities завершился ошибкой');
}
}
// contracts_req: date_start/date_end — unix-timestamp (int), не DATETIME.
$hasSaleContract = false;
$qContracts = $this->db->query("SELECT * FROM contracts_req WHERE req_id = {$srcId}");
while ($c = $this->db->fetch_assoc($qContracts)) {
$cType = (int)$c['type'];
$cNumber = mysql_real_escape_string((string)$c['number']);
$cStart = (int)$c['date_start'];
$cEnd = (int)$c['date_end'];
$cDays = (int)$c['days'];
$cSend = (int)$c['send'];
$cAgency = (int)$c['agency_id'];
$contractInsSql = "INSERT INTO contracts_req
(req_id, type, number, date_start, date_end, days, send, agency_id)
VALUES
({$newId}, {$cType}, '{$cNumber}', {$cStart}, {$cEnd}, {$cDays}, {$cSend}, {$cAgency})";
if (!$this->db->query($contractInsSql)) {
throw new Exception('INSERT contracts_req завершился ошибкой');
}
if ($cType === 2) {
$hasSaleContract = true;
}
}
// Cross-link contracts_object — только если есть договор type=2 (продажа).
if ($copyObject && $newObjectId > 0 && $hasSaleContract) {
$this->contractInObject($newId, $newObjectId);
}
// copy_created: историческое событие в исходнике (без уведомления). dop_text = id новой заявки.
$sqlEvSrc = "INSERT INTO events_clients
(req_id, event, dop_text, user_id, from_id, date, send_telegramm, send_max, `read`)
VALUES
({$srcId}, 'copy_created', '{$newId}', {$currentUserId}, 0, NOW(), 1, 1, 1)";
if (!$this->db->query($sqlEvSrc)) {
throw new Exception('INSERT events_clients (copy_created) завершился ошибкой');
}
// copy_from: событие в копии. read=1 всегда — копия не передача, поллер не должен показывать модалку «Передача заявки».
// send_telegramm/send_max=0 при копировании на другого сотрудника — cron отдельно шлёт уведомление в TG/MAX.
$skipTgMax = ($whoWorkId === $currentUserId) ? 1 : 0;
$sqlEvNew = "INSERT INTO events_clients
(req_id, event, dop_text, user_id, from_id, date, send_telegramm, send_max, `read`)
VALUES
({$newId}, 'copy_from', '{$srcId}', {$currentUserId}, {$whoWorkId}, NOW(), {$skipTgMax}, {$skipTgMax}, 1)";
if (!$this->db->query($sqlEvNew)) {
throw new Exception('INSERT events_clients (copy_from) завершился ошибкой');
}
$this->db->commit();
return array(
'success' => true,
'id' => $newId,
'error' => null,
'code' => null,
);
} catch (Exception $e) {
// inTransaction() — защита от двойного rollback (DDL-ошибки откатывают сами).
if ($this->db->inTransaction()) {
$this->db->rollBack();
}
return array(
'success' => false,
'error' => 'Ошибка при создании копии: ' . $e->getMessage(),
'code' => 'exception',
'id' => null,
);
}
}
// Право на редактирование/копирование заявки. Без $_SESSION (для cron). TODO: унифицировать с дубликатами в этом классе.
public function user_can_edit_req($srcRow, $userId) {
$userId = (int)$userId;
if ($userId <= 0) {
return false;
}
if (isset($srcRow['who_work']) && (int)$srcRow['who_work'] === $userId) {
return true;
}
$sql = "SELECT u.`agency`, u.`users_admin`, p.`menu_all_submissions_edit`
FROM `users` u
LEFT JOIN `user_permissions` p ON p.`user_id` = u.`id`
WHERE u.`id` = {$userId}
LIMIT 1";
$result = mysql_query($sql);
if (!$result) {
return false;
}
$u = mysql_fetch_assoc($result);
if (!$u) {
return false;
}
if ((int)$u['agency'] === 1
|| (int)$u['users_admin'] === 1
|| (int)$u['menu_all_submissions_edit'] === 1) {
return true;
}
// doers (соисполнители): doers_confirm=0 И userId — ключ в JSON со значением 1.
$doersConfirm = isset($srcRow['doers_confirm']) ? (int)$srcRow['doers_confirm'] : 0;
$doersRaw = isset($srcRow['doers']) ? $srcRow['doers'] : '';
if ($doersConfirm === 0 && $doersRaw !== '') {
$doers = json_decode(html_entity_decode((string)$doersRaw), true);
if (is_array($doers) && isset($doers[$userId]) && (int)$doers[$userId] === 1) {
return true;
}
}
// admin_department / manager_office своего отдела (по users.department_id ответственного).
$whoWorkId = isset($srcRow['who_work']) ? (int)$srcRow['who_work'] : 0;
if ($whoWorkId <= 0) {
return false;
}
$depHelper = new Department();
$callerDep = $depHelper->getDepartment($userId);
if (!is_array($callerDep) || !isset($callerDep['role'])) {
return false;
}
$callerRole = $callerDep['role'];
if ($callerRole === 'admin_department' || $callerRole === 'manager_office') {
$q = mysql_query(
"SELECT u_caller.`department_id` AS caller_dep,
u_responsible.`department_id` AS resp_dep
FROM `users` u_caller
LEFT JOIN `users` u_responsible ON u_responsible.`id` = {$whoWorkId}
WHERE u_caller.`id` = {$userId}
LIMIT 1"
);
$deps = $q ? mysql_fetch_assoc($q) : null;
if (!$deps) {
return false;
}
$callerDepId = (int)$deps['caller_dep'];
$respDepId = (int)$deps['resp_dep'];
if ($callerDepId > 0 && $callerDepId === $respDepId) {
return true;
}
return false;
}
// manager / manager_office_menager в цепочке id_manager ответственного.
if ($callerRole === 'manager' || $callerRole === 'manager_office_menager') {
$qm = mysql_query(
"SELECT u_caller.`id_manager` AS caller_mgr,
u_responsible.`id_manager` AS resp_mgr
FROM `users` u_caller
LEFT JOIN `users` u_responsible ON u_responsible.`id` = {$whoWorkId}
WHERE u_caller.`id` = {$userId}
LIMIT 1"
);
$mgrs = $qm ? mysql_fetch_assoc($qm) : null;
if (!$mgrs) {
return false;
}
$callerMgr = (int)$mgrs['caller_mgr'];
$respMgr = (int)$mgrs['resp_mgr'];
if ($respMgr > 0 && $respMgr === $userId) {
return true;
}
if ($callerMgr > 0 && $callerMgr === $respMgr) {
return true;
}
if ($callerMgr > 0 && $callerMgr === $whoWorkId) {
return true;
}
}
return false;
}
// Может ли $userId быть ответственным: agency-tree + blocked=0 + НЕ в funnel.users_no_see.
public function user_can_be_responsible($userId, $agencyId, $funnelId) {
$userId = (int)$userId;
$agencyId = (int)$agencyId;
$funnelId = (int)$funnelId;
if ($userId <= 0 || $agencyId <= 0 || $funnelId <= 0) {
return false;
}
if ((int)User::getUserAgencyID($userId) !== $agencyId) {
return false;
}
$blockedQ = mysql_query("SELECT `blocked` FROM `users` WHERE `id` = {$userId} LIMIT 1");
$blockedRow = mysql_fetch_assoc($blockedQ);
if (!$blockedRow || (int)$blockedRow['blocked'] === 1) {
return false;
}
$sql = "SELECT `users_no_see` FROM `funnel` WHERE `id` = {$funnelId} LIMIT 1";
$result = mysql_query($sql);
if (!$result) {
return false;
}
$funnelRow = mysql_fetch_assoc($result);
if (!$funnelRow) {
return false;
}
if (!empty($funnelRow['users_no_see'])) {
$hidden = json_decode($funnelRow['users_no_see'], true);
if (is_array($hidden)) {
$hiddenInt = array_map('intval', $hidden);
if (in_array($userId, $hiddenInt, true)) {
return false;
}
}
}
return true;
}
//Изменение воронки
public function updateFunnel($funnel_id, $req_id) {
@ -6525,5 +6884,67 @@ error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE);*/
}
}
public function getRequisitionsByStage($stageId, $funnelId = 0, $search = '', $period = '', $responsible = [], $offset = 0, $limit = 20) {
$sql = "SELECT r.* FROM requisitions r
WHERE r.stage = ? AND r.deleted = 0";
if ($funnelId > 0) {
$sql .= " AND r.funnel_id = ?";
}
if (!empty($search)) {
$sql .= " AND (r.fio LIKE ? OR r.phone LIKE ?)";
}
if (!empty($period)) {
// добавьте свою логику по периоду
}
if (!empty($responsible)) {
$placeholders = str_repeat('?,', count($responsible) - 1) . '?';
$sql .= " AND r.users_id IN ($placeholders)";
}
$sql .= " ORDER BY r.id DESC LIMIT ? OFFSET ?";
$params = [$stageId];
if ($funnelId > 0) $params[] = $funnelId;
if (!empty($search)) {
$like = '%' . $search . '%';
$params[] = $like;
$params[] = $like;
}
if (!empty($responsible)) {
foreach ($responsible as $uid) {
$params[] = (int)$uid;
}
}
$params[] = $limit;
$params[] = $offset;
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return [
'items' => $stmt->fetchAll(PDO::FETCH_ASSOC)
];
}
}
// Базовый тип: 1=Подбор, 2=Реализация, 3=Другие услуги, 4=Спрос. heir != 0 — кастомный наследник.
function requisition_base_type_id($typeRow) {
$heir = isset($typeRow['heir']) ? (int)$typeRow['heir'] : 0;
if ($heir !== 0) {
return $heir;
}
return (int)$typeRow['id'];
}
// object_id копируется только для типов 3 и 4.
function requisition_should_copy_object($baseTypeId) {
return in_array((int)$baseTypeId, [3, 4], true);
}
// Фильтры поиска копируются для всех типов кроме 1 (Подбор).
function requisition_should_copy_search_filters($baseTypeId) {
return (int)$baseTypeId !== 1;
}
?>

View File

@ -1331,6 +1331,11 @@ $t = $endtime - $starttime;
</div>
<div class="actions">
<a v-if="item.is_deal == 1" href="javascript:{};" class="no-border" title="Сделка"
@click="initEditDeal(item.id)">
<span style="color: #43A047 !important;" class="simple-button settings"><i class="fas fa-handshake"></i></span>
</a>
<a v-if="!isEmpty(item.union_req)" href="javascript:{};" :data-id="'req'+item.id"
class="no-border" title="Заявки для объединения" @click="open_union_req_form(item.union_req, item.id)">
<span style="color: #EE6161 !important;" class="simple-button settings"><i class="far fa-clone"></i>