修改商机字段展示与展示标签
parent
9733de9f22
commit
2709171839
|
|
@ -91,6 +91,7 @@
|
|||
<workItem from="1775198320186" duration="1451000" />
|
||||
<workItem from="1775200047785" duration="3902000" />
|
||||
<workItem from="1775541962012" duration="805000" />
|
||||
<workItem from="1775611219527" duration="1273000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="修改定位信息 0323">
|
||||
<option name="closed" value="true" />
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ public class OpportunityMetaDTO {
|
|||
private List<OpportunityDictOptionDTO> operatorOptions;
|
||||
private List<OpportunityDictOptionDTO> projectLocationOptions;
|
||||
private List<OpportunityDictOptionDTO> opportunityTypeOptions;
|
||||
private List<OpportunityDictOptionDTO> confidenceOptions;
|
||||
|
||||
public OpportunityMetaDTO() {
|
||||
}
|
||||
|
|
@ -16,11 +17,13 @@ public class OpportunityMetaDTO {
|
|||
List<OpportunityDictOptionDTO> stageOptions,
|
||||
List<OpportunityDictOptionDTO> operatorOptions,
|
||||
List<OpportunityDictOptionDTO> projectLocationOptions,
|
||||
List<OpportunityDictOptionDTO> opportunityTypeOptions) {
|
||||
List<OpportunityDictOptionDTO> opportunityTypeOptions,
|
||||
List<OpportunityDictOptionDTO> confidenceOptions) {
|
||||
this.stageOptions = stageOptions;
|
||||
this.operatorOptions = operatorOptions;
|
||||
this.projectLocationOptions = projectLocationOptions;
|
||||
this.opportunityTypeOptions = opportunityTypeOptions;
|
||||
this.confidenceOptions = confidenceOptions;
|
||||
}
|
||||
|
||||
public List<OpportunityDictOptionDTO> getStageOptions() {
|
||||
|
|
@ -54,4 +57,12 @@ public class OpportunityMetaDTO {
|
|||
public void setOpportunityTypeOptions(List<OpportunityDictOptionDTO> opportunityTypeOptions) {
|
||||
this.opportunityTypeOptions = opportunityTypeOptions;
|
||||
}
|
||||
|
||||
public List<OpportunityDictOptionDTO> getConfidenceOptions() {
|
||||
return confidenceOptions;
|
||||
}
|
||||
|
||||
public void setConfidenceOptions(List<OpportunityDictOptionDTO> confidenceOptions) {
|
||||
this.confidenceOptions = confidenceOptions;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ public interface OpportunityMapper {
|
|||
@Param("typeCode") String typeCode,
|
||||
@Param("itemValue") String itemValue);
|
||||
|
||||
String selectDictValueByLabel(
|
||||
@Param("typeCode") String typeCode,
|
||||
@Param("itemLabel") String itemLabel);
|
||||
|
||||
@DataScope(tableAlias = "o", ownerColumn = "owner_user_id")
|
||||
List<OpportunityItemDTO> selectOpportunities(
|
||||
@Param("userId") Long userId,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ public class OpportunityServiceImpl implements OpportunityService {
|
|||
private static final String STAGE_TYPE_CODE = "sj_xmjd";
|
||||
private static final String OPERATOR_TYPE_CODE = "sj_yzf";
|
||||
private static final String OPPORTUNITY_TYPE_CODE = "sj_jslx";
|
||||
private static final String CONFIDENCE_TYPE_CODE = "sj_xmbwd";
|
||||
private static final Logger log = LoggerFactory.getLogger(OpportunityServiceImpl.class);
|
||||
|
||||
private final OpportunityMapper opportunityMapper;
|
||||
|
|
@ -48,11 +49,13 @@ public class OpportunityServiceImpl implements OpportunityService {
|
|||
@Override
|
||||
public OpportunityMetaDTO getMeta() {
|
||||
List<OpportunityDictOptionDTO> opportunityTypeOptions = opportunityMapper.selectDictItems(OPPORTUNITY_TYPE_CODE);
|
||||
List<OpportunityDictOptionDTO> confidenceOptions = opportunityMapper.selectDictItems(CONFIDENCE_TYPE_CODE);
|
||||
return new OpportunityMetaDTO(
|
||||
opportunityMapper.selectDictItems(STAGE_TYPE_CODE),
|
||||
opportunityMapper.selectDictItems(OPERATOR_TYPE_CODE),
|
||||
opportunityMapper.selectProvinceAreaOptions(),
|
||||
opportunityTypeOptions.isEmpty() ? buildDefaultOpportunityTypeOptions() : opportunityTypeOptions);
|
||||
opportunityTypeOptions.isEmpty() ? buildDefaultOpportunityTypeOptions() : opportunityTypeOptions,
|
||||
confidenceOptions.isEmpty() ? buildDefaultConfidenceOptions() : confidenceOptions);
|
||||
}
|
||||
|
||||
private List<OpportunityDictOptionDTO> buildDefaultOpportunityTypeOptions() {
|
||||
|
|
@ -62,6 +65,13 @@ public class OpportunityServiceImpl implements OpportunityService {
|
|||
buildDictOption("替换", "替换"));
|
||||
}
|
||||
|
||||
private List<OpportunityDictOptionDTO> buildDefaultConfidenceOptions() {
|
||||
return List.of(
|
||||
buildDictOption("A", "A"),
|
||||
buildDictOption("B", "B"),
|
||||
buildDictOption("C", "C"));
|
||||
}
|
||||
|
||||
private OpportunityDictOptionDTO buildDictOption(String label, String value) {
|
||||
OpportunityDictOptionDTO option = new OpportunityDictOptionDTO();
|
||||
option.setLabel(label);
|
||||
|
|
@ -488,13 +498,44 @@ public class OpportunityServiceImpl implements OpportunityService {
|
|||
throw new BusinessException(blankMessage);
|
||||
}
|
||||
|
||||
String upper = normalized.toUpperCase();
|
||||
String dictValue = opportunityMapper.selectDictValueByLabel(CONFIDENCE_TYPE_CODE, normalized);
|
||||
if (!isBlank(dictValue)) {
|
||||
return dictValue;
|
||||
}
|
||||
|
||||
String dictLabel = opportunityMapper.selectDictLabel(CONFIDENCE_TYPE_CODE, normalized);
|
||||
if (!isBlank(dictLabel)) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
String legacyGrade = normalizeLegacyConfidenceGrade(normalized);
|
||||
if (legacyGrade != null) {
|
||||
String legacyDictValue = opportunityMapper.selectDictValueByLabel(CONFIDENCE_TYPE_CODE, legacyGrade);
|
||||
if (!isBlank(legacyDictValue)) {
|
||||
return legacyDictValue;
|
||||
}
|
||||
String legacyDictLabel = opportunityMapper.selectDictLabel(CONFIDENCE_TYPE_CODE, legacyGrade);
|
||||
if (!isBlank(legacyDictLabel)) {
|
||||
return legacyGrade;
|
||||
}
|
||||
return legacyGrade;
|
||||
}
|
||||
|
||||
throw new BusinessException("项目把握度无效: " + normalized);
|
||||
}
|
||||
|
||||
private String normalizeLegacyConfidenceGrade(String value) {
|
||||
if (isBlank(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String upper = value.trim().toUpperCase();
|
||||
if ("A".equals(upper) || "B".equals(upper) || "C".equals(upper)) {
|
||||
return upper;
|
||||
}
|
||||
|
||||
if (normalized.matches("\\d+(\\.\\d+)?")) {
|
||||
double numericValue = Double.parseDouble(normalized);
|
||||
if (upper.matches("\\d+(\\.\\d+)?")) {
|
||||
double numericValue = Double.parseDouble(upper);
|
||||
if (numericValue >= 80) {
|
||||
return "A";
|
||||
}
|
||||
|
|
@ -503,8 +544,7 @@ public class OpportunityServiceImpl implements OpportunityService {
|
|||
}
|
||||
return "C";
|
||||
}
|
||||
|
||||
throw new BusinessException("项目把握度仅支持A、B、C");
|
||||
return null;
|
||||
}
|
||||
|
||||
private BigDecimal requirePositiveAmount(BigDecimal value, String message) {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ unisbase:
|
|||
access-token-safety-seconds: 120
|
||||
oms:
|
||||
enabled: ${OMS_ENABLED:true}
|
||||
base-url: ${OMS_BASE_URL:http://121.41.238.146:28080}
|
||||
base-url: ${OMS_BASE_URL:https://oms.unissense.top}
|
||||
api-key: ${OMS_API_KEY:c7f858d0-30b8-4b7f-9ea1-0ccf5ceb1c54}
|
||||
api-key-header: ${OMS_API_KEY_HEADER:apiKey}
|
||||
user-info-path: ${OMS_USER_INFO_PATH:/api/v1/user/info}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,17 @@
|
|||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectDictValueByLabel" resultType="java.lang.String">
|
||||
select item_value
|
||||
from sys_dict_item
|
||||
where type_code = #{typeCode}
|
||||
and item_label = #{itemLabel}
|
||||
and status = 1
|
||||
and coalesce(is_deleted, 0) = 0
|
||||
order by sort_order asc nulls last, dict_item_id asc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectOpportunities" resultType="com.unis.crm.dto.opportunity.OpportunityItemDTO">
|
||||
select
|
||||
o.id,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { AdaptiveSelect } from "@/components/AdaptiveSelect";
|
|||
import { useIsWecomBrowser } from "@/hooks/useIsWecomBrowser";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const CONFIDENCE_OPTIONS = [
|
||||
const FALLBACK_CONFIDENCE_OPTIONS = [
|
||||
{ value: "A", label: "A" },
|
||||
{ value: "B", label: "B" },
|
||||
{ value: "C", label: "C" },
|
||||
|
|
@ -19,8 +19,6 @@ const FALLBACK_OPPORTUNITY_TYPE_OPTIONS = [
|
|||
{ value: "替换", label: "替换" },
|
||||
] as const;
|
||||
|
||||
type ConfidenceGrade = (typeof CONFIDENCE_OPTIONS)[number]["value"];
|
||||
|
||||
const COMPETITOR_OPTIONS = [
|
||||
"深信服",
|
||||
"锐捷",
|
||||
|
|
@ -159,7 +157,11 @@ function matchesOpportunityRelationFilter(hasRelation: boolean, filterValue?: st
|
|||
return true;
|
||||
}
|
||||
|
||||
function matchesOpportunityExportFilters(item: OpportunityItem, filters: OpportunityExportFilters) {
|
||||
function matchesOpportunityExportFilters(
|
||||
item: OpportunityItem,
|
||||
filters: OpportunityExportFilters,
|
||||
confidenceOptions: OpportunityDictOption[],
|
||||
) {
|
||||
const keywordText = [
|
||||
item.code,
|
||||
item.name,
|
||||
|
|
@ -188,7 +190,7 @@ function matchesOpportunityExportFilters(item: OpportunityItem, filters: Opportu
|
|||
if (!matchesOpportunityTextFilter([item.stageCode, item.stage], filters.stage)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.confidence && normalizeConfidenceGrade(item.confidence) !== filters.confidence) {
|
||||
if (filters.confidence && normalizeConfidenceValue(item.confidence, confidenceOptions) !== filters.confidence) {
|
||||
return false;
|
||||
}
|
||||
if (!matchesOpportunityTextFilter([item.projectLocation], filters.projectLocation)) {
|
||||
|
|
@ -206,14 +208,14 @@ function matchesOpportunityExportFilters(item: OpportunityItem, filters: Opportu
|
|||
return matchesOpportunityRelationFilter(Boolean(item.channelExpansionId || item.channelExpansionName), filters.hasChannelExpansion);
|
||||
}
|
||||
|
||||
function toFormFromItem(item: OpportunityItem): CreateOpportunityPayload {
|
||||
function toFormFromItem(item: OpportunityItem, confidenceOptions: OpportunityDictOption[]): CreateOpportunityPayload {
|
||||
return {
|
||||
opportunityName: item.name || "",
|
||||
customerName: item.client || "",
|
||||
projectLocation: item.projectLocation || "",
|
||||
amount: item.amount || 0,
|
||||
expectedCloseDate: item.date || "",
|
||||
confidencePct: normalizeConfidenceGrade(item.confidence),
|
||||
confidencePct: normalizeConfidenceValue(item.confidence, confidenceOptions),
|
||||
stage: item.stageCode || item.stage || "",
|
||||
opportunityType: item.type || "",
|
||||
productType: item.product || "VDI云桌面",
|
||||
|
|
@ -226,9 +228,9 @@ function toFormFromItem(item: OpportunityItem): CreateOpportunityPayload {
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeConfidenceGrade(value?: string | number | null): ConfidenceGrade {
|
||||
function normalizeLegacyConfidenceGrade(value?: string | number | null) {
|
||||
if (value === null || value === undefined) {
|
||||
return "C";
|
||||
return "";
|
||||
}
|
||||
|
||||
if (typeof value === "number") {
|
||||
|
|
@ -247,22 +249,62 @@ function normalizeConfidenceGrade(value?: string | number | null): ConfidenceGra
|
|||
if (normalized === "60") {
|
||||
return "B";
|
||||
}
|
||||
return "C";
|
||||
return "";
|
||||
}
|
||||
|
||||
function getConfidenceOptionValue(score?: string | number | null) {
|
||||
return normalizeConfidenceGrade(score);
|
||||
function getEffectiveConfidenceOptions(confidenceOptions: OpportunityDictOption[]) {
|
||||
return confidenceOptions.length > 0 ? confidenceOptions : [...FALLBACK_CONFIDENCE_OPTIONS];
|
||||
}
|
||||
|
||||
function getConfidenceLabel(score?: string | number | null) {
|
||||
const matchedOption = CONFIDENCE_OPTIONS.find((item) => item.value === normalizeConfidenceGrade(score));
|
||||
return matchedOption?.label || "C";
|
||||
function normalizeConfidenceValue(score: string | number | null | undefined, confidenceOptions: OpportunityDictOption[]) {
|
||||
const rawValue = typeof score === "string" ? score.trim() : typeof score === "number" ? String(score) : "";
|
||||
const options = getEffectiveConfidenceOptions(confidenceOptions);
|
||||
|
||||
if (!rawValue) {
|
||||
return "";
|
||||
}
|
||||
|
||||
function getConfidenceBadgeClass(score?: string | number | null) {
|
||||
const normalizedGrade = normalizeConfidenceGrade(score);
|
||||
if (normalizedGrade === "A") return "crm-pill crm-pill-emerald";
|
||||
if (normalizedGrade === "B") return "crm-pill crm-pill-amber";
|
||||
const matchedByValue = options.find((item) => (item.value || "").trim() === rawValue);
|
||||
if (matchedByValue?.value) {
|
||||
return matchedByValue.value;
|
||||
}
|
||||
|
||||
const matchedByLabel = options.find((item) => (item.label || "").trim() === rawValue);
|
||||
if (matchedByLabel?.value) {
|
||||
return matchedByLabel.value;
|
||||
}
|
||||
|
||||
const legacyGrade = normalizeLegacyConfidenceGrade(score);
|
||||
if (!legacyGrade) {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
const legacyValueMatch = options.find((item) => (item.value || "").trim().toUpperCase() === legacyGrade);
|
||||
if (legacyValueMatch?.value) {
|
||||
return legacyValueMatch.value;
|
||||
}
|
||||
|
||||
const legacyLabelMatch = options.find((item) => (item.label || "").trim().toUpperCase() === legacyGrade);
|
||||
if (legacyLabelMatch?.value) {
|
||||
return legacyLabelMatch.value;
|
||||
}
|
||||
|
||||
return legacyGrade;
|
||||
}
|
||||
|
||||
function getConfidenceLabel(score: string | number | null | undefined, confidenceOptions: OpportunityDictOption[]) {
|
||||
const normalizedValue = normalizeConfidenceValue(score, confidenceOptions);
|
||||
const matchedOption = getEffectiveConfidenceOptions(confidenceOptions).find((item) => (item.value || "") === normalizedValue);
|
||||
return matchedOption?.label || matchedOption?.value || normalizedValue || "无";
|
||||
}
|
||||
|
||||
function getConfidenceBadgeClass(score: string | number | null | undefined, confidenceOptions: OpportunityDictOption[]) {
|
||||
const options = getEffectiveConfidenceOptions(confidenceOptions);
|
||||
const normalizedValue = normalizeConfidenceValue(score, confidenceOptions);
|
||||
const matchedIndex = options.findIndex((item) => (item.value || "") === normalizedValue);
|
||||
if (matchedIndex < 0) return "crm-pill crm-pill-rose";
|
||||
if (matchedIndex === 0) return "crm-pill crm-pill-emerald";
|
||||
if (matchedIndex === 1) return "crm-pill crm-pill-amber";
|
||||
return "crm-pill crm-pill-rose";
|
||||
}
|
||||
|
||||
|
|
@ -553,6 +595,7 @@ function OpportunityExportFilterModal({
|
|||
exportError,
|
||||
archiveTab,
|
||||
stageOptions,
|
||||
confidenceOptions,
|
||||
projectLocationOptions,
|
||||
opportunityTypeOptions,
|
||||
operatorOptions,
|
||||
|
|
@ -564,6 +607,7 @@ function OpportunityExportFilterModal({
|
|||
exportError: string;
|
||||
archiveTab: OpportunityArchiveTab;
|
||||
stageOptions: OpportunityDictOption[];
|
||||
confidenceOptions: OpportunityDictOption[];
|
||||
projectLocationOptions: OpportunityDictOption[];
|
||||
opportunityTypeOptions: OpportunityDictOption[];
|
||||
operatorOptions: OpportunityDictOption[];
|
||||
|
|
@ -673,7 +717,10 @@ function OpportunityExportFilterModal({
|
|||
value={draftFilters.confidence ?? ""}
|
||||
options={[
|
||||
{ value: "", label: "全部把握度" },
|
||||
...CONFIDENCE_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
|
||||
...getEffectiveConfidenceOptions(confidenceOptions).map((option) => ({
|
||||
value: option.value || "",
|
||||
label: option.label || option.value || "",
|
||||
})),
|
||||
]}
|
||||
placeholder="全部把握度"
|
||||
sheetTitle="选择项目把握度"
|
||||
|
|
@ -1315,6 +1362,7 @@ export default function Opportunities() {
|
|||
const [operatorOptions, setOperatorOptions] = useState<OpportunityDictOption[]>([]);
|
||||
const [projectLocationOptions, setProjectLocationOptions] = useState<OpportunityDictOption[]>([]);
|
||||
const [opportunityTypeOptions, setOpportunityTypeOptions] = useState<OpportunityDictOption[]>([]);
|
||||
const [confidenceOptions, setConfidenceOptions] = useState<OpportunityDictOption[]>([]);
|
||||
const [form, setForm] = useState<CreateOpportunityPayload>(defaultForm);
|
||||
const [pushPreSalesId, setPushPreSalesId] = useState<number | undefined>(undefined);
|
||||
const [pushPreSalesName, setPushPreSalesName] = useState("");
|
||||
|
|
@ -1384,6 +1432,7 @@ export default function Opportunities() {
|
|||
setOperatorOptions((data.operatorOptions ?? []).filter((item) => item.value));
|
||||
setProjectLocationOptions((data.projectLocationOptions ?? []).filter((item) => item.value));
|
||||
setOpportunityTypeOptions((data.opportunityTypeOptions ?? []).filter((item) => item.value));
|
||||
setConfidenceOptions((data.confidenceOptions ?? []).filter((item) => item.value));
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
|
|
@ -1391,6 +1440,7 @@ export default function Opportunities() {
|
|||
setOperatorOptions([]);
|
||||
setProjectLocationOptions([]);
|
||||
setOpportunityTypeOptions([]);
|
||||
setConfidenceOptions([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1419,6 +1469,21 @@ export default function Opportunities() {
|
|||
setForm((current) => (current.opportunityType ? current : { ...current, opportunityType: defaultOpportunityType }));
|
||||
}, [opportunityTypeOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
const effectiveConfidenceOptions = getEffectiveConfidenceOptions(confidenceOptions);
|
||||
if (!effectiveConfidenceOptions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultConfidence = effectiveConfidenceOptions[0]?.value || "";
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
confidencePct: current.confidencePct
|
||||
? normalizeConfidenceValue(current.confidencePct, effectiveConfidenceOptions)
|
||||
: defaultConfidence,
|
||||
}));
|
||||
}, [confidenceOptions]);
|
||||
|
||||
const followUpRecords: OpportunityFollowUp[] = selectedItem?.followUps ?? [];
|
||||
const visibleItems = items.filter((item) => (archiveTab === "active" ? !item.archived : Boolean(item.archived)));
|
||||
const stageFilterOptions = [
|
||||
|
|
@ -1443,6 +1508,11 @@ export default function Opportunities() {
|
|||
const effectiveOpportunityTypeOptions = opportunityTypeOptions.length > 0
|
||||
? opportunityTypeOptions
|
||||
: FALLBACK_OPPORTUNITY_TYPE_OPTIONS;
|
||||
const effectiveConfidenceOptions = getEffectiveConfidenceOptions(confidenceOptions);
|
||||
const buildEmptyForm = (): CreateOpportunityPayload => ({
|
||||
...defaultForm,
|
||||
confidencePct: effectiveConfidenceOptions[0]?.value || defaultForm.confidencePct,
|
||||
});
|
||||
const opportunityTypeSelectOptions = [
|
||||
{ value: "", label: "请选择" },
|
||||
...effectiveOpportunityTypeOptions.map((item) => ({
|
||||
|
|
@ -1510,9 +1580,11 @@ export default function Opportunities() {
|
|||
}, [archiveTab, selectedItem, visibleItems]);
|
||||
|
||||
const getConfidenceColor = (score?: string | number | null) => {
|
||||
const normalizedGrade = normalizeConfidenceGrade(score);
|
||||
if (normalizedGrade === "A") return "text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20";
|
||||
if (normalizedGrade === "B") return "text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 border-amber-200 dark:border-amber-500/20";
|
||||
const normalizedValue = normalizeConfidenceValue(score, effectiveConfidenceOptions);
|
||||
const matchedIndex = effectiveConfidenceOptions.findIndex((item) => (item.value || "") === normalizedValue);
|
||||
if (matchedIndex < 0) return "text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-500/10 border-rose-200 dark:border-rose-500/20";
|
||||
if (matchedIndex === 0) return "text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20";
|
||||
if (matchedIndex === 1) return "text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 border-amber-200 dark:border-amber-500/20";
|
||||
return "text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-500/10 border-rose-200 dark:border-rose-500/20";
|
||||
};
|
||||
|
||||
|
|
@ -1529,7 +1601,7 @@ export default function Opportunities() {
|
|||
const overview = await getOpportunityOverview("", "全部");
|
||||
const exportItems = (overview.items ?? [])
|
||||
.filter((item) => (archiveTab === "active" ? !item.archived : Boolean(item.archived)))
|
||||
.filter((item) => matchesOpportunityExportFilters(item, filters));
|
||||
.filter((item) => matchesOpportunityExportFilters(item, filters, effectiveConfidenceOptions));
|
||||
if (exportItems.length <= 0) {
|
||||
throw new Error(`当前筛选条件下暂无可导出的${archiveTab === "active" ? "未归档" : "已归档"}商机`);
|
||||
}
|
||||
|
|
@ -1594,7 +1666,7 @@ export default function Opportunities() {
|
|||
normalizeOpportunityExportText(item.type || "新建"),
|
||||
normalizeOpportunityExportText(item.operatorName),
|
||||
normalizeOpportunityExportText(item.stage),
|
||||
getConfidenceLabel(item.confidence),
|
||||
getConfidenceLabel(item.confidence, effectiveConfidenceOptions),
|
||||
item.amount === null || item.amount === undefined ? "" : `¥${formatAmount(item.amount)}`,
|
||||
normalizeOpportunityExportText(item.date),
|
||||
normalizeOpportunityExportText(item.salesExpansionName || relatedSales?.name),
|
||||
|
|
@ -1678,7 +1750,7 @@ export default function Opportunities() {
|
|||
const handleOpenCreate = () => {
|
||||
setError("");
|
||||
setFieldErrors({});
|
||||
setForm(defaultForm);
|
||||
setForm(buildEmptyForm());
|
||||
setSelectedCompetitors([]);
|
||||
setCustomCompetitorName("");
|
||||
setCreateOpen(true);
|
||||
|
|
@ -1690,7 +1762,7 @@ export default function Opportunities() {
|
|||
setSubmitting(false);
|
||||
setError("");
|
||||
setFieldErrors({});
|
||||
setForm(defaultForm);
|
||||
setForm(buildEmptyForm());
|
||||
setSelectedCompetitors([]);
|
||||
setCustomCompetitorName("");
|
||||
};
|
||||
|
|
@ -1739,7 +1811,7 @@ export default function Opportunities() {
|
|||
}
|
||||
setError("");
|
||||
setFieldErrors({});
|
||||
setForm(toFormFromItem(selectedItem));
|
||||
setForm(toFormFromItem(selectedItem, effectiveConfidenceOptions));
|
||||
const competitorState = parseCompetitorState(selectedItem.competitorName);
|
||||
setSelectedCompetitors(competitorState.selections);
|
||||
setCustomCompetitorName(competitorState.customName);
|
||||
|
|
@ -1990,18 +2062,8 @@ export default function Opportunities() {
|
|||
<h3 className="truncate text-base font-semibold text-slate-900 dark:text-white sm:text-lg">{opp.name || "未命名商机"}</h3>
|
||||
<p className="mt-1 truncate text-xs leading-5 text-slate-500 dark:text-slate-400 sm:text-sm">项目编号:{opp.code || "待生成"}</p>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full border px-2.5 py-1 text-[11px] font-semibold leading-none",
|
||||
isOwnedByCurrentUser
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-600 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300"
|
||||
: "border-slate-200 bg-slate-50 text-slate-500 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-300",
|
||||
)}
|
||||
>
|
||||
{isOwnedByCurrentUser ? "我的" : "只读"}
|
||||
</span>
|
||||
<span className={getConfidenceBadgeClass(opp.confidence)}>
|
||||
{getConfidenceLabel(opp.confidence)}
|
||||
<span className={getConfidenceBadgeClass(opp.confidence, effectiveConfidenceOptions)}>
|
||||
{getConfidenceLabel(opp.confidence, effectiveConfidenceOptions)}
|
||||
</span>
|
||||
<span className="crm-pill crm-pill-neutral">
|
||||
{opp.stage || "初步沟通"}
|
||||
|
|
@ -2057,6 +2119,7 @@ export default function Opportunities() {
|
|||
exportError={exportError}
|
||||
archiveTab={archiveTab}
|
||||
stageOptions={stageOptions}
|
||||
confidenceOptions={effectiveConfidenceOptions}
|
||||
projectLocationOptions={projectLocationOptions}
|
||||
opportunityTypeOptions={opportunityTypeOptions}
|
||||
operatorOptions={operatorOptions}
|
||||
|
|
@ -2231,14 +2294,17 @@ export default function Opportunities() {
|
|||
<label className="space-y-2">
|
||||
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">项目把握度<RequiredMark /></span>
|
||||
<AdaptiveSelect
|
||||
value={getConfidenceOptionValue(form.confidencePct)}
|
||||
value={normalizeConfidenceValue(form.confidencePct, effectiveConfidenceOptions)}
|
||||
placeholder="请选择"
|
||||
sheetTitle="项目把握度"
|
||||
options={CONFIDENCE_OPTIONS.map((item) => ({ value: item.value, label: item.label }))}
|
||||
options={effectiveConfidenceOptions.map((item) => ({
|
||||
value: item.value || "",
|
||||
label: item.label || item.value || "",
|
||||
}))}
|
||||
className={cn(
|
||||
fieldErrors.confidencePct ? "border-rose-400 bg-rose-50/60 focus:border-rose-500 focus:ring-rose-500 dark:border-rose-500/70 dark:bg-rose-500/10" : "",
|
||||
)}
|
||||
onChange={(value) => handleChange("confidencePct", normalizeConfidenceGrade(value))}
|
||||
onChange={(value) => handleChange("confidencePct", normalizeConfidenceValue(value, effectiveConfidenceOptions))}
|
||||
/>
|
||||
{fieldErrors.confidencePct ? <p className="text-xs text-rose-500">{fieldErrors.confidencePct}</p> : null}
|
||||
</label>
|
||||
|
|
@ -2461,7 +2527,7 @@ export default function Opportunities() {
|
|||
<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>
|
||||
<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)}</span>
|
||||
<span className={`rounded-full px-2.5 py-1 text-xs font-medium ${getConfidenceColor(selectedItem.confidence)}`}>项目把握度 {getConfidenceLabel(selectedItem.confidence, effectiveConfidenceOptions)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -2478,7 +2544,7 @@ export default function Opportunities() {
|
|||
<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)} />
|
||||
<DetailItem label="项目把握度" value={getConfidenceLabel(selectedItem.confidence, effectiveConfidenceOptions)} />
|
||||
<DetailItem label="项目阶段" value={selectedItem.stage || "无"} icon={<Activity className="h-3 w-3" />} />
|
||||
<DetailItem
|
||||
label="是否已推送OMS"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
-- ensure opportunity confidence dictionary exists
|
||||
|
||||
begin;
|
||||
|
||||
insert into sys_dict_item (type_code, item_label, item_value, sort_order, status, is_deleted, remark)
|
||||
select v.type_code, v.item_label, v.item_value, v.sort_order, 1, 0, '商机项目把握度'
|
||||
from (
|
||||
values
|
||||
('sj_xmbwd', 'A', 'A', 1),
|
||||
('sj_xmbwd', 'B', 'B', 2),
|
||||
('sj_xmbwd', 'C', 'C', 3)
|
||||
) as v(type_code, item_label, item_value, sort_order)
|
||||
where not exists (
|
||||
select 1
|
||||
from sys_dict_item s
|
||||
where s.type_code = v.type_code
|
||||
and s.item_value = v.item_value
|
||||
and coalesce(s.is_deleted, 0) = 0
|
||||
);
|
||||
|
||||
commit;
|
||||
|
|
@ -943,7 +943,10 @@ FROM (
|
|||
VALUES
|
||||
('sj_jslx', '新建', '新建', 1),
|
||||
('sj_jslx', '扩容', '扩容', 2),
|
||||
('sj_jslx', '替换', '替换', 3)
|
||||
('sj_jslx', '替换', '替换', 3),
|
||||
('sj_xmbwd', 'A', 'A', 1),
|
||||
('sj_xmbwd', 'B', 'B', 2),
|
||||
('sj_xmbwd', 'C', 'C', 3)
|
||||
) AS v(type_code, item_label, item_value, sort_order)
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
|
|
|
|||
Loading…
Reference in New Issue