Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 86 additions & 2 deletions actions/admin/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import {
PostRequest,
PatchRequest,
} from '@/base-api/method';
import { inviteMentorProps } from '@/types/requests';
import type { inviteMentorProps } from '@/types/requests';
import type { inviteAdminProps } from '@/types/requests';
import type { getStartedMentorFormValues } from '@/types/get-started';

import { revalidateTag } from 'next/cache';
const Url = {
adminAccount: `admin/account`,
adminMentors: `admin/mentor`,
mentorProfile: `mentor/profile`,
inviteAdmin: `admin/user`,
};

export const checkAccount = async (params: string) => {
Expand All @@ -30,7 +35,7 @@ export const createOrganazation = async (
'createOrg',
body
);
const data = await putRequest.putData();
const data = await putRequest.patchData();
return data;
};

Expand Down Expand Up @@ -59,6 +64,85 @@ export const inviteMentore = async (body: inviteMentorProps) => {
return data;
};

export const inviteAdmin = async (body: inviteAdminProps) => {
const postRequest = new PostRequest(
`${Url.inviteAdmin}`,
'invite-admin',
body
);
const data = postRequest.postData();
return data;
};

export const submitMentorForm = async (
formData: getStartedMentorFormValues,
accountId: string
) => {
// Transform data to match backend format
const backendData = {
expertise: formData.expertise.reduce(
(acc, exp) => {
const expertiseMap: Record<string, string> = {
marriageCounseling: 'Expert in marriage counseling',
discipleship: 'Expert in discipleship counseling',
spritual: 'Expert in spiritual counseling',
dayToDay: 'Expert in day-to-day counseling',
lifeCoach: 'Expert in life coaching',
psychology: 'Expert in psychological counseling',
};
const key =
exp
.replace(/Counseling$/, 'Counselor')
.replace(/([A-Z])/g, ' $1')
.trim()
.toLowerCase() + 'Counselor';
acc[key] = expertiseMap[exp] || `Expert in ${exp}`;
return acc;
},
{} as Record<string, string>
),
capacity: formData.capacity,
availability: Object.entries(formData.availability)
.filter(([_, value]) => value !== undefined)
.reduce(
(acc, [day, value]) => {
if (!value) return acc;

const formattedDay = day.charAt(0).toUpperCase() + day.slice(1);
const startTime = value.startTime;
const endTime = value.endTime;
const timeRange = `${startTime.hour}:${startTime.minute} ${startTime.dayPeriod} - ${endTime.hour}:${endTime.minute} ${endTime.dayPeriod}`;

acc[formattedDay] = [timeRange];
return acc;
},
{} as Record<string, string[]>
),
age: formData.age,
gender: formData.gender.toUpperCase(),
location: formData.location,
};

const url = `${Url.mentorProfile}?accountId=${accountId}`;

const patchRequest = new PatchRequest(url, 'submit-mentor-form', backendData);
return await patchRequest.patchData();
};

export const toggleMentorStatus = async (
mentorId: string | number,
accountId: string,
isActive: boolean
) => {
const url = `${Url.adminMentors}/${mentorId}/toggle-status?accountId=${accountId}`;
const patchRequest = new PatchRequest(url, 'toggle-mentor-status', {
isActive,
});
const data = await patchRequest.patchData();
revalidateTag('admin-mentors');
return data;
};

export async function revalidateWithLogging(tag: string) {
return revalidateTag(tag);
}
2 changes: 1 addition & 1 deletion base-api/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class PatchRequest extends ApiRequest {
super(url, tag, data, 'PATCH');
}

public async putData() {
public async patchData() {
return this.executeRequest();
}
}
Expand Down
121 changes: 121 additions & 0 deletions components/shared/admin/admin-mentors/invite-admin-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use client';
import { useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogTrigger,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { toast } from '@/hooks/use-toast';
import React from 'react';
import { InviteAdminFormData, InviteAdminDialogProps } from '@/types/admin';

import { inviteAdmin } from '@/actions/admin/admin';

export function InviteAdminDialog({
accountId,
roleId,
triggerState,
setTriggerState,
}: InviteAdminDialogProps) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [formData, setFormData] = useState<InviteAdminFormData>({
name: '',
email: '',
});

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const requestBody = {
accountId: accountId as string,
name: formData.name,
email: formData.email,
roleId: roleId as string,
};

try {
const response = await inviteAdmin(requestBody);
if (response.error) {
toast({
variant: 'destructive',
title: 'Error!',
description: response.error.message,
});
return;
}
toast({
variant: 'success',
title: 'Success!',
description: `Invitation sent to ${formData.email}`,
});
setFormData({ name: '', email: '' });
setIsOpen(false);
} catch (error) {
toast({
variant: 'destructive',
title: 'Error!',
description: 'Failed to send invitation.',
});
}
setTriggerState(!triggerState);
};

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};

