商机详情显示关联信息的回显问题修复
parent
53fba9d991
commit
3a11b86a62
|
|
@ -5,6 +5,7 @@ import com.unis.crm.common.CurrentUserUtils;
|
|||
import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest;
|
||||
import com.unis.crm.dto.opportunity.CreateOpportunityRequest;
|
||||
import com.unis.crm.dto.opportunity.OmsPreSalesOptionDTO;
|
||||
import com.unis.crm.dto.opportunity.OpportunityItemDTO;
|
||||
import com.unis.crm.dto.opportunity.OpportunityMetaDTO;
|
||||
import com.unis.crm.dto.opportunity.OpportunityOverviewDTO;
|
||||
import com.unis.crm.dto.opportunity.PushOpportunityToOmsRequest;
|
||||
|
|
@ -45,6 +46,13 @@ public class OpportunityController {
|
|||
return ApiResponse.success(opportunityService.getOverview(CurrentUserUtils.requireCurrentUserId(userId), keyword, stage));
|
||||
}
|
||||
|
||||
@GetMapping("/{opportunityId}")
|
||||
public ApiResponse<OpportunityItemDTO> getDetail(
|
||||
@RequestHeader("X-User-Id") Long userId,
|
||||
@PathVariable("opportunityId") Long opportunityId) {
|
||||
return ApiResponse.success(opportunityService.getDetail(CurrentUserUtils.requireCurrentUserId(userId), opportunityId));
|
||||
}
|
||||
|
||||
@GetMapping("/oms/pre-sales")
|
||||
public ApiResponse<List<OmsPreSalesOptionDTO>> getOmsPreSalesOptions(@RequestHeader("X-User-Id") Long userId) {
|
||||
return ApiResponse.success(opportunityService.getOmsPreSalesOptions(CurrentUserUtils.requireCurrentUserId(userId)));
|
||||
|
|
|
|||
|
|
@ -28,8 +28,12 @@ public class OpportunityItemDTO {
|
|||
private String source;
|
||||
private Long salesExpansionId;
|
||||
private String salesExpansionName;
|
||||
private String salesExpansionIntent;
|
||||
private Boolean salesExpansionActive;
|
||||
private Long channelExpansionId;
|
||||
private String channelExpansionName;
|
||||
private String channelExpansionIntent;
|
||||
private String channelExpansionEstablishedDate;
|
||||
private Long preSalesId;
|
||||
private String preSalesName;
|
||||
private String competitorName;
|
||||
|
|
@ -214,6 +218,22 @@ public class OpportunityItemDTO {
|
|||
this.salesExpansionName = salesExpansionName;
|
||||
}
|
||||
|
||||
public String getSalesExpansionIntent() {
|
||||
return salesExpansionIntent;
|
||||
}
|
||||
|
||||
public void setSalesExpansionIntent(String salesExpansionIntent) {
|
||||
this.salesExpansionIntent = salesExpansionIntent;
|
||||
}
|
||||
|
||||
public Boolean getSalesExpansionActive() {
|
||||
return salesExpansionActive;
|
||||
}
|
||||
|
||||
public void setSalesExpansionActive(Boolean salesExpansionActive) {
|
||||
this.salesExpansionActive = salesExpansionActive;
|
||||
}
|
||||
|
||||
public Long getChannelExpansionId() {
|
||||
return channelExpansionId;
|
||||
}
|
||||
|
|
@ -230,6 +250,22 @@ public class OpportunityItemDTO {
|
|||
this.channelExpansionName = channelExpansionName;
|
||||
}
|
||||
|
||||
public String getChannelExpansionIntent() {
|
||||
return channelExpansionIntent;
|
||||
}
|
||||
|
||||
public void setChannelExpansionIntent(String channelExpansionIntent) {
|
||||
this.channelExpansionIntent = channelExpansionIntent;
|
||||
}
|
||||
|
||||
public String getChannelExpansionEstablishedDate() {
|
||||
return channelExpansionEstablishedDate;
|
||||
}
|
||||
|
||||
public void setChannelExpansionEstablishedDate(String channelExpansionEstablishedDate) {
|
||||
this.channelExpansionEstablishedDate = channelExpansionEstablishedDate;
|
||||
}
|
||||
|
||||
public Long getPreSalesId() {
|
||||
return preSalesId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ public interface OpportunityMapper {
|
|||
@Param("keyword") String keyword,
|
||||
@Param("stage") String stage);
|
||||
|
||||
@DataScope(tableAlias = "o", ownerColumn = "owner_user_id")
|
||||
OpportunityItemDTO selectOpportunityDetail(
|
||||
@Param("userId") Long userId,
|
||||
@Param("opportunityId") Long opportunityId);
|
||||
|
||||
@DataScope(tableAlias = "o", ownerColumn = "owner_user_id")
|
||||
List<OpportunityFollowUpDTO> selectOpportunityFollowUps(
|
||||
@Param("userId") Long userId,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.unis.crm.service;
|
|||
import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest;
|
||||
import com.unis.crm.dto.opportunity.CreateOpportunityRequest;
|
||||
import com.unis.crm.dto.opportunity.OmsPreSalesOptionDTO;
|
||||
import com.unis.crm.dto.opportunity.OpportunityItemDTO;
|
||||
import com.unis.crm.dto.opportunity.OpportunityMetaDTO;
|
||||
import com.unis.crm.dto.opportunity.OpportunityOverviewDTO;
|
||||
import com.unis.crm.dto.opportunity.PushOpportunityToOmsRequest;
|
||||
|
|
@ -15,6 +16,8 @@ public interface OpportunityService {
|
|||
|
||||
OpportunityOverviewDTO getOverview(Long userId, String keyword, String stage);
|
||||
|
||||
OpportunityItemDTO getDetail(Long userId, Long opportunityId);
|
||||
|
||||
List<OmsPreSalesOptionDTO> getOmsPreSalesOptions(Long userId);
|
||||
|
||||
Long createOpportunity(Long userId, CreateOpportunityRequest request);
|
||||
|
|
|
|||
|
|
@ -91,6 +91,21 @@ public class OpportunityServiceImpl implements OpportunityService {
|
|||
return new OpportunityOverviewDTO(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpportunityItemDTO getDetail(Long userId, Long opportunityId) {
|
||||
if (opportunityId == null || opportunityId <= 0) {
|
||||
throw new BusinessException("商机不存在");
|
||||
}
|
||||
|
||||
OpportunityItemDTO item = opportunityMapper.selectOpportunityDetail(userId, opportunityId);
|
||||
if (item == null) {
|
||||
throw new BusinessException("未找到商机详情");
|
||||
}
|
||||
|
||||
attachFollowUps(userId, List.of(item));
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OmsPreSalesOptionDTO> getOmsPreSalesOptions(Long userId) {
|
||||
if (userId == null || userId <= 0) {
|
||||
|
|
|
|||
|
|
@ -90,8 +90,25 @@
|
|||
coalesce(o.source, '主动开发') as source,
|
||||
o.sales_expansion_id as salesExpansionId,
|
||||
coalesce(se.candidate_name, '') as salesExpansionName,
|
||||
case se.intent_level
|
||||
when 'high' then '高'
|
||||
when 'medium' then '中'
|
||||
when 'low' then '低'
|
||||
else ''
|
||||
end as salesExpansionIntent,
|
||||
case
|
||||
when o.sales_expansion_id is null then null
|
||||
else (se.employment_status = 'active')
|
||||
end as salesExpansionActive,
|
||||
o.channel_expansion_id as channelExpansionId,
|
||||
coalesce(ce.channel_name, '') as channelExpansionName,
|
||||
case ce.intent_level
|
||||
when 'high' then '高'
|
||||
when 'medium' then '中'
|
||||
when 'low' then '低'
|
||||
else ''
|
||||
end as channelExpansionIntent,
|
||||
coalesce(to_char(ce.contact_established_date, 'YYYY-MM-DD'), '') as channelExpansionEstablishedDate,
|
||||
o.pre_sales_id as preSalesId,
|
||||
coalesce(o.pre_sales_name, '') as preSalesName,
|
||||
coalesce(o.competitor_name, '') as competitorName,
|
||||
|
|
@ -177,6 +194,141 @@
|
|||
order by coalesce(o.updated_at, o.created_at) desc, o.id desc
|
||||
</select>
|
||||
|
||||
<select id="selectOpportunityDetail" resultType="com.unis.crm.dto.opportunity.OpportunityItemDTO">
|
||||
select
|
||||
o.id,
|
||||
o.owner_user_id as ownerUserId,
|
||||
o.opportunity_code as code,
|
||||
o.opportunity_name as name,
|
||||
coalesce(c.customer_name, '未填写最终客户') as client,
|
||||
coalesce(u.display_name, '当前用户') as owner,
|
||||
coalesce(
|
||||
to_char(
|
||||
case
|
||||
when opportunity_followup.latest_followup_time is null then o.updated_at
|
||||
else greatest(o.updated_at, opportunity_followup.latest_followup_time)
|
||||
end,
|
||||
'YYYY-MM-DD HH24:MI'
|
||||
),
|
||||
'无'
|
||||
) as updatedAt,
|
||||
coalesce(o.project_location, '') as projectLocation,
|
||||
coalesce(operator_dict.item_value, o.operator_name, '') as operatorCode,
|
||||
coalesce(operator_dict.item_label, nullif(o.operator_name, ''), '') as operatorName,
|
||||
o.amount,
|
||||
to_char(o.expected_close_date, 'YYYY-MM-DD') as date,
|
||||
o.confidence_pct as confidence,
|
||||
coalesce(stage_dict.item_value, o.stage, '') as stageCode,
|
||||
coalesce(
|
||||
stage_dict.item_label,
|
||||
case coalesce(o.stage, 'initial_contact')
|
||||
when 'initial_contact' then '初步沟通'
|
||||
when 'solution_discussion' then '方案交流'
|
||||
when 'bidding' then '招投标'
|
||||
when 'business_negotiation' then '商务谈判'
|
||||
when 'won' then '已成交'
|
||||
when 'lost' then '已放弃'
|
||||
else coalesce(o.stage, '初步沟通')
|
||||
end
|
||||
) as stage,
|
||||
coalesce(o.opportunity_type, '新建') as type,
|
||||
coalesce(o.archived, false) as archived,
|
||||
coalesce(o.pushed_to_oms, false) as pushedToOms,
|
||||
coalesce(o.product_type, 'VDI云桌面') as product,
|
||||
coalesce(o.source, '主动开发') as source,
|
||||
o.sales_expansion_id as salesExpansionId,
|
||||
coalesce(se.candidate_name, '') as salesExpansionName,
|
||||
case se.intent_level
|
||||
when 'high' then '高'
|
||||
when 'medium' then '中'
|
||||
when 'low' then '低'
|
||||
else ''
|
||||
end as salesExpansionIntent,
|
||||
case
|
||||
when o.sales_expansion_id is null then null
|
||||
else (se.employment_status = 'active')
|
||||
end as salesExpansionActive,
|
||||
o.channel_expansion_id as channelExpansionId,
|
||||
coalesce(ce.channel_name, '') as channelExpansionName,
|
||||
case ce.intent_level
|
||||
when 'high' then '高'
|
||||
when 'medium' then '中'
|
||||
when 'low' then '低'
|
||||
else ''
|
||||
end as channelExpansionIntent,
|
||||
coalesce(to_char(ce.contact_established_date, 'YYYY-MM-DD'), '') as channelExpansionEstablishedDate,
|
||||
o.pre_sales_id as preSalesId,
|
||||
coalesce(o.pre_sales_name, '') as preSalesName,
|
||||
coalesce(o.competitor_name, '') as competitorName,
|
||||
coalesce((
|
||||
select
|
||||
case
|
||||
when f.content like '项目最新进展:%' then
|
||||
split_part(split_part(f.content, E'\n', 1), '项目最新进展:', 2)
|
||||
else f.content
|
||||
end
|
||||
from crm_opportunity_followup f
|
||||
where f.opportunity_id = o.id
|
||||
and coalesce(nullif(btrim(f.content), ''), '') <> ''
|
||||
order by f.followup_time desc, f.id desc
|
||||
limit 1
|
||||
), '') as latestProgress,
|
||||
coalesce((
|
||||
select
|
||||
case
|
||||
when coalesce(nullif(btrim(f.next_action), ''), '') <> '' then f.next_action
|
||||
when f.content like '%后续规划:%' then
|
||||
split_part(split_part(f.content, '后续规划:', 2), E'\n', 1)
|
||||
else ''
|
||||
end
|
||||
from crm_opportunity_followup f
|
||||
where f.opportunity_id = o.id
|
||||
and (
|
||||
coalesce(nullif(btrim(f.next_action), ''), '') <> ''
|
||||
or f.content like '%后续规划:%'
|
||||
)
|
||||
order by f.followup_time desc, f.id desc
|
||||
limit 1
|
||||
), '') as nextPlan,
|
||||
coalesce(o.description, '') as notes
|
||||
from crm_opportunity o
|
||||
left join (
|
||||
select
|
||||
f.opportunity_id,
|
||||
max(f.followup_time) as latest_followup_time
|
||||
from crm_opportunity_followup f
|
||||
group by f.opportunity_id
|
||||
) opportunity_followup on opportunity_followup.opportunity_id = o.id
|
||||
left join crm_customer c on c.id = o.customer_id
|
||||
left join sys_user u on u.user_id = o.owner_user_id
|
||||
left join crm_sales_expansion se on se.id = o.sales_expansion_id
|
||||
left join crm_channel_expansion ce on ce.id = o.channel_expansion_id
|
||||
left join sys_dict_item stage_dict
|
||||
on stage_dict.type_code = 'sj_xmjd'
|
||||
and (
|
||||
stage_dict.item_value = o.stage
|
||||
or stage_dict.item_label = case coalesce(o.stage, 'initial_contact')
|
||||
when 'initial_contact' then '初步沟通'
|
||||
when 'solution_discussion' then '方案交流'
|
||||
when 'bidding' then '招投标'
|
||||
when 'business_negotiation' then '商务谈判'
|
||||
when 'won' then '已成交'
|
||||
when 'lost' then '已放弃'
|
||||
else o.stage
|
||||
end
|
||||
)
|
||||
and stage_dict.status = 1
|
||||
and coalesce(stage_dict.is_deleted, 0) = 0
|
||||
left join sys_dict_item operator_dict
|
||||
on operator_dict.type_code = 'sj_yzf'
|
||||
and operator_dict.item_value = o.operator_name
|
||||
and operator_dict.status = 1
|
||||
and coalesce(operator_dict.is_deleted, 0) = 0
|
||||
where 1 = 1
|
||||
and o.id = #{opportunityId}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectOpportunityFollowUps" resultType="com.unis.crm.dto.opportunity.OpportunityFollowUpDTO">
|
||||
select
|
||||
f.id,
|
||||
|
|
|
|||
|
|
@ -381,8 +381,12 @@ export interface OpportunityItem {
|
|||
source?: string;
|
||||
salesExpansionId?: number;
|
||||
salesExpansionName?: string;
|
||||
salesExpansionIntent?: string;
|
||||
salesExpansionActive?: boolean;
|
||||
channelExpansionId?: number;
|
||||
channelExpansionName?: string;
|
||||
channelExpansionIntent?: string;
|
||||
channelExpansionEstablishedDate?: string;
|
||||
preSalesId?: number;
|
||||
preSalesName?: string;
|
||||
competitorName?: string;
|
||||
|
|
@ -1102,6 +1106,10 @@ export async function getOpportunityOverview(keyword?: string, stage?: string) {
|
|||
return request<OpportunityOverview>(`/api/opportunities/overview${query ? `?${query}` : ""}`, undefined, true);
|
||||
}
|
||||
|
||||
export async function getOpportunityDetail(opportunityId: number) {
|
||||
return request<OpportunityItem>(`/api/opportunities/${opportunityId}`, undefined, true);
|
||||
}
|
||||
|
||||
export async function getOpportunityMeta() {
|
||||
return request<OpportunityMeta>("/api/opportunities/meta", undefined, true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
createSalesExpansion,
|
||||
getExpansionCityOptions,
|
||||
getExpansionMeta,
|
||||
getOpportunityDetail,
|
||||
getOpportunityMeta,
|
||||
getOpportunityExpansionOptions,
|
||||
getOpportunityOmsPreSalesOptions,
|
||||
|
|
@ -1451,6 +1452,9 @@ export default function Opportunities() {
|
|||
const [stageFilterOpen, setStageFilterOpen] = useState(false);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [selectedItem, setSelectedItem] = useState<OpportunityItem | null>(null);
|
||||
const [selectedItemDetail, setSelectedItemDetail] = useState<OpportunityItem | null>(null);
|
||||
const [loadingSelectedItemDetail, setLoadingSelectedItemDetail] = useState(false);
|
||||
const [selectedItemDetailError, setSelectedItemDetailError] = useState("");
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [pushConfirmOpen, setPushConfirmOpen] = useState(false);
|
||||
|
|
@ -1553,6 +1557,42 @@ export default function Opportunities() {
|
|||
}
|
||||
}, [location.state, items]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedItem?.id) {
|
||||
setSelectedItemDetail(null);
|
||||
setSelectedItemDetailError("");
|
||||
setLoadingSelectedItemDetail(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setLoadingSelectedItemDetail(true);
|
||||
setSelectedItemDetail(null);
|
||||
setSelectedItemDetailError("");
|
||||
|
||||
void getOpportunityDetail(selectedItem.id)
|
||||
.then((detail) => {
|
||||
if (!cancelled) {
|
||||
setSelectedItemDetail(detail);
|
||||
}
|
||||
})
|
||||
.catch((detailError) => {
|
||||
if (!cancelled) {
|
||||
setSelectedItemDetail(null);
|
||||
setSelectedItemDetailError(detailError instanceof Error ? detailError.message : "加载商机详情失败");
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) {
|
||||
setLoadingSelectedItemDetail(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedItem?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
|
|
@ -1784,7 +1824,8 @@ export default function Opportunities() {
|
|||
setQuickCreateOpen(true);
|
||||
};
|
||||
|
||||
const followUpRecords: OpportunityFollowUp[] = selectedItem?.followUps ?? [];
|
||||
const detailItem = selectedItem ? (selectedItemDetail ?? selectedItem) : null;
|
||||
const followUpRecords: OpportunityFollowUp[] = detailItem?.followUps ?? [];
|
||||
const visibleItems = items.filter((item) => (archiveTab === "active" ? !item.archived : Boolean(item.archived)));
|
||||
const stageFilterOptions = [
|
||||
{ label: "全部", value: "全部" },
|
||||
|
|
@ -1827,21 +1868,31 @@ export default function Opportunities() {
|
|||
),
|
||||
];
|
||||
const activeStageFilterLabel = stageFilterOptions.find((item) => item.value === filter)?.label || "全部";
|
||||
const selectedSalesExpansion = selectedItem?.salesExpansionId
|
||||
? salesExpansionOptions.find((item) => item.id === selectedItem.salesExpansionId) ?? null
|
||||
const selectedSalesExpansion = detailItem?.salesExpansionId
|
||||
? salesExpansionOptions.find((item) => item.id === detailItem.salesExpansionId) ?? null
|
||||
: null;
|
||||
const selectedChannelExpansion = selectedItem?.channelExpansionId
|
||||
? channelExpansionOptions.find((item) => item.id === selectedItem.channelExpansionId) ?? null
|
||||
const selectedChannelExpansion = detailItem?.channelExpansionId
|
||||
? channelExpansionOptions.find((item) => item.id === detailItem.channelExpansionId) ?? null
|
||||
: null;
|
||||
const selectedSalesExpansionName = formatSalesExpansionOptionLabel(selectedSalesExpansion)
|
||||
|| selectedItem?.salesExpansionName
|
||||
|| detailItem?.salesExpansionName
|
||||
|| "";
|
||||
const selectedSalesExpansionIntent = detailItem?.salesExpansionIntent
|
||||
|| selectedSalesExpansion?.intent
|
||||
|| "";
|
||||
const selectedSalesExpansionActive = detailItem?.salesExpansionActive ?? selectedSalesExpansion?.active;
|
||||
const selectedChannelExpansionName = detailItem?.channelExpansionName || selectedChannelExpansion?.name || "";
|
||||
const selectedChannelExpansionIntent = detailItem?.channelExpansionIntent
|
||||
|| selectedChannelExpansion?.intent
|
||||
|| "";
|
||||
const selectedChannelExpansionEstablishedDate = detailItem?.channelExpansionEstablishedDate
|
||||
|| selectedChannelExpansion?.establishedDate
|
||||
|| "";
|
||||
const selectedChannelExpansionName = selectedItem?.channelExpansionName || selectedChannelExpansion?.name || "";
|
||||
const hasSelectedSalesExpansion = Boolean(selectedSalesExpansionName);
|
||||
const hasSelectedChannelExpansion = Boolean(selectedChannelExpansionName);
|
||||
const selectedPreSalesName = selectedItem?.preSalesName || "无";
|
||||
const isSelectedItemOwnedByCurrentUser = Boolean(selectedItem && currentUserId !== undefined && selectedItem.ownerUserId === currentUserId);
|
||||
const isSelectedItemArchived = Boolean(selectedItem?.archived);
|
||||
const selectedPreSalesName = detailItem?.preSalesName || "无";
|
||||
const isSelectedItemOwnedByCurrentUser = Boolean(detailItem && currentUserId !== undefined && detailItem.ownerUserId === currentUserId);
|
||||
const isSelectedItemArchived = Boolean(detailItem?.archived);
|
||||
const canEditSelectedItem = isSelectedItemOwnedByCurrentUser && !isSelectedItemArchived;
|
||||
const canPushSelectedItem = isSelectedItemOwnedByCurrentUser && !isSelectedItemArchived;
|
||||
const operatorMode = resolveOperatorMode(form.operatorName, operatorOptions);
|
||||
|
|
@ -1961,6 +2012,7 @@ export default function Opportunities() {
|
|||
const relatedChannel = item.channelExpansionId
|
||||
? channelExpansionOptions.find((option) => option.id === item.channelExpansionId) ?? null
|
||||
: null;
|
||||
const relatedSalesActive = item.salesExpansionActive ?? relatedSales?.active;
|
||||
const followUpText = (item.followUps ?? [])
|
||||
.map((record) => {
|
||||
const summary = getOpportunityFollowUpSummary(record);
|
||||
|
|
@ -1985,11 +2037,11 @@ export default function Opportunities() {
|
|||
item.amount === null || item.amount === undefined ? "" : `¥${formatAmount(item.amount)}`,
|
||||
normalizeOpportunityExportText(item.date),
|
||||
normalizeOpportunityExportText(item.salesExpansionName || relatedSales?.name),
|
||||
normalizeOpportunityExportText(relatedSales?.intent),
|
||||
relatedSales ? (relatedSales.active ? "是" : "否") : "",
|
||||
normalizeOpportunityExportText(item.salesExpansionIntent || relatedSales?.intent),
|
||||
relatedSalesActive === undefined ? "" : relatedSalesActive ? "是" : "否",
|
||||
normalizeOpportunityExportText(item.channelExpansionName || relatedChannel?.name),
|
||||
normalizeOpportunityExportText(relatedChannel?.intent),
|
||||
normalizeOpportunityExportText(relatedChannel?.establishedDate),
|
||||
normalizeOpportunityExportText(item.channelExpansionIntent || relatedChannel?.intent),
|
||||
normalizeOpportunityExportText(item.channelExpansionEstablishedDate || relatedChannel?.establishedDate),
|
||||
normalizeOpportunityExportText(item.salesExpansionName || relatedSales?.name),
|
||||
normalizeOpportunityExportText(item.preSalesName),
|
||||
normalizeOpportunityExportText(item.competitorName),
|
||||
|
|
@ -2272,6 +2324,7 @@ export default function Opportunities() {
|
|||
if (!selectedItem) {
|
||||
return;
|
||||
}
|
||||
const sourceItem = detailItem ?? selectedItem;
|
||||
if (!isSelectedItemOwnedByCurrentUser) {
|
||||
setError("仅可编辑本人负责的商机");
|
||||
return;
|
||||
|
|
@ -2282,8 +2335,8 @@ export default function Opportunities() {
|
|||
}
|
||||
setError("");
|
||||
setFieldErrors({});
|
||||
setForm(toFormFromItem(selectedItem, effectiveConfidenceOptions));
|
||||
const competitorState = parseCompetitorState(selectedItem.competitorName);
|
||||
setForm(toFormFromItem(sourceItem, effectiveConfidenceOptions));
|
||||
const competitorState = parseCompetitorState(sourceItem.competitorName);
|
||||
setSelectedCompetitors(competitorState.selections);
|
||||
setCustomCompetitorName(competitorState.customName);
|
||||
setEditOpen(true);
|
||||
|
|
@ -2385,6 +2438,7 @@ export default function Opportunities() {
|
|||
if (!selectedItem || pushingOms) {
|
||||
return;
|
||||
}
|
||||
const sourceItem = detailItem ?? selectedItem;
|
||||
if (!isSelectedItemOwnedByCurrentUser) {
|
||||
setError("仅可推送本人负责的商机");
|
||||
return;
|
||||
|
|
@ -2395,14 +2449,14 @@ export default function Opportunities() {
|
|||
}
|
||||
|
||||
setError("");
|
||||
syncPushPreSalesSelection(selectedItem, omsPreSalesOptions);
|
||||
syncPushPreSalesSelection(sourceItem, omsPreSalesOptions);
|
||||
setPushConfirmOpen(true);
|
||||
setLoadingOmsPreSales(true);
|
||||
|
||||
try {
|
||||
const data = await getOpportunityOmsPreSalesOptions();
|
||||
setOmsPreSalesOptions(data);
|
||||
syncPushPreSalesSelection(selectedItem, data);
|
||||
syncPushPreSalesSelection(sourceItem, data);
|
||||
} catch (loadError) {
|
||||
setOmsPreSalesOptions([]);
|
||||
setError(loadError instanceof Error ? loadError.message : "加载售前人员失败");
|
||||
|
|
@ -2874,14 +2928,14 @@ export default function Opportunities() {
|
|||
/>
|
||||
{fieldErrors.opportunityType ? <p className="text-xs text-rose-500">{fieldErrors.opportunityType}</p> : null}
|
||||
</label>
|
||||
{selectedItem ? (
|
||||
{detailItem ? (
|
||||
<>
|
||||
<label className="space-y-2 sm:col-span-2">
|
||||
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">项目最新进展</span>
|
||||
<textarea
|
||||
rows={3}
|
||||
readOnly
|
||||
value={selectedItem.latestProgress || "暂无日报回写进展"}
|
||||
value={detailItem.latestProgress || "暂无日报回写进展"}
|
||||
className="w-full cursor-not-allowed rounded-xl border border-slate-200 bg-slate-100 px-4 py-3 text-sm text-slate-500 outline-none dark:border-slate-800 dark:bg-slate-800/70 dark:text-slate-400"
|
||||
/>
|
||||
<p className="text-xs text-slate-400 dark:text-slate-500">该字段由日报自动回写,基础信息编辑时不可修改。</p>
|
||||
|
|
@ -2891,7 +2945,7 @@ export default function Opportunities() {
|
|||
<textarea
|
||||
rows={3}
|
||||
readOnly
|
||||
value={selectedItem.nextPlan || "暂无日报回写规划"}
|
||||
value={detailItem.nextPlan || "暂无日报回写规划"}
|
||||
className="w-full cursor-not-allowed rounded-xl border border-slate-200 bg-slate-100 px-4 py-3 text-sm text-slate-500 outline-none dark:border-slate-800 dark:bg-slate-800/70 dark:text-slate-400"
|
||||
/>
|
||||
<p className="text-xs text-slate-400 dark:text-slate-500">该字段由日报自动回写,基础信息编辑时不可修改。</p>
|
||||
|
|
@ -2960,7 +3014,7 @@ export default function Opportunities() {
|
|||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{pushConfirmOpen && selectedItem ? (
|
||||
{pushConfirmOpen && detailItem ? (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
|
|
@ -2993,7 +3047,7 @@ export default function Opportunities() {
|
|||
<div className="px-5 py-4 text-sm text-slate-600 dark:text-slate-300 sm:px-6">
|
||||
<div className="crm-form-section">
|
||||
<p className="text-xs text-slate-400 dark:text-slate-500">当前商机</p>
|
||||
<p className="mt-1 font-medium text-slate-900 dark:text-white">{selectedItem.name || selectedItem.code || `#${selectedItem.id}`}</p>
|
||||
<p className="mt-1 font-medium text-slate-900 dark:text-white">{detailItem.name || detailItem.code || `#${detailItem.id}`}</p>
|
||||
</div>
|
||||
<div className="crm-form-section mt-4">
|
||||
<p className="mb-2 text-xs text-slate-400 dark:text-slate-500">售前人员</p>
|
||||
|
|
@ -3040,7 +3094,7 @@ export default function Opportunities() {
|
|||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{selectedItem && (
|
||||
{detailItem && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
|
|
@ -3070,12 +3124,12 @@ export default function Opportunities() {
|
|||
<div className="crm-modal-stack">
|
||||
<div>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-slate-400 dark:text-slate-500">{selectedItem.code || `#${selectedItem.id}`}</span>
|
||||
<span className="text-xs font-medium text-slate-400 dark:text-slate-500">{detailItem.code || `#${detailItem.id}`}</span>
|
||||
</div>
|
||||
<h3 className="line-clamp-1 text-lg font-bold leading-tight text-slate-900 dark:text-white sm:break-anywhere sm:line-clamp-none sm:text-xl">{selectedItem.name || "未命名商机"}</h3>
|
||||
<h3 className="line-clamp-1 text-lg font-bold leading-tight text-slate-900 dark:text-white sm:break-anywhere sm:line-clamp-none sm:text-xl">{detailItem.name || "未命名商机"}</h3>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<span className="crm-pill crm-pill-neutral">{selectedItem.stage || "初步沟通"}</span>
|
||||
<span className={`rounded-full px-2.5 py-1 text-xs font-medium ${getConfidenceColor(selectedItem.confidence)}`}>项目把握度 {getConfidenceLabel(selectedItem.confidence, effectiveConfidenceOptions)}</span>
|
||||
<span className="crm-pill crm-pill-neutral">{detailItem.stage || "初步沟通"}</span>
|
||||
<span className={`rounded-full px-2.5 py-1 text-xs font-medium ${getConfidenceColor(detailItem.confidence)}`}>项目把握度 {getConfidenceLabel(detailItem.confidence, effectiveConfidenceOptions)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -3084,10 +3138,16 @@ export default function Opportunities() {
|
|||
<FileText className="crm-icon-md text-violet-500" />
|
||||
基本信息
|
||||
</h4>
|
||||
{loadingSelectedItemDetail ? (
|
||||
<p className="crm-field-note">正在加载最新商机详情...</p>
|
||||
) : null}
|
||||
{selectedItemDetailError ? (
|
||||
<div className="crm-alert crm-alert-error">{selectedItemDetailError}</div>
|
||||
) : null}
|
||||
<div className="crm-detail-grid text-sm md:grid-cols-2">
|
||||
<DetailItem label="项目地" value={selectedItem.projectLocation || "无"} />
|
||||
<DetailItem label="最终用户" value={selectedItem.client || "无"} icon={<Building className="h-3 w-3" />} />
|
||||
<DetailItem label="运作方" value={selectedItem.operatorName || "无"} />
|
||||
<DetailItem label="项目地" value={detailItem.projectLocation || "无"} />
|
||||
<DetailItem label="最终用户" value={detailItem.client || "无"} icon={<Building className="h-3 w-3" />} />
|
||||
<DetailItem label="运作方" value={detailItem.operatorName || "无"} />
|
||||
{hasSelectedChannelExpansion ? (
|
||||
<DetailItem label="渠道名称" value={selectedChannelExpansionName} icon={<Building className="h-3 w-3" />} />
|
||||
) : null}
|
||||
|
|
@ -3095,30 +3155,30 @@ export default function Opportunities() {
|
|||
<DetailItem label="新华三负责人" value={selectedSalesExpansionName} icon={<User className="h-3 w-3" />} />
|
||||
) : null}
|
||||
<DetailItem label="售前" value={selectedPreSalesName} icon={<User className="h-3 w-3" />} />
|
||||
<DetailItem label="预计金额(元)" value={<span className="text-rose-600 dark:text-rose-400">¥{formatAmount(selectedItem.amount)}</span>} icon={<DollarSign className="h-3 w-3" />} />
|
||||
<DetailItem label="预计下单时间" value={selectedItem.date || "待定"} icon={<Calendar className="h-3 w-3" />} />
|
||||
<DetailItem label="项目把握度" value={getConfidenceLabel(selectedItem.confidence, effectiveConfidenceOptions)} />
|
||||
<DetailItem label="项目阶段" value={selectedItem.stage || "无"} icon={<Activity className="h-3 w-3" />} />
|
||||
<DetailItem label="预计金额(元)" value={<span className="text-rose-600 dark:text-rose-400">¥{formatAmount(detailItem.amount)}</span>} icon={<DollarSign className="h-3 w-3" />} />
|
||||
<DetailItem label="预计下单时间" value={detailItem.date || "待定"} icon={<Calendar className="h-3 w-3" />} />
|
||||
<DetailItem label="项目把握度" value={getConfidenceLabel(detailItem.confidence, effectiveConfidenceOptions)} />
|
||||
<DetailItem label="项目阶段" value={detailItem.stage || "无"} icon={<Activity className="h-3 w-3" />} />
|
||||
<DetailItem
|
||||
label="是否已推送OMS"
|
||||
value={(
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex rounded-full px-2.5 py-1 text-xs font-semibold",
|
||||
selectedItem.pushedToOms
|
||||
detailItem.pushedToOms
|
||||
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-300"
|
||||
: "bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-300",
|
||||
)}
|
||||
>
|
||||
{selectedItem.pushedToOms ? "已推送" : "未推送"}
|
||||
{detailItem.pushedToOms ? "已推送" : "未推送"}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
<DetailItem label="竞争对手" value={selectedItem.competitorName || "无"} />
|
||||
<DetailItem label="建设类型" value={selectedItem.type || "新建"} icon={<Tag className="h-3 w-3" />} />
|
||||
<DetailItem label="项目最新进展" value={selectedItem.latestProgress || "暂无日报回写进展"} className="md:col-span-2" />
|
||||
<DetailItem label="后续规划" value={selectedItem.nextPlan || "暂无日报回写规划"} className="md:col-span-2" />
|
||||
<DetailItem label="备注说明" value={selectedItem.notes || "无"} className="md:col-span-2" />
|
||||
<DetailItem label="竞争对手" value={detailItem.competitorName || "无"} />
|
||||
<DetailItem label="建设类型" value={detailItem.type || "新建"} icon={<Tag className="h-3 w-3" />} />
|
||||
<DetailItem label="项目最新进展" value={detailItem.latestProgress || "暂无日报回写进展"} className="md:col-span-2" />
|
||||
<DetailItem label="后续规划" value={detailItem.nextPlan || "暂无日报回写规划"} className="md:col-span-2" />
|
||||
<DetailItem label="备注说明" value={detailItem.notes || "无"} className="md:col-span-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -3163,8 +3223,8 @@ export default function Opportunities() {
|
|||
selectedSalesExpansion || selectedSalesExpansionName ? (
|
||||
<div className="crm-detail-grid text-sm sm:grid-cols-3">
|
||||
<DetailItem label="姓名" value={selectedSalesExpansion?.name || selectedSalesExpansionName || "无"} />
|
||||
<DetailItem label="合作意向" value={selectedSalesExpansion?.intent || "无"} />
|
||||
<DetailItem label="是否在职" value={selectedSalesExpansion ? (selectedSalesExpansion.active ? "是" : "否") : "待同步"} />
|
||||
<DetailItem label="合作意向" value={selectedSalesExpansionIntent || "无"} />
|
||||
<DetailItem label="是否在职" value={selectedSalesExpansionActive === undefined ? "待同步" : selectedSalesExpansionActive ? "是" : "否"} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="crm-empty-panel">
|
||||
|
|
@ -3177,8 +3237,8 @@ export default function Opportunities() {
|
|||
selectedChannelExpansion || selectedChannelExpansionName ? (
|
||||
<div className="crm-detail-grid text-sm sm:grid-cols-3">
|
||||
<DetailItem label="渠道名称" value={selectedChannelExpansion?.name || selectedChannelExpansionName || "无"} />
|
||||
<DetailItem label="合作意向" value={selectedChannelExpansion?.intent || "无"} />
|
||||
<DetailItem label="建立联系时间" value={selectedChannelExpansion?.establishedDate || "无"} />
|
||||
<DetailItem label="合作意向" value={selectedChannelExpansionIntent || "无"} />
|
||||
<DetailItem label="建立联系时间" value={selectedChannelExpansionEstablishedDate || "无"} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="crm-empty-panel">
|
||||
|
|
@ -3253,7 +3313,7 @@ export default function Opportunities() {
|
|||
? !isSelectedItemOwnedByCurrentUser
|
||||
? "仅本人可操作"
|
||||
: "已签单不可推送"
|
||||
: selectedItem.pushedToOms
|
||||
: detailItem.pushedToOms
|
||||
? "重新推送 OMS"
|
||||
: "推送 OMS"
|
||||
}
|
||||
|
|
@ -3270,7 +3330,7 @@ export default function Opportunities() {
|
|||
: "已签单不可推送"
|
||||
: pushingOms
|
||||
? "推送中..."
|
||||
: selectedItem.pushedToOms
|
||||
: detailItem.pushedToOms
|
||||
? "重新推送 OMS"
|
||||
: "推送 OMS"}
|
||||
</button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue