diff --git a/app/(app)/analytics/page.tsx b/app/(app)/analytics/page.tsx index 2f8bf15..e19aebf 100644 --- a/app/(app)/analytics/page.tsx +++ b/app/(app)/analytics/page.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { subDays } from 'date-fns'; import { DateRangePicker } from '@/app/components/ui/DateRangePicker'; import { TeamSelector } from '@/app/components/ui/TeamSelector'; +import { ProjectSelector } from '@/app/components/ui/ProjectSelector'; export default function AnalyticsPage() { // 默认日期范围为最近7天 @@ -15,6 +16,9 @@ export default function AnalyticsPage() { // 添加团队选择状态 - 使用数组支持多选 const [selectedTeamIds, setSelectedTeamIds] = useState([]); + + // 添加项目选择状态 - 使用数组支持多选 + const [selectedProjectIds, setSelectedProjectIds] = useState([]); return (
@@ -28,6 +32,13 @@ export default function AnalyticsPage() { className="w-[250px]" multiple={true} /> + setSelectedProjectIds(Array.isArray(value) ? value : [value])} + className="w-[250px]" + multiple={true} + teamId={selectedTeamIds.length === 1 ? selectedTeamIds[0] : undefined} + />
- {/* 如果没有选择团队,显示提示信息 */} - {selectedTeamIds.length === 0 && ( + {/* 如果没有选择团队或项目,显示提示信息 */} + {selectedTeamIds.length === 0 && selectedProjectIds.length === 0 && (

- Please select one or more teams to view analytics + Please select teams or projects to view analytics

)} - {/* 如果选择了团队,这里可以显示团队相关的分析数据 */} + {/* 显示团队相关的分析数据 */} {selectedTeamIds.length > 0 && ( -
-
-

- Analytics for {selectedTeamIds.length} selected {selectedTeamIds.length === 1 ? 'team' : 'teams'} -

-
- {/* You can map through selectedTeamIds and display data for each team */} - {selectedTeamIds.map((teamId) => ( -
-

Team ID: {teamId}

-

Team analytics will appear here

-
- ))} -
+
+

+ Team Analytics ({selectedTeamIds.length} selected) +

+
+ {selectedTeamIds.map((teamId) => ( +
+

Team ID: {teamId}

+

Team analytics will appear here

+
+ ))} +
+
+ )} + + {/* 显示项目相关的分析数据 */} + {selectedProjectIds.length > 0 && ( +
+

+ Project Analytics ({selectedProjectIds.length} selected) +

+
+ {selectedProjectIds.map((projectId) => ( +
+

Project ID: {projectId}

+

Project analytics will appear here

+
+ ))}
)} diff --git a/app/components/ui/ProjectSelector.tsx b/app/components/ui/ProjectSelector.tsx new file mode 100644 index 0000000..af53ba5 --- /dev/null +++ b/app/components/ui/ProjectSelector.tsx @@ -0,0 +1,292 @@ +"use client"; + +import * as React from 'react'; +import { useEffect, useState, useRef } from 'react'; +import { getSupabaseClient } from '../../utils/supabase'; +import { AuthChangeEvent, Session } from '@supabase/supabase-js'; +import { Loader2, X, Check } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +// Define our own Project type +interface Project { + id: string; + name: string; + description?: string | null; + attributes?: Record; + created_at?: string; + updated_at?: string; + deleted_at?: string | null; + schema_version?: number | null; + creator_id?: string | null; +} + +// ProjectSelector component with multi-select support +export function ProjectSelector({ + value, + onChange, + className, + multiple = false, + teamId, +}: { + value?: string | string[]; + onChange?: (projectId: string | string[]) => void; + className?: string; + multiple?: boolean; + teamId?: string; // Optional team ID to filter projects by team +}) { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [projects, setProjects] = useState([]); + const [selectedIds, setSelectedIds] = useState([]); + const [isOpen, setIsOpen] = useState(false); + const selectorRef = useRef(null); + + // Initialize selected projects based on value prop + useEffect(() => { + if (value) { + if (Array.isArray(value)) { + setSelectedIds(value); + } else { + setSelectedIds(value ? [value] : []); + } + } else { + setSelectedIds([]); + } + }, [value]); + + // Add click outside listener to close dropdown + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (selectorRef.current && !selectorRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + } + + // Only add the event listener if the dropdown is open + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + } + }, [isOpen]); + + useEffect(() => { + let isMounted = true; + + const fetchProjects = async (userId: string) => { + if (!isMounted) return; + setLoading(true); + setError(null); + + try { + const supabase = getSupabaseClient(); + + let projectsQuery; + + if (teamId) { + // If a teamId is provided, fetch projects for that team + projectsQuery = supabase + .from('team_projects') + .select('project_id, projects:project_id(*)') + .eq('team_id', teamId) + .is('projects.deleted_at', null); + } else { + // Otherwise, fetch projects the user is a member of + projectsQuery = 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 (!projectsData || projectsData.length === 0) { + if (isMounted) setProjects([]); + return; + } + + // Extract the project data from the query results + if (isMounted && projectsData && projectsData.length > 0) { + const projectList: Project[] = []; + + for (const item of projectsData) { + if (item.projects && typeof item.projects === 'object' && 'id' in item.projects && 'name' in item.projects) { + projectList.push(item.projects as Project); + } + } + + setProjects(projectList); + } + } catch (err) { + if (isMounted) { + setError(err instanceof Error ? err.message : 'Failed to load projects'); + } + } finally { + if (isMounted) { + setLoading(false); + } + } + }; + + const supabase = getSupabaseClient(); + + const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => { + if (event === 'SIGNED_IN' && session?.user?.id) { + fetchProjects(session.user.id); + } else if (event === 'SIGNED_OUT') { + setProjects([]); + setError(null); + } + }); + + supabase.auth.getSession().then(({ data: { session } }) => { + if (session?.user?.id) { + fetchProjects(session.user.id); + } + }); + + return () => { + isMounted = false; + subscription.unsubscribe(); + }; + }, [teamId]); + + const handleToggle = () => { + if (!loading && !error && projects.length > 0) { + setIsOpen(!isOpen); + } + }; + + const handleProjectSelect = (projectId: string) => { + let newSelected: string[]; + + if (multiple) { + // For multi-select: toggle project in/out of selection + if (selectedIds.includes(projectId)) { + newSelected = selectedIds.filter(id => id !== projectId); + } else { + newSelected = [...selectedIds, projectId]; + } + } else { + // For single-select: replace selection with the new project + newSelected = [projectId]; + setIsOpen(false); + } + + setSelectedIds(newSelected); + + if (onChange) { + onChange(multiple ? newSelected : newSelected[0] || ''); + } + }; + + const removeProject = (e: React.MouseEvent, projectId: string) => { + e.stopPropagation(); + const newSelected = selectedIds.filter(id => id !== projectId); + setSelectedIds(newSelected); + if (onChange) { + onChange(multiple ? newSelected : newSelected[0] || ''); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ {error} +
+ ); + } + + if (projects.length === 0) { + return ( +
+ No projects available +
+ ); + } + + const selectedProjects = projects.filter(project => selectedIds.includes(project.id)); + + return ( +
+
+ {selectedProjects.length > 0 ? ( +
+ {selectedProjects.map(project => ( +
+ {project.name} + {multiple && ( + removeProject(e, project.id)} + /> + )} +
+ ))} +
+ ) : ( +
Select a project
+ )} +
+ + {isOpen && ( +
+ {projects.map(project => ( +
handleProjectSelect(project.id)} + > + + {project.name} + {project.description && ( + + {project.description} + + )} + + {selectedIds.includes(project.id) && ( + + )} +
+ ))} +
+ )} +
+ ); +} \ No newline at end of file