return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant={'outline'}>
<span className="mr-1">+</span> Invite an Admin
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Invite An Admin</DialogTitle>
<DialogDescription>
Send an invitation link through their email
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 pt-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
name="name"
placeholder="Insert their name"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="Insert their email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<Button
type="submit"
className="w-full bg-[#0F172A] hover:bg-[#1E293B]"
>
Invite
</Button>
</form>
</DialogContent>
</Dialog>
);
}
77 changes: 64 additions & 13 deletions components/shared/admin/admin-mentors/mentors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import type { Column, FilterOption } from '@/types/data-table';
import { toast } from '@/hooks/use-toast';
import { InviteMentorDialog } from './invite-mentor-dialog';
import { endPoints } from '@/data/end-points';
import { deleteMentor } from '@/actions/admin/admin';
import { deleteMentor, toggleMentorStatus } from '@/actions/admin/admin';
import type { Account } from '@/types/users';
import { userProfile } from '@/actions/auth/login';
import { Mentor } from '@/types/mentor';
import { Switch } from '@/components/ui/switch';

const columns: Column<Mentor>[] = [
const createColumns = (
client: Account | null,
setTriggerState: React.Dispatch<React.SetStateAction<boolean>>
): Column<Mentor>[] => [
{
key: 'name',
header: 'Name',
Expand All @@ -30,41 +34,87 @@ const columns: Column<Mentor>[] = [
{
key: 'age',
header: 'Age',
render: (mentor: Mentor) => (mentor.age === null ? 'null' : mentor.age),
render: (mentor: Mentor) => (mentor.age === null ? 'N/A' : mentor.age),
},
{ key: 'gender', header: 'Gender' },
{
key: 'expertise',
header: 'Expertise',
render: (mentor: Mentor) =>
mentor.expertise === null ? 'null' : mentor.expertise,
mentor.expertise
? Object.entries(mentor.expertise)
.map(([key, description]) => `${key}: ${description}`)
.join(', ')
: 'N/A',
},
{
key: 'availability',
header: 'Availability',
render: (mentor: Mentor) =>
mentor.availability === null
? 'null'
: mentor.availability?.startDate || 'null',
mentor.availability
? Object.entries(mentor.availability)
.map(([day, times]) => `${day}: ${times.join(', ')}`)
.join('; ')
: 'N/A',
},
{
key: 'capacity',
header: 'Capacity',
render: (mentor: Mentor) =>
mentor.capacity === null ? 'N/A' : mentor.capacity,
},
{
key: 'location',
header: 'Location',
render: (mentor: Mentor) =>
mentor.location === null ? 'null' : mentor.location,
render: (mentor: Mentor) => (mentor.location ? mentor.location : 'N/A'),
},
{
key: 'isActive',
header: 'Status',
render: (mentor: Mentor) => (
<Badge variant="secondary">{mentor.isActive ? 'Yes' : 'No'}</Badge>
<div className="flex items-center gap-2">
<Switch
checked={mentor.isActive}
onCheckedChange={async (checked) => {
try {
if (!client?.id) {
throw new Error('User account ID missing');
}

await toggleMentorStatus(String(mentor.id), client.id, checked);

mentor.isActive = checked;
setTriggerState((prev) => !prev);

toast({
variant: 'success',
title: 'Status Updated!',
description: `Mentor status has been ${checked ? 'activated' : 'deactivated'}.`,
});
} catch (error) {
console.error('Status toggle error:', error);
toast({
variant: 'destructive',
title: 'Error!',
description:
error instanceof Error
? error.message
: 'Failed to update mentor status',
});
}
}}
/>
<Badge variant={mentor.isActive ? 'default' : 'secondary'}>
{mentor.isActive ? 'Active' : 'Inactive'}
</Badge>
</div>
),
},
];

const filterOptions: FilterOption<Mentor>[] = [
{ key: 'isActive', label: 'Yes' },
{ key: 'isActive', label: 'No' },
{ key: 'gender', label: 'FEMALE' },
{ key: 'gender', label: 'MALE' },
];

const searchFields: (keyof Mentor)[] = ['name', 'email', 'gender', 'isActive'];
Expand All @@ -81,6 +131,7 @@ const MentorsTable: React.FC = () => {
};
fetchUserProfile();
}, []);

const endPoint = `${endPoints.adminMentors}?accountId=${clientUser?.id}`;
const [itemsPerPage, onItemsPerPageChange] = useState<number>(10);

Expand Down Expand Up @@ -130,7 +181,7 @@ const MentorsTable: React.FC = () => {
<DataTable<Mentor>
tag="admin-mentors"
apiUrl={endPoint}
columns={columns}
columns={createColumns(clientUser, setTriggerState)}
searchFields={searchFields}
filterOptions={filterOptions}
itemsPerPage={itemsPerPage}
Expand Down
Loading