diff --git a/useTeam.ts b/useTeam.ts new file mode 100644 index 0000000..ce9fdea --- /dev/null +++ b/useTeam.ts @@ -0,0 +1,172 @@ +import { useState, useEffect, useCallback } from 'react' + +export type EmployeeRole = 'OWNER' | 'MANAGER' | 'AGENT' +export type InvitationStatus = 'accepted' | 'pending' | 'revoked' + +export interface TeamMember { + id: string + firstName: string + lastName: string + email: string + phone: string | null + role: EmployeeRole + isActive: boolean + createdAt: string + updatedAt: string + lastActiveAt: string | null + invitationStatus: InvitationStatus +} + +export interface TeamStats { + total: number + active: number + pending: number + inactive: number +} + +export interface InvitePayload { + firstName: string + lastName: string + email: string + role: 'MANAGER' | 'AGENT' +} + +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'https://api.rentflow.com/api/v1' + +async function apiFetch( + path: string, + options?: RequestInit +): Promise { + const res = await fetch(`${API_BASE}${path}`, { + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + ...options, + }) + + const json = await res.json() + + if (!res.ok) { + const err = new Error(json.message ?? 'Request failed') as any + err.code = json.error + err.statusCode = res.status + throw err + } + + return json.data as T +} + +// ─── useTeam ───────────────────────────────────────────────── + +export function useTeam() { + const [members, setMembers] = useState([]) + const [stats, setStats] = useState({ total: 0, active: 0, pending: 0, inactive: 0 }) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const fetchMembers = useCallback(async () => { + setLoading(true) + setError(null) + try { + const [membersData, statsData] = await Promise.all([ + apiFetch('/team'), + apiFetch('/team/stats'), + ]) + setMembers(membersData) + setStats(statsData) + } catch (err: any) { + setError(err.message) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { fetchMembers() }, [fetchMembers]) + + // ── Invite ───────────────────────────────────────────────── + + const invite = useCallback(async (payload: InvitePayload) => { + const result = await apiFetch<{ employee: TeamMember; invitationId: string }>( + '/team/invite', + { method: 'POST', body: JSON.stringify(payload) } + ) + // Optimistically add the pending member + setMembers((prev) => [...prev, result.employee]) + setStats((prev) => ({ + ...prev, + total: prev.total + 1, + pending: prev.pending + 1, + })) + return result + }, []) + + // ── Update role ──────────────────────────────────────────── + + const updateRole = useCallback(async (memberId: string, role: 'MANAGER' | 'AGENT') => { + const updated = await apiFetch(`/team/${memberId}/role`, { + method: 'PATCH', + body: JSON.stringify({ role }), + }) + setMembers((prev) => + prev.map((m) => (m.id === memberId ? { ...m, role: updated.role } : m)) + ) + return updated + }, []) + + // ── Deactivate ───────────────────────────────────────────── + + const deactivate = useCallback(async (memberId: string) => { + const updated = await apiFetch(`/team/${memberId}/deactivate`, { + method: 'POST', + }) + setMembers((prev) => + prev.map((m) => (m.id === memberId ? { ...m, isActive: false } : m)) + ) + setStats((prev) => ({ ...prev, active: prev.active - 1, inactive: prev.inactive + 1 })) + return updated + }, []) + + // ── Reactivate ───────────────────────────────────────────── + + const reactivate = useCallback(async (memberId: string) => { + const updated = await apiFetch(`/team/${memberId}/reactivate`, { + method: 'POST', + }) + setMembers((prev) => + prev.map((m) => (m.id === memberId ? { ...m, isActive: true } : m)) + ) + setStats((prev) => ({ ...prev, active: prev.active + 1, inactive: prev.inactive - 1 })) + return updated + }, []) + + // ── Remove ───────────────────────────────────────────────── + + const remove = useCallback(async (memberId: string) => { + await apiFetch<{ success: boolean }>(`/team/${memberId}`, { + method: 'DELETE', + }) + const target = members.find((m) => m.id === memberId) + setMembers((prev) => prev.filter((m) => m.id !== memberId)) + setStats((prev) => ({ + ...prev, + total: prev.total - 1, + active: target?.isActive ? prev.active - 1 : prev.active, + pending: target?.invitationStatus === 'pending' ? prev.pending - 1 : prev.pending, + inactive: !target?.isActive && target?.invitationStatus === 'accepted' + ? prev.inactive - 1 + : prev.inactive, + })) + }, [members]) + + return { + members, + stats, + loading, + error, + refetch: fetchMembers, + invite, + updateRole, + deactivate, + reactivate, + remove, + } +}