diff --git a/app/api/events/geo/route.ts b/app/api/events/geo/route.ts index 4230096..6f4b22b 100644 --- a/app/api/events/geo/route.ts +++ b/app/api/events/geo/route.ts @@ -6,11 +6,20 @@ export async function GET(request: NextRequest) { try { const searchParams = request.nextUrl.searchParams; + // 获取团队、项目和标签筛选参数 + const teamIds = searchParams.getAll('teamId'); + const projectIds = searchParams.getAll('projectId'); + const tagIds = searchParams.getAll('tagId'); + const data = await getGeoAnalytics({ startTime: searchParams.get('startTime') || undefined, endTime: searchParams.get('endTime') || undefined, linkId: searchParams.get('linkId') || undefined, - groupBy: (searchParams.get('groupBy') || 'country') as 'country' | 'city' + groupBy: (searchParams.get('groupBy') || 'country') as 'country' | 'city', + // 添加团队、项目和标签筛选 + teamIds: teamIds.length > 0 ? teamIds : undefined, + projectIds: projectIds.length > 0 ? projectIds : undefined, + tagIds: tagIds.length > 0 ? tagIds : undefined }); const response: ApiResponse = { diff --git a/app/api/events/route.ts b/app/api/events/route.ts index b1a1013..cee45d4 100644 --- a/app/api/events/route.ts +++ b/app/api/events/route.ts @@ -35,10 +35,8 @@ export async function GET(request: NextRequest) { linkId, linkSlug, userId, - teamId: teamIds.length > 0 ? teamIds[0] : undefined, - teamIds: teamIds.length > 1 ? teamIds : undefined, - projectId: projectIds.length > 0 ? projectIds[0] : undefined, - projectIds: projectIds.length > 1 ? projectIds : undefined, + teamIds: teamIds.length > 0 ? teamIds : undefined, + projectIds: projectIds.length > 0 ? projectIds : undefined, tagIds: tagIds.length > 0 ? tagIds : undefined, startTime, endTime, diff --git a/app/components/ui/ProjectSelector.tsx b/app/components/ui/ProjectSelector.tsx index 2d7b6f0..9ca1d57 100644 --- a/app/components/ui/ProjectSelector.tsx +++ b/app/components/ui/ProjectSelector.tsx @@ -35,12 +35,14 @@ export function ProjectSelector({ className, multiple = false, teamId, + teamIds, }: { value?: string | string[]; onChange?: (projectId: string | string[]) => void; className?: string; multiple?: boolean; teamId?: string; // Optional team ID to filter projects by team + teamIds?: string[]; // Optional array of team IDs to filter projects by multiple teams }) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -49,6 +51,16 @@ export function ProjectSelector({ const [isOpen, setIsOpen] = useState(false); const selectorRef = useRef(null); + // Normalize team IDs to ensure we're always working with an array + const effectiveTeamIds = React.useMemo(() => { + if (teamIds && teamIds.length > 0) { + return teamIds; + } else if (teamId) { + return [teamId]; + } + return undefined; + }, [teamId, teamIds]); + // Initialize selected projects based on value prop useEffect(() => { if (value) { @@ -90,38 +102,59 @@ export function ProjectSelector({ try { const supabase = getSupabaseClient(); - let projectsQuery; - - if (teamId) { - // 如果提供了teamId,获取该团队的项目 - projectsQuery = supabase + if (effectiveTeamIds && effectiveTeamIds.length > 0) { + // If team IDs are provided, get projects for those teams + const { data: projectsData, error: projectsError } = await supabase .from('team_projects') .select('project_id, projects:project_id(*), teams:team_id(name)') - .eq('team_id', teamId) + .in('team_id', effectiveTeamIds) .is('projects.deleted_at', null); + + if (projectsError) throw projectsError; + + if (!projectsData || projectsData.length === 0) { + if (isMounted) setProjects([]); + return; + } + + // Extract projects from response with team info + if (isMounted) { + const projectList: Project[] = []; + + for (const item of projectsData as ProjectWithTeam[]) { + if (item.projects && typeof item.projects === 'object' && 'id' in item.projects && 'name' in item.projects) { + const project = item.projects as Project; + if (item.teams && 'name' in item.teams) { + project.team_name = item.teams.name; + } + // Avoid duplicate projects from different teams + if (!projectList.some(p => p.id === project.id)) { + projectList.push(project); + } + } + } + + setProjects(projectList); + } } else { - // 否则,获取用户所属的所有项目及其所属团队 - projectsQuery = supabase + // If no team IDs, get all user's projects + const { data: projectsData, error: projectsError } = await supabase .from('user_projects') .select('project_id, projects:project_id(*)') .eq('user_id', userId) .is('projects.deleted_at', null); - } - const { data: projectsData, error: projectsError } = await projectsQuery; + if (projectsError) throw projectsError; - if (projectsError) throw projectsError; + if (!projectsData || projectsData.length === 0) { + if (isMounted) setProjects([]); + return; + } - if (!projectsData || projectsData.length === 0) { - if (isMounted) setProjects([]); - return; - } - - // 如果没有提供teamId,需要单独获取每个项目对应的团队信息 - if (!teamId && projectsData.length > 0) { + // Fetch team info for these projects const projectIds = projectsData.map(item => item.project_id); - // 获取项目所属的团队信息 + // Get team info for each project const { data: teamProjectsData, error: teamProjectsError } = await supabase .from('team_projects') .select('project_id, teams:team_id(name)') @@ -129,7 +162,7 @@ export function ProjectSelector({ if (teamProjectsError) throw teamProjectsError; - // 创建项目ID到团队名称的映射 + // Create project ID to team name mapping const projectTeamMap: Record = {}; if (teamProjectsData) { teamProjectsData.forEach(item => { @@ -139,7 +172,7 @@ export function ProjectSelector({ }); } - // 提取项目数据,并添加团队名称 + // Extract projects with team names if (isMounted && projectsData) { const projectList: Project[] = []; @@ -151,23 +184,6 @@ export function ProjectSelector({ } } - setProjects(projectList); - } - } else { - // 如果提供了teamId,直接从查询结果中提取项目和团队信息 - if (isMounted && projectsData) { - const projectList: Project[] = []; - - for (const item of projectsData as ProjectWithTeam[]) { - if (item.projects && typeof item.projects === 'object' && 'id' in item.projects && 'name' in item.projects) { - const project = item.projects as Project; - if (item.teams && 'name' in item.teams) { - project.team_name = item.teams.name; - } - projectList.push(project); - } - } - setProjects(projectList); } } @@ -203,7 +219,7 @@ export function ProjectSelector({ isMounted = false; subscription.unsubscribe(); }; - }, [teamId]); + }, [effectiveTeamIds]); const handleToggle = () => { if (!loading && !error && projects.length > 0) { diff --git a/app/components/ui/TagSelector.tsx b/app/components/ui/TagSelector.tsx index 754a1bc..f687678 100644 --- a/app/components/ui/TagSelector.tsx +++ b/app/components/ui/TagSelector.tsx @@ -30,14 +30,14 @@ export function TagSelector({ className, multiple = false, teamId, - tagType, + teamIds, }: { value?: string | string[]; - onChange?: (tagId: string | string[]) => void; + onChange?: (tagIds: string | string[]) => void; className?: string; multiple?: boolean; - teamId?: string; // Optional team ID to filter tags by team - tagType?: string; // Optional tag type for filtering + teamId?: string; // Optional single team ID + teamIds?: string[]; // Optional array of team IDs }) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -46,6 +46,16 @@ export function TagSelector({ const [isOpen, setIsOpen] = useState(false); const selectorRef = useRef(null); + // Normalize team IDs to ensure we're always working with an array + const effectiveTeamIds = React.useMemo(() => { + if (teamIds && teamIds.length > 0) { + return teamIds; + } else if (teamId) { + return [teamId]; + } + return undefined; + }, [teamId, teamIds]); + // 标签名称与ID的映射函数 const getTagIdByName = (name: string): string | undefined => { const tag = tags.find(t => t.name === name); @@ -119,13 +129,8 @@ export function TagSelector({ let query = supabase.from('tags').select('*').is('deleted_at', null); // Filter by team if teamId is provided - if (teamId) { - query = query.eq('team_id', teamId); - } - - // Filter by tag type if provided - if (tagType) { - query = query.eq('type', tagType); + if (effectiveTeamIds) { + query = query.in('team_id', effectiveTeamIds); } const { data: tagsData, error: tagsError } = await query; @@ -170,7 +175,7 @@ export function TagSelector({ isMounted = false; subscription.unsubscribe(); }; - }, [teamId, tagType]); + }, [effectiveTeamIds]); const handleToggle = () => { if (!loading && !error && tags.length > 0) { diff --git a/app/page.tsx b/app/page.tsx index c343440..9d92191 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -248,7 +248,18 @@ export default function HomePage() {
setSelectedTeamIds(Array.isArray(value) ? value : [value])} + onChange={(value) => { + const newTeamIds = Array.isArray(value) ? value : [value]; + + // Check if team selection has changed + if (JSON.stringify(newTeamIds) !== JSON.stringify(selectedTeamIds)) { + // Clear project selection when team changes + setSelectedProjectIds([]); + + // Update team selection + setSelectedTeamIds(newTeamIds); + } + }} className="w-[250px]" multiple={true} /> @@ -257,14 +268,14 @@ export default function HomePage() { onChange={(value) => setSelectedProjectIds(Array.isArray(value) ? value : [value])} className="w-[250px]" multiple={true} - teamId={selectedTeamIds.length === 1 ? selectedTeamIds[0] : undefined} + teamIds={selectedTeamIds.length > 0 ? selectedTeamIds : undefined} /> setSelectedTagIds(Array.isArray(value) ? value : [value])} className="w-[250px]" multiple={true} - teamId={selectedTeamIds.length === 1 ? selectedTeamIds[0] : undefined} + teamIds={selectedTeamIds.length > 0 ? selectedTeamIds : undefined} /> { const filter = buildFilter(params); @@ -213,9 +216,12 @@ export async function getGeoAnalytics(params: { endTime?: string; linkId?: string; groupBy?: 'country' | 'city'; + teamIds?: string[]; + projectIds?: string[]; + tagIds?: string[]; }): Promise { const filter = buildFilter(params); - const groupByField = 'ip_address'; // 暂时按 IP 地址分组 + const groupByField = params.groupBy === 'city' ? 'city' : 'country'; const query = ` SELECT @@ -228,7 +234,7 @@ export async function getGeoAnalytics(params: { GROUP BY ${groupByField} HAVING location != '' ORDER BY visits DESC - LIMIT 10 + LIMIT 20 `; return executeQuery(query);