195 lines
5.8 KiB
TypeScript
195 lines
5.8 KiB
TypeScript
import { AutoComplete, Button, Drawer, Flex, Form, message, Modal, Select, Spin, Typography } from '@/shared/antd-imports';
|
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
|
import {
|
|
toggleInviteMemberDrawer,
|
|
triggerTeamMembersRefresh,
|
|
} from '../../../features/settings/member/memberSlice';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { jobTitlesApiService } from '@/api/settings/job-titles/job-titles.api.service';
|
|
import { IJobTitle } from '@/types/job.types';
|
|
import { teamMembersApiService } from '@/api/team-members/teamMembers.api.service';
|
|
import { ITeamMemberCreateRequest } from '@/types/teamMembers/team-member-create-request';
|
|
import { LinkOutlined } from '@ant-design/icons';
|
|
|
|
interface FormValues {
|
|
email: string[];
|
|
jobTitle: string;
|
|
access: 'member' | 'admin';
|
|
}
|
|
|
|
const InviteTeamMembers = () => {
|
|
const [searching, setSearching] = useState(false);
|
|
const [jobTitles, setJobTitles] = useState<IJobTitle[]>([]);
|
|
const [emails, setEmails] = useState<string[]>([]);
|
|
const [selectedJobTitle, setSelectedJobTitle] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const [form] = Form.useForm<FormValues>();
|
|
|
|
const { t } = useTranslation('settings/team-members');
|
|
const isDrawerOpen = useAppSelector(state => state.memberReducer.isInviteMemberDrawerOpen);
|
|
const dispatch = useAppDispatch();
|
|
|
|
const handleSearch = useCallback(
|
|
async (value: string) => {
|
|
try {
|
|
setSearching(true);
|
|
const res = await jobTitlesApiService.getJobTitles(1, 10, null, null, value || null);
|
|
if (res.done) {
|
|
setJobTitles(res.body.data || []);
|
|
}
|
|
} catch (error) {
|
|
message.error(t('Failed to fetch job titles'));
|
|
} finally {
|
|
setSearching(false);
|
|
}
|
|
},
|
|
[t]
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (isDrawerOpen) {
|
|
handleSearch('');
|
|
}
|
|
}, [isDrawerOpen, handleSearch]);
|
|
|
|
const handleFormSubmit = async (values: FormValues) => {
|
|
try {
|
|
setLoading(true);
|
|
const body: ITeamMemberCreateRequest = {
|
|
job_title: selectedJobTitle,
|
|
emails: emails,
|
|
is_admin: values.access === 'admin',
|
|
};
|
|
const res = await teamMembersApiService.createTeamMember(body);
|
|
if (res.done) {
|
|
form.resetFields();
|
|
setEmails([]);
|
|
setSelectedJobTitle(null);
|
|
dispatch(triggerTeamMembersRefresh()); // Trigger refresh in TeamMembersSettings
|
|
dispatch(toggleInviteMemberDrawer());
|
|
}
|
|
} catch (error) {
|
|
message.error(t('createMemberErrorMessage'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleClose = () => {
|
|
form.resetFields();
|
|
dispatch(toggleInviteMemberDrawer());
|
|
};
|
|
|
|
const handleEmailChange = (value: string[]) => {
|
|
setEmails(value);
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
title={
|
|
<Typography.Text strong style={{ fontSize: 16 }}>
|
|
{t('addMemberDrawerTitle')}
|
|
</Typography.Text>
|
|
}
|
|
open={isDrawerOpen}
|
|
onCancel={handleClose}
|
|
destroyOnClose
|
|
afterOpenChange={visible => visible && handleSearch('')}
|
|
width={400}
|
|
loading={loading}
|
|
footer={
|
|
<Flex justify="space-between">
|
|
{/* <Button
|
|
style={{ width: 140, fontSize: 12 }}
|
|
block
|
|
icon={<LinkOutlined />}
|
|
disabled
|
|
>
|
|
{t('copyTeamLink')}
|
|
</Button> */}
|
|
<Flex justify="end">
|
|
<Button onClick={form.submit} style={{ fontSize: 12 }}>
|
|
{t('addToTeamButton')}
|
|
</Button>
|
|
</Flex>
|
|
</Flex>
|
|
}
|
|
>
|
|
<Form
|
|
form={form}
|
|
onFinish={handleFormSubmit}
|
|
layout="vertical"
|
|
initialValues={{ access: 'member' }}
|
|
>
|
|
<Form.Item
|
|
name="emails"
|
|
label={t('memberEmailLabel')}
|
|
rules={[
|
|
{
|
|
type: 'array',
|
|
required: true,
|
|
validator: (_, value) => {
|
|
if (!value?.length) return Promise.reject(t('memberEmailRequiredError'));
|
|
return Promise.resolve();
|
|
},
|
|
},
|
|
]}
|
|
>
|
|
<Flex vertical gap={4}>
|
|
<Select
|
|
mode="tags"
|
|
style={{ width: '100%' }}
|
|
placeholder={t('memberEmailPlaceholder')}
|
|
onChange={handleEmailChange}
|
|
notFoundContent={
|
|
<Typography.Text type="secondary">{t('noResultFound')}</Typography.Text>
|
|
}
|
|
tokenSeparators={[',', ' ', ';']}
|
|
/>
|
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
{t('addMemberEmailHint')}
|
|
</Typography.Text>
|
|
</Flex>
|
|
</Form.Item>
|
|
|
|
<Form.Item label={t('jobTitleLabel')} name="jobTitle">
|
|
<AutoComplete
|
|
options={jobTitles.map(job => ({
|
|
id: job.id,
|
|
label: job.name,
|
|
value: job.name,
|
|
}))}
|
|
allowClear
|
|
onSearch={handleSearch}
|
|
placeholder={t('jobTitlePlaceholder')}
|
|
onChange={(value, option) => {
|
|
form.setFieldsValue({ jobTitle: option?.label || value });
|
|
}}
|
|
onSelect={value => setSelectedJobTitle(value)}
|
|
dropdownRender={menu => (
|
|
<div>
|
|
{searching && <Spin size="small" />}
|
|
{menu}
|
|
</div>
|
|
)}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item label={t('memberAccessLabel')} name="access">
|
|
<Select
|
|
options={[
|
|
{ value: 'member', label: t('memberText') },
|
|
{ value: 'admin', label: t('adminText') },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default InviteTeamMembers;
|