diff --git a/src/components/teams/TeamManagement.tsx b/src/components/teams/TeamManagement.tsx index e9fb816a3..9ef8d4d8c 100644 --- a/src/components/teams/TeamManagement.tsx +++ b/src/components/teams/TeamManagement.tsx @@ -1,23 +1,82 @@ -import { Component, createSignal } from 'solid-js'; -import { A } from '@solidjs/router'; -import { useAuth } from '../../lib/auth'; -import { Team } from '../../lib/auth/types'; +import { Component, createSignal, For, Show } from 'solid-js'; +import { A, useLocation } from '@solidjs/router'; +import { createTRPCClient } from '../../trpc/client'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import type { Team as DbTeam, TeamMember as DbTeamMember } from '../../db/schema/teams'; -export const TeamManagement: Component = () => { - const auth = useAuth(); - const [teams] = createSignal([ - { - id: 'team_default', - name: 'My Workspace', - members: [ - { userId: auth().user?.id || '', role: 'owner', joinedAt: new Date().toISOString() }, - ], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }, - ]); +const API_URL = (import.meta as any).env?.VITE_API_URL || 'http://localhost:8080'; + +function formatDateTime(iso: Date | string): string { + return new Date(iso).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); +} + +const TeamList: Component = () => { + const queryClient = useQueryClient(); + const client = createTRPCClient(API_URL); const [showCreateDialog, setShowCreateDialog] = createSignal(false); const [newTeamName, setNewTeamName] = createSignal(''); + const [error, setError] = createSignal(''); + + const teamsQuery = useQuery({ + queryKey: ['team', 'listTeams'], + queryFn: async () => { + const auth = localStorage.getItem('clerk_token'); + const res = await fetch(`${API_URL}/trpc/team.listTeams`, { + headers: auth ? { Authorization: `Bearer ${auth}` } : {}, + }); + if (!res.ok) throw new Error('Failed to fetch teams'); + const data = await res.json(); + return data.data as DbTeam[]; + }, + }); + + const createTeamMutation = useMutation({ + mutationFn: async (name: string) => { + const auth = localStorage.getItem('clerk_token'); + const res = await fetch(`${API_URL}/trpc/team.createTeam`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(auth ? { Authorization: `Bearer ${auth}` } : {}), + }, + body: JSON.stringify({ input: { name } }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.message || 'Failed to create team'); + } + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['team', 'listTeams'] }); + setShowCreateDialog(false); + setNewTeamName(''); + }, + onError: (err) => setError(err.message), + }); + + const deleteTeamMutation = useMutation({ + mutationFn: async (id: string) => { + const auth = localStorage.getItem('clerk_token'); + const res = await fetch(`${API_URL}/trpc/team.deleteTeam`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(auth ? { Authorization: `Bearer ${auth}` } : {}), + }, + body: JSON.stringify({ input: { id } }), + }); + if (!res.ok) throw new Error('Failed to delete team'); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['team', 'listTeams'] }); + }, + }); return (
@@ -25,29 +84,70 @@ export const TeamManagement: Component = () => {

Teams

-
- {teams().map((team) => ( - -
👥
-

{team.name}

-

{team.members.length} member{team.members.length !== 1 ? 's' : ''}

- - Created {new Date(team.createdAt).toLocaleDateString()} - -
- ))} + {error() && ( +
+ {error()} + +
+ )} - -
+ {teamsQuery.isLoading ? ( +
Loading teams...
+ ) : teamsQuery.error ? ( +
+
⚠️
+

Failed to load teams

+

{teamsQuery.error.message}

+ +
+ ) : ( +
+ + {(team) => ( +
+ +
👥
+

{team.name}

+ + Created {formatDateTime(team.createdAt)} + +
+
+ +
+
+ )} +
+ + +
+ )} {showCreateDialog() && (
setShowCreateDialog(false)}> @@ -58,9 +158,10 @@ export const TeamManagement: Component = () => {
{ e.preventDefault(); - if (newTeamName().trim()) { - setShowCreateDialog(false); - setNewTeamName(''); + const name = newTeamName().trim(); + if (name) { + setError(''); + createTeamMutation.mutate(name); } }}>
@@ -73,11 +174,22 @@ export const TeamManagement: Component = () => { value={newTeamName()} onInput={(e) => setNewTeamName(e.target.value)} autofocus + required />
- - +
@@ -88,3 +200,305 @@ export const TeamManagement: Component = () => { ); }; + +const TeamDetail: Component<{ teamId: string }> = ({ teamId }) => { + const queryClient = useQueryClient(); + const [error, setError] = createSignal(''); + const [showInviteDialog, setShowInviteDialog] = createSignal(false); + const [inviteUserId, setInviteUserId] = createSignal(''); + const [inviteRole, setInviteRole] = createSignal<'editor' | 'viewer' | 'admin'>('editor'); + + const teamQuery = useQuery({ + queryKey: ['team', 'getTeam', teamId], + queryFn: async () => { + const auth = localStorage.getItem('clerk_token'); + const res = await fetch(`${API_URL}/trpc/team.getTeam?input=${encodeURIComponent(JSON.stringify({ id: teamId }))}`, { + headers: auth ? { Authorization: `Bearer ${auth}` } : {}, + }); + if (!res.ok) throw new Error('Failed to fetch team'); + const data = await res.json(); + return data.data as DbTeam; + }, + }); + + const membersQuery = useQuery({ + queryKey: ['team', 'listMembers', teamId], + queryFn: async () => { + const auth = localStorage.getItem('clerk_token'); + const res = await fetch(`${API_URL}/trpc/team.listMembers?input=${encodeURIComponent(JSON.stringify({ teamId }))}`, { + headers: auth ? { Authorization: `Bearer ${auth}` } : {}, + }); + if (!res.ok) throw new Error('Failed to fetch members'); + const data = await res.json(); + return data.data as DbTeamMember[]; + }, + }); + + const updateRoleMutation = useMutation({ + mutationFn: async ({ userId, role }: { userId: number; role: string }) => { + const auth = localStorage.getItem('clerk_token'); + const res = await fetch(`${API_URL}/trpc/team.updateMemberRole`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(auth ? { Authorization: `Bearer ${auth}` } : {}), + }, + body: JSON.stringify({ input: { teamId, userId, role } }), + }); + if (!res.ok) throw new Error('Failed to update role'); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['team', 'listMembers', teamId] }); + }, + onError: (err) => setError(err.message), + }); + + const removeMemberMutation = useMutation({ + mutationFn: async (userId: number) => { + const auth = localStorage.getItem('clerk_token'); + const res = await fetch(`${API_URL}/trpc/team.removeMember`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(auth ? { Authorization: `Bearer ${auth}` } : {}), + }, + body: JSON.stringify({ input: { teamId, userId } }), + }); + if (!res.ok) throw new Error('Failed to remove member'); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['team', 'listMembers', teamId] }); + }, + onError: (err) => setError(err.message), + }); + + const leaveTeamMutation = useMutation({ + mutationFn: async () => { + const auth = localStorage.getItem('clerk_token'); + const res = await fetch(`${API_URL}/trpc/team.leaveTeam`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(auth ? { Authorization: `Bearer ${auth}` } : {}), + }, + body: JSON.stringify({ input: { teamId } }), + }); + if (!res.ok) throw new Error('Failed to leave team'); + return res.json(); + }, + onSuccess: () => { + window.location.href = '/teams'; + }, + onError: (err) => setError(err.message), + }); + + const addMemberMutation = useMutation({ + mutationFn: async ({ userId, role }: { userId: number; role: string }) => { + const auth = localStorage.getItem('clerk_token'); + const res = await fetch(`${API_URL}/trpc/team.addMember`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(auth ? { Authorization: `Bearer ${auth}` } : {}), + }, + body: JSON.stringify({ input: { teamId, userId, role } }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.message || 'Failed to add member'); + } + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['team', 'listMembers', teamId] }); + setShowInviteDialog(false); + setInviteUserId(''); + }, + onError: (err) => setError(err.message), + }); + + return ( +
+
+
+ ← Back to Teams +

+ {teamQuery.isLoading ? 'Loading...' : teamQuery.data?.name || 'Team'} +

+

+ Created {teamQuery.data ? formatDateTime(teamQuery.data.createdAt) : ''} +

+
+
+ + +
+
+ + {error() && ( +
+ {error()} + +
+ )} + +
+

Members ({membersQuery.data?.length || 0})

+ + {membersQuery.isLoading ? ( +
Loading members...
+ ) : membersQuery.error ? ( +
+

Failed to load members: {membersQuery.error.message}

+ +
+ ) : (membersQuery.data || []).length === 0 ? ( +
+
👤
+

No members yet

+

Invite people to collaborate with your team.

+
+ ) : ( +
+ + {(member) => ( +
+
+ User #{member.userId} + Joined {formatDateTime(member.joinedAt)} +
+
+ + +
+
+ )} +
+
+ )} +
+ + {showInviteDialog() && ( +
setShowInviteDialog(false)}> +
e.stopPropagation()}> +
+

Invite Member

+ +
+ { + e.preventDefault(); + setError(''); + const userId = parseInt(inviteUserId(), 10); + if (isNaN(userId) || userId <= 0) { + setError('Please enter a valid user ID.'); + return; + } + addMemberMutation.mutate({ userId, role: inviteRole() }); + }}> +
+ + setInviteUserId(e.target.value)} + required + /> +
+
+ + +
+
+ + +
+ +
+
+ )} +
+ ); +}; + +export const TeamManagement: Component = (props) => { + const location = useLocation(); + const teamId = () => { + const match = location.pathname.match(/^\/teams\/([^/]+)$/); + return match ? match[1] : null; + }; + + return ( + <> + }> + + + + ); +}; + +