控制首页展示的权限添加
parent
7b1c424ba2
commit
04e7422438
|
|
@ -12,6 +12,7 @@ public class DashboardActivityDTO {
|
|||
private String content;
|
||||
private Long operatorUserId;
|
||||
private String operatorName;
|
||||
private String targetTab;
|
||||
private OffsetDateTime createdAt;
|
||||
private String timeText;
|
||||
|
||||
|
|
@ -79,6 +80,14 @@ public class DashboardActivityDTO {
|
|||
this.operatorName = operatorName;
|
||||
}
|
||||
|
||||
public String getTargetTab() {
|
||||
return targetTab;
|
||||
}
|
||||
|
||||
public void setTargetTab(String targetTab) {
|
||||
this.targetTab = targetTab;
|
||||
}
|
||||
|
||||
public OffsetDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ public class DashboardHomeDTO {
|
|||
private String jobTitle;
|
||||
private String deptName;
|
||||
private Long onboardingDays;
|
||||
private Boolean todoCardVisible;
|
||||
private Boolean activityCardVisible;
|
||||
private List<DashboardStatDTO> stats;
|
||||
private List<DashboardTodoDTO> todos;
|
||||
private List<DashboardActivityDTO> activities;
|
||||
|
|
@ -17,6 +19,7 @@ public class DashboardHomeDTO {
|
|||
}
|
||||
|
||||
public DashboardHomeDTO(Long userId, String realName, String jobTitle, String deptName, Long onboardingDays,
|
||||
Boolean todoCardVisible, Boolean activityCardVisible,
|
||||
List<DashboardStatDTO> stats, List<DashboardTodoDTO> todos,
|
||||
List<DashboardActivityDTO> activities) {
|
||||
this.userId = userId;
|
||||
|
|
@ -24,6 +27,8 @@ public class DashboardHomeDTO {
|
|||
this.jobTitle = jobTitle;
|
||||
this.deptName = deptName;
|
||||
this.onboardingDays = onboardingDays;
|
||||
this.todoCardVisible = todoCardVisible;
|
||||
this.activityCardVisible = activityCardVisible;
|
||||
this.stats = stats;
|
||||
this.todos = todos;
|
||||
this.activities = activities;
|
||||
|
|
@ -69,6 +74,22 @@ public class DashboardHomeDTO {
|
|||
this.onboardingDays = onboardingDays;
|
||||
}
|
||||
|
||||
public Boolean getTodoCardVisible() {
|
||||
return todoCardVisible;
|
||||
}
|
||||
|
||||
public void setTodoCardVisible(Boolean todoCardVisible) {
|
||||
this.todoCardVisible = todoCardVisible;
|
||||
}
|
||||
|
||||
public Boolean getActivityCardVisible() {
|
||||
return activityCardVisible;
|
||||
}
|
||||
|
||||
public void setActivityCardVisible(Boolean activityCardVisible) {
|
||||
this.activityCardVisible = activityCardVisible;
|
||||
}
|
||||
|
||||
public List<DashboardStatDTO> getStats() {
|
||||
return stats;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,12 @@ public interface DashboardMapper {
|
|||
|
||||
int markTodoDone(@Param("userId") Long userId, @Param("todoId") Long todoId);
|
||||
|
||||
List<DashboardActivityDTO> selectLatestActivities(@Param("userId") Long userId);
|
||||
@DataScope(tableAlias = "a", ownerColumn = "owner_user_id")
|
||||
List<DashboardActivityDTO> selectLatestOpportunityActivities(@Param("userId") Long userId);
|
||||
|
||||
@DataScope(tableAlias = "a", ownerColumn = "owner_user_id")
|
||||
List<DashboardActivityDTO> selectLatestSalesActivities(@Param("userId") Long userId);
|
||||
|
||||
@DataScope(tableAlias = "a", ownerColumn = "owner_user_id")
|
||||
List<DashboardActivityDTO> selectLatestChannelActivities(@Param("userId") Long userId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,21 +8,34 @@ import com.unis.crm.dto.dashboard.DashboardTodoDTO;
|
|||
import com.unis.crm.dto.dashboard.UserWelcomeDTO;
|
||||
import com.unis.crm.mapper.DashboardMapper;
|
||||
import com.unis.crm.service.DashboardService;
|
||||
import com.unisbase.service.SysPermissionService;
|
||||
import com.unisbase.spi.UnisBaseTenantProvider;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DashboardServiceImpl implements DashboardService {
|
||||
|
||||
private final DashboardMapper dashboardMapper;
|
||||
private static final String DASHBOARD_TODO_CARD_VIEW_PERMISSION = "dashboard_todo_card:view";
|
||||
private static final String DASHBOARD_ACTIVITY_CARD_VIEW_PERMISSION = "dashboard_activity_card:view";
|
||||
|
||||
public DashboardServiceImpl(DashboardMapper dashboardMapper) {
|
||||
private final DashboardMapper dashboardMapper;
|
||||
private final SysPermissionService sysPermissionService;
|
||||
private final UnisBaseTenantProvider tenantProvider;
|
||||
|
||||
public DashboardServiceImpl(DashboardMapper dashboardMapper,
|
||||
SysPermissionService sysPermissionService,
|
||||
UnisBaseTenantProvider tenantProvider) {
|
||||
this.dashboardMapper = dashboardMapper;
|
||||
this.sysPermissionService = sysPermissionService;
|
||||
this.tenantProvider = tenantProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -41,8 +54,11 @@ public class DashboardServiceImpl implements DashboardService {
|
|||
if (monthlyChannelStat != null) {
|
||||
stats.add(monthlyChannelStat);
|
||||
}
|
||||
List<DashboardTodoDTO> todos = dashboardMapper.selectTodos(userId);
|
||||
List<DashboardActivityDTO> activities = dashboardMapper.selectLatestActivities(userId);
|
||||
Set<String> permissionCodes = loadPermissionCodes(userId);
|
||||
boolean todoCardVisible = permissionCodes.contains(DASHBOARD_TODO_CARD_VIEW_PERMISSION);
|
||||
boolean activityCardVisible = permissionCodes.contains(DASHBOARD_ACTIVITY_CARD_VIEW_PERMISSION);
|
||||
List<DashboardTodoDTO> todos = todoCardVisible ? dashboardMapper.selectTodos(userId) : List.of();
|
||||
List<DashboardActivityDTO> activities = activityCardVisible ? loadLatestActivities(userId) : List.of();
|
||||
enrichActivityTimeText(activities);
|
||||
|
||||
long onboardingDays = 0;
|
||||
|
|
@ -57,6 +73,8 @@ public class DashboardServiceImpl implements DashboardService {
|
|||
user.getJobTitle(),
|
||||
user.getDeptName(),
|
||||
onboardingDays,
|
||||
todoCardVisible,
|
||||
activityCardVisible,
|
||||
stats,
|
||||
todos,
|
||||
activities
|
||||
|
|
@ -77,6 +95,35 @@ public class DashboardServiceImpl implements DashboardService {
|
|||
}
|
||||
}
|
||||
|
||||
private Set<String> loadPermissionCodes(Long userId) {
|
||||
try {
|
||||
Long tenantId = tenantProvider.getCurrentTenantId();
|
||||
if (tenantId == null) {
|
||||
return Set.of();
|
||||
}
|
||||
Set<String> permissionCodes = sysPermissionService.listPermissionCodesByUserId(userId, tenantId);
|
||||
if (permissionCodes == null) {
|
||||
return Set.of();
|
||||
}
|
||||
return permissionCodes;
|
||||
} catch (Exception ignored) {
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
|
||||
private List<DashboardActivityDTO> loadLatestActivities(Long userId) {
|
||||
List<DashboardActivityDTO> activities = new ArrayList<>();
|
||||
activities.addAll(dashboardMapper.selectLatestOpportunityActivities(userId));
|
||||
activities.addAll(dashboardMapper.selectLatestSalesActivities(userId));
|
||||
activities.addAll(dashboardMapper.selectLatestChannelActivities(userId));
|
||||
activities.sort(Comparator.comparing(DashboardActivityDTO::getCreatedAt,
|
||||
Comparator.nullsLast(Comparator.reverseOrder())));
|
||||
if (activities.size() > 8) {
|
||||
return new ArrayList<>(activities.subList(0, 8));
|
||||
}
|
||||
return activities;
|
||||
}
|
||||
|
||||
private void enrichActivityTimeText(List<DashboardActivityDTO> activities) {
|
||||
if (activities == null || activities.isEmpty()) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -116,110 +116,7 @@
|
|||
and status in ('todo', 'done')
|
||||
</update>
|
||||
|
||||
<select id="selectLatestActivities" resultType="com.unis.crm.dto.dashboard.DashboardActivityDTO">
|
||||
with latest_report_comment as (
|
||||
select distinct on (c.report_id)
|
||||
c.report_id,
|
||||
c.reviewer_user_id,
|
||||
c.score,
|
||||
c.comment_content,
|
||||
c.reviewed_at
|
||||
from work_daily_report_comment c
|
||||
order by c.report_id, c.reviewed_at desc, c.id desc
|
||||
),
|
||||
activity_union as (
|
||||
select
|
||||
l.id,
|
||||
l.biz_type as bizType,
|
||||
l.biz_id as bizId,
|
||||
l.action_type as actionType,
|
||||
l.title,
|
||||
l.content,
|
||||
l.operator_user_id as operatorUserId,
|
||||
l.created_at as createdAt
|
||||
from sys_activity_log l
|
||||
where l.operator_user_id = #{userId}
|
||||
or l.operator_user_id is null
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
(1000000000 + o.id) as id,
|
||||
'opportunity' as bizType,
|
||||
o.id as bizId,
|
||||
'stage_update' as actionType,
|
||||
'商机阶段更新' as title,
|
||||
o.opportunity_name || ' 已推进至' ||
|
||||
case o.stage
|
||||
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 || '阶段' as content,
|
||||
o.owner_user_id as operatorUserId,
|
||||
o.updated_at as createdAt
|
||||
from crm_opportunity o
|
||||
where o.owner_user_id = #{userId}
|
||||
and o.updated_at > o.created_at
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
(2000000000 + r.id) as id,
|
||||
'report' as bizType,
|
||||
r.id as bizId,
|
||||
case
|
||||
when r.status = 'reviewed' or lc.score is not null then 'report_reviewed'
|
||||
else 'report_read'
|
||||
end as actionType,
|
||||
case
|
||||
when r.status = 'reviewed' or lc.score is not null then '日报已点评'
|
||||
else '日报已阅'
|
||||
end as title,
|
||||
case
|
||||
when lc.score is not null then '主管对你' || to_char(r.report_date, 'MM-DD') || '的日报给出了 ' || lc.score || ' 分'
|
||||
when r.status = 'reviewed' then '你的' || to_char(r.report_date, 'MM-DD') || '日报已完成主管点评'
|
||||
else '你的' || to_char(r.report_date, 'MM-DD') || '日报已被查阅'
|
||||
end as content,
|
||||
coalesce(lc.reviewer_user_id, r.user_id) as operatorUserId,
|
||||
coalesce(lc.reviewed_at, r.updated_at, r.created_at) as createdAt
|
||||
from work_daily_report r
|
||||
left join latest_report_comment lc on lc.report_id = r.id
|
||||
where r.user_id = #{userId}
|
||||
and r.status in ('read', 'reviewed')
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
(3000000000 + c.id) as id,
|
||||
'channel' as bizType,
|
||||
c.id as bizId,
|
||||
'channel_created' as actionType,
|
||||
'新渠道录入' as title,
|
||||
'成功录入 ' || c.channel_name || ' 渠道商信息' as content,
|
||||
c.owner_user_id as operatorUserId,
|
||||
c.created_at as createdAt
|
||||
from crm_channel_expansion c
|
||||
where c.owner_user_id = #{userId}
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
(4000000000 + f.id) as id,
|
||||
'opportunity_followup' as bizType,
|
||||
f.opportunity_id as bizId,
|
||||
'opportunity_followup' as actionType,
|
||||
'商机跟进新增' as title,
|
||||
o.opportunity_name || ' 新增了一条' || f.followup_type || '跟进记录' as content,
|
||||
f.followup_user_id as operatorUserId,
|
||||
f.followup_time as createdAt
|
||||
from crm_opportunity_followup f
|
||||
join crm_opportunity o on o.id = f.opportunity_id
|
||||
where f.followup_user_id = #{userId}
|
||||
)
|
||||
<select id="selectLatestOpportunityActivities" resultType="com.unis.crm.dto.dashboard.DashboardActivityDTO">
|
||||
select
|
||||
a.id,
|
||||
a.bizType,
|
||||
|
|
@ -229,10 +126,180 @@
|
|||
a.content,
|
||||
a.operatorUserId,
|
||||
u.display_name as operatorName,
|
||||
a.targetTab,
|
||||
a.createdAt
|
||||
from activity_union a
|
||||
from (
|
||||
select
|
||||
(1000000000 + o.id) as id,
|
||||
'opportunity' as bizType,
|
||||
o.id as bizId,
|
||||
'status_update' as actionType,
|
||||
'商机状态更新' as title,
|
||||
o.opportunity_name || ' · 当前阶段:' ||
|
||||
case o.stage
|
||||
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 ||
|
||||
case when coalesce(o.archived, false) then ' · 已签单' else '' end ||
|
||||
case when coalesce(o.pushed_to_oms, false) then ' · 已推送OMS' else '' end as content,
|
||||
o.owner_user_id as operatorUserId,
|
||||
case when coalesce(o.archived, false) then 'archived' else 'active' end as targetTab,
|
||||
o.owner_user_id as owner_user_id,
|
||||
o.updated_at as createdAt
|
||||
from crm_opportunity o
|
||||
where o.updated_at > o.created_at
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
(1100000000 + f.id) as id,
|
||||
'opportunity' as bizType,
|
||||
f.opportunity_id as bizId,
|
||||
'followup_update' as actionType,
|
||||
'商机跟进更新' as title,
|
||||
o.opportunity_name || ' · ' || coalesce(f.followup_type, '跟进') || ' · ' ||
|
||||
left(coalesce(nullif(btrim(f.next_action), ''), nullif(btrim(f.content), ''), '已新增跟进记录'), 80) as content,
|
||||
f.followup_user_id as operatorUserId,
|
||||
case when coalesce(o.archived, false) then 'archived' else 'active' end as targetTab,
|
||||
o.owner_user_id as owner_user_id,
|
||||
f.followup_time as createdAt
|
||||
from crm_opportunity_followup f
|
||||
join crm_opportunity o on o.id = f.opportunity_id
|
||||
) a
|
||||
left join sys_user u on u.user_id = a.operatorUserId
|
||||
where 1 = 1
|
||||
order by a.createdAt desc nulls last
|
||||
limit 8
|
||||
limit 12
|
||||
</select>
|
||||
|
||||
<select id="selectLatestSalesActivities" resultType="com.unis.crm.dto.dashboard.DashboardActivityDTO">
|
||||
select
|
||||
a.id,
|
||||
a.bizType,
|
||||
a.bizId,
|
||||
a.actionType,
|
||||
a.title,
|
||||
a.content,
|
||||
a.operatorUserId,
|
||||
u.display_name as operatorName,
|
||||
a.targetTab,
|
||||
a.createdAt
|
||||
from (
|
||||
select
|
||||
(2000000000 + s.id) as id,
|
||||
'sales' as bizType,
|
||||
s.id as bizId,
|
||||
'status_update' as actionType,
|
||||
'销售拓展状态更新' as title,
|
||||
s.candidate_name || ' · 当前阶段:' ||
|
||||
case s.stage
|
||||
when 'initial_contact' then '初步沟通'
|
||||
when 'solution_discussion' then '方案交流'
|
||||
when 'bidding' then '招投标'
|
||||
when 'business_negotiation' then '商务谈判'
|
||||
when 'won' then '已成交'
|
||||
when 'lost' then '已放弃'
|
||||
else coalesce(s.stage, '未知')
|
||||
end ||
|
||||
' · ' || case when coalesce(s.employment_status, 'active') = 'active' then '在职' else '离职' end as content,
|
||||
s.owner_user_id as operatorUserId,
|
||||
'sales' as targetTab,
|
||||
s.owner_user_id as owner_user_id,
|
||||
s.updated_at as createdAt
|
||||
from crm_sales_expansion s
|
||||
where s.updated_at > s.created_at
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
(2100000000 + f.id) as id,
|
||||
'sales' as bizType,
|
||||
f.biz_id as bizId,
|
||||
'followup_update' as actionType,
|
||||
'销售拓展跟进更新' as title,
|
||||
s.candidate_name || ' · ' || coalesce(f.followup_type, '跟进') || ' · ' ||
|
||||
left(coalesce(nullif(btrim(f.next_plan), ''), nullif(btrim(f.content), ''), '已新增跟进记录'), 80) as content,
|
||||
f.followup_user_id as operatorUserId,
|
||||
'sales' as targetTab,
|
||||
s.owner_user_id as owner_user_id,
|
||||
f.followup_time as createdAt
|
||||
from crm_expansion_followup f
|
||||
join crm_sales_expansion s on s.id = f.biz_id and f.biz_type = 'sales'
|
||||
) a
|
||||
left join sys_user u on u.user_id = a.operatorUserId
|
||||
where 1 = 1
|
||||
order by a.createdAt desc nulls last
|
||||
limit 12
|
||||
</select>
|
||||
|
||||
<select id="selectLatestChannelActivities" resultType="com.unis.crm.dto.dashboard.DashboardActivityDTO">
|
||||
select
|
||||
a.id,
|
||||
a.bizType,
|
||||
a.bizId,
|
||||
a.actionType,
|
||||
a.title,
|
||||
a.content,
|
||||
a.operatorUserId,
|
||||
u.display_name as operatorName,
|
||||
a.targetTab,
|
||||
a.createdAt
|
||||
from (
|
||||
select
|
||||
(3000000000 + c.id) as id,
|
||||
'channel' as bizType,
|
||||
c.id as bizId,
|
||||
'status_update' as actionType,
|
||||
'渠道拓展状态更新' as title,
|
||||
c.channel_name || ' · 当前阶段:' ||
|
||||
case c.stage
|
||||
when 'initial_contact' then '初步接触'
|
||||
when 'solution_discussion' then '方案交流'
|
||||
when 'bidding' then '招投标'
|
||||
when 'business_negotiation' then '合作洽谈'
|
||||
when 'won' then '已合作'
|
||||
when 'lost' then '已终止'
|
||||
else coalesce(c.stage, '未知')
|
||||
end ||
|
||||
' · 合作意向:' ||
|
||||
case c.intent_level
|
||||
when 'high' then '高'
|
||||
when 'medium' then '中'
|
||||
when 'low' then '低'
|
||||
else '未知'
|
||||
end as content,
|
||||
c.owner_user_id as operatorUserId,
|
||||
'channel' as targetTab,
|
||||
c.owner_user_id as owner_user_id,
|
||||
c.updated_at as createdAt
|
||||
from crm_channel_expansion c
|
||||
where c.updated_at > c.created_at
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
(3100000000 + f.id) as id,
|
||||
'channel' as bizType,
|
||||
f.biz_id as bizId,
|
||||
'followup_update' as actionType,
|
||||
'渠道拓展跟进更新' as title,
|
||||
c.channel_name || ' · ' || coalesce(f.followup_type, '跟进') || ' · ' ||
|
||||
left(coalesce(nullif(btrim(f.next_plan), ''), nullif(btrim(f.content), ''), '已新增跟进记录'), 80) as content,
|
||||
f.followup_user_id as operatorUserId,
|
||||
'channel' as targetTab,
|
||||
c.owner_user_id as owner_user_id,
|
||||
f.followup_time as createdAt
|
||||
from crm_expansion_followup f
|
||||
join crm_channel_expansion c on c.id = f.biz_id and f.biz_type = 'channel'
|
||||
) a
|
||||
left join sys_user u on u.user_id = a.operatorUserId
|
||||
where 1 = 1
|
||||
order by a.createdAt desc nulls last
|
||||
limit 12
|
||||
</select>
|
||||
</mapper>
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ export interface DashboardActivity {
|
|||
content?: string;
|
||||
operatorUserId?: number;
|
||||
operatorName?: string;
|
||||
targetTab?: string;
|
||||
createdAt?: string;
|
||||
timeText?: string;
|
||||
}
|
||||
|
|
@ -104,6 +105,8 @@ export interface DashboardHome {
|
|||
jobTitle?: string;
|
||||
deptName?: string;
|
||||
onboardingDays?: number;
|
||||
todoCardVisible?: boolean;
|
||||
activityCardVisible?: boolean;
|
||||
stats?: DashboardStat[];
|
||||
todos?: DashboardTodo[];
|
||||
activities?: DashboardActivity[];
|
||||
|
|
@ -650,6 +653,11 @@ function applyAuthHeaders(headers: Headers) {
|
|||
headers.set("X-User-Id", String(userId));
|
||||
}
|
||||
|
||||
const activeTenantId = localStorage.getItem("activeTenantId");
|
||||
if (activeTenantId?.trim()) {
|
||||
headers.set("X-Tenant-Id", activeTenantId.trim());
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,9 @@ export default function Dashboard() {
|
|||
const visibleActivities = showAllActivities ? activities : activities.slice(0, DASHBOARD_PREVIEW_COUNT);
|
||||
const hasMoreActivities = activities.length > DASHBOARD_PREVIEW_COUNT && activities[0]?.id !== 0;
|
||||
const hasMoreHistoryTodos = historyTodos.length > DASHBOARD_HISTORY_PREVIEW_COUNT;
|
||||
const showTodoCard = home.todoCardVisible !== false;
|
||||
const showActivityCard = home.activityCardVisible !== false;
|
||||
const dashboardPanelCount = Number(showTodoCard) + Number(showActivityCard);
|
||||
|
||||
const handleCompleteTodo = async (todoId: string) => {
|
||||
if (!todoId || completingTodoId === todoId) {
|
||||
|
|
@ -151,6 +154,23 @@ export default function Dashboard() {
|
|||
navigate(target.pathname, target.state ? { state: target.state } : undefined);
|
||||
};
|
||||
|
||||
const handleActivityClick = (activity: DashboardActivity) => {
|
||||
if (!activity?.bizId || !activity.bizType) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activity.bizType === "opportunity") {
|
||||
const archiveTab = activity.targetTab === "archived" ? "archived" : "active";
|
||||
navigate("/opportunities", { state: { selectedId: activity.bizId, archiveTab } });
|
||||
return;
|
||||
}
|
||||
|
||||
if (activity.bizType === "sales" || activity.bizType === "channel") {
|
||||
const tab = activity.targetTab === "channel" ? "channel" : "sales";
|
||||
navigate("/expansion", { state: { tab, selectedId: activity.bizId } });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="crm-page-stack">
|
||||
<header className="crm-page-header">
|
||||
|
|
@ -223,7 +243,8 @@ export default function Dashboard() {
|
|||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-5 md:mt-6 md:grid-cols-2 md:gap-6">
|
||||
<div className={`mt-5 grid gap-5 md:mt-6 ${dashboardPanelCount > 1 ? "md:grid-cols-2" : "md:grid-cols-1"} md:gap-6`}>
|
||||
{showTodoCard ? (
|
||||
<motion.div
|
||||
initial={disableMobileMotion ? false : { opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -320,7 +341,9 @@ export default function Dashboard() {
|
|||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
|
||||
{showActivityCard ? (
|
||||
<motion.div
|
||||
initial={disableMobileMotion ? false : { opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -330,17 +353,35 @@ export default function Dashboard() {
|
|||
<h2 className="mb-4 text-lg font-semibold text-slate-900 dark:text-white">最新动态</h2>
|
||||
<div className="crm-section-stack sm:gap-5">
|
||||
{visibleActivities.map((news: DashboardActivity, i: number) => (
|
||||
<div key={news.id ?? i} className="flex gap-4">
|
||||
<div className="relative mt-1 flex h-3 w-3 items-center justify-center">
|
||||
<button
|
||||
key={news.id ?? i}
|
||||
type="button"
|
||||
onClick={() => handleActivityClick(news)}
|
||||
disabled={!news?.bizId || !news?.bizType}
|
||||
className="group flex w-full gap-4 rounded-xl border border-transparent px-2 py-2 text-left transition-colors hover:border-violet-100 hover:bg-violet-50/60 disabled:cursor-default disabled:hover:border-transparent disabled:hover:bg-transparent dark:hover:border-violet-900/40 dark:hover:bg-slate-900/50"
|
||||
>
|
||||
<div className="relative mt-1 flex h-3 w-3 shrink-0 items-center justify-center">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-violet-400 opacity-20"></span>
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-violet-500"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-900 dark:text-white">{news.title || "无"}</p>
|
||||
<p className="crm-field-note mt-0.5">{news.content || "无"}</p>
|
||||
<p className="mt-1 text-[10px] text-slate-400 dark:text-slate-500">{news.timeText || "无"}</p>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<p className="text-sm font-medium text-slate-900 transition-colors group-hover:text-violet-700 dark:text-white dark:group-hover:text-violet-300">
|
||||
{news.title || "无"}
|
||||
</p>
|
||||
{news.bizId ? (
|
||||
<span className="shrink-0 rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-500 group-hover:bg-violet-100 group-hover:text-violet-700 dark:bg-slate-800 dark:text-slate-400 dark:group-hover:bg-violet-500/10 dark:group-hover:text-violet-300">
|
||||
查看
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="crm-field-note mt-0.5 line-clamp-2">{news.content || "无"}</p>
|
||||
<div className="mt-1 flex items-center gap-2 text-[10px] text-slate-400 dark:text-slate-500">
|
||||
<span>{news.timeText || "无"}</span>
|
||||
{news.operatorName ? <span>· {news.operatorName}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{hasMoreActivities && !showAllActivities ? (
|
||||
|
|
@ -353,6 +394,7 @@ export default function Dashboard() {
|
|||
</button>
|
||||
) : null}
|
||||
</motion.div>
|
||||
) : null}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { cn } from "@/lib/utils";
|
|||
|
||||
type ExpansionItem = SalesExpansionItem | ChannelExpansionItem;
|
||||
type ExpansionTab = "sales" | "channel";
|
||||
type ExpansionLocationState = { tab?: ExpansionTab; selectedId?: number } | null;
|
||||
type ExpansionExportFilters = {
|
||||
keyword?: string;
|
||||
intent?: string;
|
||||
|
|
@ -1038,12 +1039,42 @@ export default function Expansion() {
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const requestedTab = (location.state as { tab?: ExpansionTab } | null)?.tab;
|
||||
const requestedTab = (location.state as ExpansionLocationState)?.tab;
|
||||
if (requestedTab === "sales" || requestedTab === "channel") {
|
||||
setActiveTab(requestedTab);
|
||||
}
|
||||
}, [location.state]);
|
||||
|
||||
useEffect(() => {
|
||||
const requestedState = location.state as ExpansionLocationState;
|
||||
const requestedTab = requestedState?.tab;
|
||||
const requestedId = requestedState?.selectedId;
|
||||
if (!requestedId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestedTab === "sales") {
|
||||
const matched = salesData.find((item) => item.id === requestedId);
|
||||
if (matched) {
|
||||
setSelectedItem(matched);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestedTab === "channel") {
|
||||
const matched = channelData.find((item) => item.id === requestedId);
|
||||
if (matched) {
|
||||
setSelectedItem(matched);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const matched = salesData.find((item) => item.id === requestedId) ?? channelData.find((item) => item.id === requestedId) ?? null;
|
||||
if (matched) {
|
||||
setSelectedItem(matched);
|
||||
}
|
||||
}, [location.state, salesData, channelData]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useEffect, useRef, useState, type ReactNode } from "react";
|
|||
import { Search, Plus, Download, ChevronDown, Check, Building, Calendar, DollarSign, Activity, X, Clock, FileText, User, Tag, AlertTriangle, ListFilter } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createOpportunity, getExpansionOverview, getOpportunityMeta, getOpportunityOmsPreSalesOptions, getOpportunityOverview, getStoredCurrentUserId, pushOpportunityToOms, updateOpportunity, type ChannelExpansionItem, type CreateOpportunityPayload, type OmsPreSalesOption, type OpportunityDictOption, type OpportunityFollowUp, type OpportunityItem, type PushOpportunityToOmsPayload, type SalesExpansionItem } from "@/lib/auth";
|
||||
import { AdaptiveSelect } from "@/components/AdaptiveSelect";
|
||||
import { useIsWecomBrowser } from "@/hooks/useIsWecomBrowser";
|
||||
|
|
@ -32,6 +33,7 @@ const COMPETITOR_OPTIONS = [
|
|||
type CompetitorOption = (typeof COMPETITOR_OPTIONS)[number];
|
||||
type OperatorMode = "none" | "h3c" | "channel" | "both";
|
||||
type OpportunityArchiveTab = "active" | "archived";
|
||||
type OpportunityLocationState = { selectedId?: number; archiveTab?: OpportunityArchiveTab } | null;
|
||||
type OpportunityExportFilters = {
|
||||
keyword?: string;
|
||||
expectedStartDate?: string;
|
||||
|
|
@ -1342,6 +1344,7 @@ function CompetitorMultiSelect({
|
|||
}
|
||||
|
||||
export default function Opportunities() {
|
||||
const location = useLocation();
|
||||
const currentUserId = getStoredCurrentUserId();
|
||||
const isMobileViewport = useIsMobileViewport();
|
||||
const isWecomBrowser = useIsWecomBrowser();
|
||||
|
|
@ -1404,6 +1407,27 @@ export default function Opportunities() {
|
|||
};
|
||||
}, [keyword, filter]);
|
||||
|
||||
useEffect(() => {
|
||||
const requestedState = location.state as OpportunityLocationState;
|
||||
const requestedArchiveTab = requestedState?.archiveTab;
|
||||
if (requestedArchiveTab === "active" || requestedArchiveTab === "archived") {
|
||||
setArchiveTab(requestedArchiveTab);
|
||||
}
|
||||
}, [location.state]);
|
||||
|
||||
useEffect(() => {
|
||||
const requestedState = location.state as OpportunityLocationState;
|
||||
const requestedId = requestedState?.selectedId;
|
||||
if (!requestedId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matched = items.find((item) => item.id === requestedId);
|
||||
if (matched) {
|
||||
setSelectedItem(matched);
|
||||
}
|
||||
}, [location.state, items]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue