From 3f8b0b48eec970cb8afc1556830ff59fb269a569 Mon Sep 17 00:00:00 2001 From: Ouma Ronald Date: Mon, 12 Jan 2026 11:59:24 +0300 Subject: [PATCH 1/2] feat: add team-member approval and root layout fixes --- app/admin/users/page.tsx | 26 ++- .../users/pending-team-members-table.tsx | 150 ++++++++++++++++++ app/admin/users/user-management-actions.ts | 122 ++++++++++++++ app/globals.css | 70 ++++++++ app/layout.tsx | 33 ++++ 5 files changed, 399 insertions(+), 2 deletions(-) create mode 100644 app/admin/users/pending-team-members-table.tsx create mode 100644 app/globals.css create mode 100644 app/layout.tsx diff --git a/app/admin/users/page.tsx b/app/admin/users/page.tsx index ad75d01..4e6d205 100644 --- a/app/admin/users/page.tsx +++ b/app/admin/users/page.tsx @@ -2,12 +2,13 @@ import { redirect } from "next/navigation" import { createClient } from "@/lib/supabase/server" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" -import { Users, UserPlus, Clock } from "lucide-react" +import { Users, UserPlus, Clock, UsersRound } from "lucide-react" import Link from "next/link" import { Button } from "@/components/ui/button" import { PendingRegistrationsTable } from "./pending-registrations-table" +import { PendingTeamMembersTable } from "./pending-team-members-table" import { ExistingUsersTable } from "./existing-users-table" -import { getPendingRegistrations, getAllUsers } from "./user-management-actions" +import { getPendingRegistrations, getPendingTeamMembers, getAllUsers } from "./user-management-actions" async function updateUserRole(formData: FormData) { "use server" @@ -47,6 +48,7 @@ export default async function AdminUsersPage() { } const pendingRegistrations = await getPendingRegistrations() + const pendingTeamMembers = await getPendingTeamMembers() const allUsers = await getAllUsers() return ( @@ -76,6 +78,26 @@ export default async function AdminUsersPage() { + + +
+
+ + + Pending Team Members + + Approve team members from the Teams module and create user accounts +
+ + {pendingTeamMembers.length} pending + +
+
+ + + +
+

All Users

+ + + + + + ) +} diff --git a/app/admin/users/user-management-actions.ts b/app/admin/users/user-management-actions.ts index 5799ede..0062fbe 100644 --- a/app/admin/users/user-management-actions.ts +++ b/app/admin/users/user-management-actions.ts @@ -343,3 +343,125 @@ export async function getAllUsers() { return data || [] } + +export async function getPendingTeamMembers() { + const supabase = await createClient() + + // Get pending team members that don't have a user account yet + const { data: teamMembers, error: teamError } = await supabase + .from("team_members") + .select("*") + .eq("status", "pending") + .order("invited_at", { ascending: false }) + + if (teamError) { + console.error(" Error fetching team members:", teamError) + return [] + } + + if (!teamMembers || teamMembers.length === 0) { + return [] + } + + // Check which team members already have user accounts + const emails = teamMembers.map((tm) => tm.email) + const { data: existingUsers } = await supabase.from("profiles").select("email").in("email", emails) + + const existingEmails = new Set(existingUsers?.map((u) => u.email) || []) + + // Return only team members without user accounts + return teamMembers.filter((tm) => !existingEmails.has(tm.email)) +} + +export async function approveTeamMember(teamMemberId: string, assignedRole?: string) { + const supabase = await createClient() + const serviceClient = await getServiceClient() + + // Get current user + const { + data: { user: currentUser }, + } = await supabase.auth.getUser() + if (!currentUser) { + return { error: "Unauthorized" } + } + + // Get team member details + const { data: teamMember, error: teamError } = await supabase + .from("team_members") + .select("*") + .eq("id", teamMemberId) + .single() + + if (teamError || !teamMember) { + return { error: "Team member not found" } + } + + // Check if user already exists + const { data: existingUser } = await supabase.from("profiles").select("id").eq("email", teamMember.email).single() + + if (existingUser) { + return { error: "User account already exists for this email" } + } + + // Use assigned role or team member's role + const role = assignedRole || teamMember.role + + // Generate temporary password + const tempPassword = `Temp${Math.random().toString(36).slice(-8)}!` + + // Create auth user using service client + const { data: authUser, error: authError } = await serviceClient.auth.admin.createUser({ + email: teamMember.email, + password: tempPassword, + email_confirm: true, + user_metadata: { + first_name: teamMember.first_name, + last_name: teamMember.last_name, + }, + }) + + if (authError) { + console.error(" Auth creation error:", authError) + return { error: "Failed to create user account" } + } + + // Create profile + const { error: profileError } = await supabase.from("profiles").insert({ + id: authUser.user.id, + email: teamMember.email, + first_name: teamMember.first_name, + last_name: teamMember.last_name, + role: role, + is_admin: role === "admin", + requires_password_change: true, + is_active: true, + created_by: currentUser.id, + status: "active", + }) + + if (profileError) { + console.error(" Profile creation error:", profileError) + return { error: "Failed to create user profile" } + } + + // Update team member status to active + await supabase + .from("team_members") + .update({ + status: "active", + invitation_token: null, + }) + .eq("id", teamMemberId) + + // Log activity + await logActivity(currentUser.id, "approve_team_member", "users", authUser.user.id, { + email: teamMember.email, + assigned_role: role, + team_member_id: teamMemberId, + }) + + revalidatePath("/admin/users") + revalidatePath("/team") + + return { success: true, tempPassword, email: teamMember.email } +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..1243adc --- /dev/null +++ b/app/globals.css @@ -0,0 +1,70 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --radius: 0.5rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + border-color: hsl(var(--border)); + } + body { + background-color: hsl(var(--background)); + color: hsl(var(--foreground)); + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..bbcf514 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from "next" +import { Inter } from "next/font/google" +import { ThemeProvider } from "@/components/theme-provider" +import ClientLayout from "./client-layout" +import "./globals.css" + +const inter = Inter({ subsets: ["latin"] }) + +export const metadata: Metadata = { + title: "Exela PMS - Property Management System", + description: "Complete property management solution for landlords", +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + + {children} + + + + ) +} From af5a0a02f46c055de6077883d317c708265ff289 Mon Sep 17 00:00:00 2001 From: Jonus Green <86344954+jonusgreen@users.noreply.github.com> Date: Mon, 19 Jan 2026 09:43:18 +0300 Subject: [PATCH 2/2] Change default theme to 'light' in ThemeProvider --- app/layout.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index bbcf514..7f797a6 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -19,12 +19,12 @@ export default function RootLayout({ return ( - + {children}