diff --git a/public/locales/bg/person.json b/public/locales/bg/person.json index ef8c08897..859f7c3b8 100644 --- a/public/locales/bg/person.json +++ b/public/locales/bg/person.json @@ -40,7 +40,12 @@ "address": "Адрес", "organizer": "Организатор", "coordinator": "Координатор", - "beneficiary": "Бенефициент" + "beneficiary": "Бенефициент", + "organizerRelation": "Отношения с организатор", + "countryCode": "Държава", + "city": "Град", + "description": "Описание", + "disabled-tooltip": "Ролята не може да бъде премахната, потребителя е {{role}} в {{campaigns}} кампании" }, "cta": { "create": "Създай", diff --git a/public/locales/en/person.json b/public/locales/en/person.json index e9ce8a8cb..d1c490832 100644 --- a/public/locales/en/person.json +++ b/public/locales/en/person.json @@ -41,7 +41,12 @@ "address": "Address", "organizer": "Organizer", "coordinator": "Coordinator", - "beneficiary": "Beneficiary" + "beneficiary": "Beneficiary", + "organizerRelation": "Organizer relation", + "countryCode": "Country code", + "city": "City", + "description": "Description", + "disabled-tooltip": "Cannot remove role because the user is {{role}} in {{campaigns}} campaigns" }, "cta": { "create": "Create", diff --git a/src/components/admin/organizers/grid/DeleteModal.tsx b/src/components/admin/organizers/grid/DeleteModal.tsx index 0780b23a8..bbdb1e874 100644 --- a/src/components/admin/organizers/grid/DeleteModal.tsx +++ b/src/components/admin/organizers/grid/DeleteModal.tsx @@ -23,7 +23,7 @@ export default observer(function DeleteModal() { const { hideDelete, selectedRecord } = ModalStore const mutation = useMutation, AxiosError, string>({ - mutationFn: deleteOrganizer(selectedRecord.id), + mutationFn: deleteOrganizer(), onError: () => AlertStore.show(t('admin.alerts.delete-error'), 'error'), onSuccess: () => { hideDelete() diff --git a/src/components/common/form/CheckboxField.tsx b/src/components/common/form/CheckboxField.tsx index c9078af5d..665067c1e 100644 --- a/src/components/common/form/CheckboxField.tsx +++ b/src/components/common/form/CheckboxField.tsx @@ -1,24 +1,48 @@ +import { ChangeEvent } from 'react' + import { useField } from 'formik' import { useTranslation } from 'next-i18next' -import { Checkbox, FormControl, FormControlLabel, FormHelperText } from '@mui/material' +import { Checkbox, FormControl, FormControlLabel, FormHelperText, Tooltip } from '@mui/material' import { TranslatableField, translateError } from 'common/form/validation' export type CheckboxFieldProps = { name: string + disabled?: boolean + onChange?: (e: ChangeEvent) => void label: string | number | React.ReactElement + disabledTooltip?: string } -export default function CheckboxField({ name, label }: CheckboxFieldProps) { +export default function CheckboxField({ + name, + disabled, + onChange: handleChange, + label, + disabledTooltip, +}: CheckboxFieldProps) { const { t } = useTranslation() const [field, meta] = useField(name) const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : '' return ( - } - /> + + { + field.onChange(e) + if (handleChange) handleChange(e) + }} + /> + } + /> + {Boolean(meta.error) && {helperText}} ) diff --git a/src/components/common/person/PersonForm.tsx b/src/components/common/person/PersonForm.tsx index e5d6880c3..b3b32a69a 100644 --- a/src/components/common/person/PersonForm.tsx +++ b/src/components/common/person/PersonForm.tsx @@ -22,6 +22,10 @@ const validationSchema: yup.SchemaOf = yup.object().defined().sh companyNumber: yup.string(), legalPersonName: name, address: yup.string(), + // Roles + isBeneficiary: yup.bool().notRequired(), + isCoordinator: yup.bool().notRequired(), + isOrganizer: yup.bool().notRequired(), }) const defaults: PersonFormData = { diff --git a/src/components/common/person/grid/CreateForm.tsx b/src/components/common/person/grid/CreateForm.tsx new file mode 100644 index 000000000..6784baeda --- /dev/null +++ b/src/components/common/person/grid/CreateForm.tsx @@ -0,0 +1,220 @@ +import React, { useState } from 'react' +import * as yup from 'yup' +import { Grid } from '@mui/material' + +import GenericForm from 'components/common/form/GenericForm' +import { name, phone, email } from 'common/form/validation' +import SubmitButton from 'components/common/form/SubmitButton' +import FormTextField from 'components/common/form/FormTextField' +import EmailField from 'components/common/form/EmailField' +import { AdminPersonFormData, AdminPersonResponse } from 'gql/person' +import { useMutation } from '@tanstack/react-query' +import { AxiosError, AxiosResponse } from 'axios' +import { ApiErrors } from 'service/apiErrors' +import { useCreatePerson } from 'service/person' +import { AlertStore } from 'stores/AlertStore' +import { useRouter } from 'next/router' +import { useTranslation } from 'next-i18next' +import { routes } from 'common/routes' +import CheckboxField from 'components/common/form/CheckboxField' +import { useCreateCoordinator } from 'service/coordinator' +import { CoordinatorResponse, CoorinatorInput } from 'gql/coordinators' +import { createOrganizer } from 'service/organizer' +import { OrganizerInput } from 'gql/organizer' +import { BeneficiaryFormData, BeneficiaryListResponse } from 'gql/beneficiary' +import { BeneficiaryType } from 'components/admin/beneficiary/BeneficiaryTypes' +import { useCreateBeneficiary } from 'service/beneficiary' +import OrganizerRelationSelect from 'components/admin/beneficiary/OrganizerRelationSelect' +import CountrySelect from 'components/admin/countries/CountrySelect' +import CitySelect from 'components/admin/cities/CitySelect' + +const validationSchema = yup + .object() + .defined() + .shape({ + firstName: name.required(), + lastName: name.required(), + email: email.required(), + phone: phone.notRequired(), + isCoordinator: yup.bool().required(), + isOrganizer: yup.bool().required(), + isBeneficiary: yup.bool().required(), + description: yup.string().notRequired(), + cityId: yup.string().when('isBeneficiary', { + is: true, + then: yup.string().required(), + otherwise: yup.string().notRequired(), + }), + countryCode: yup.string().when('isBeneficiary', { + is: true, + then: yup.string().required(), + otherwise: yup.string().notRequired(), + }), + organizerRelation: yup.string().when('isBeneficiary', { + is: true, + then: yup.string().required(), + otherwise: yup.string().notRequired(), + }), + }) + +const initialValues: AdminPersonFormData = { + firstName: '', + lastName: '', + email: '', + phone: '', + isOrganizer: false, + isCoordinator: false, + isBeneficiary: false, + countryCode: 'BG', + cityId: '', + description: '', + organizerRelation: 'none', +} + +export default function CreateForm() { + const router = useRouter() + const { t } = useTranslation() + const [showBenefactor, setShowBenefactor] = useState(false) + + const mutation = useMutation< + AxiosResponse, + AxiosError, + AdminPersonFormData + >({ + mutationFn: useCreatePerson(), + onError: () => AlertStore.show(t('common:alerts.error'), 'error'), + }) + + const coordinatorCreateMutation = useMutation< + AxiosResponse, + AxiosError, + CoorinatorInput + >({ + mutationFn: useCreateCoordinator(), + onError: () => AlertStore.show(t('common:alerts.error'), 'error'), + }) + + const organizerCreateMutation = useMutation< + AxiosResponse, + AxiosError, + OrganizerInput + >({ + mutationFn: createOrganizer(), + onError: () => AlertStore.show(t('common:alerts.error'), 'error'), + }) + + const beneficiaryCreateMutation = useMutation< + AxiosResponse, + AxiosError, + BeneficiaryFormData + >({ + mutationFn: useCreateBeneficiary(), + onError: () => AlertStore.show(t('common:alerts.error'), 'error'), + }) + + async function handleSubmit(values: AdminPersonFormData) { + const { data: userResponse } = await mutation.mutateAsync(values) + + if (values.isCoordinator) coordinatorCreateMutation.mutate({ personId: userResponse.id }) + + if (values.isOrganizer) organizerCreateMutation.mutate({ personId: userResponse.id }) + + if (values.isBeneficiary) + beneficiaryCreateMutation.mutate({ + type: BeneficiaryType.individual, + personId: userResponse.id, + countryCode: values.countryCode, + cityId: values.cityId, + organizerRelation: values.organizerRelation, + description: values.description, + campaigns: [], + }) + + AlertStore.show(t('common:alerts.success'), 'success') + router.push(routes.admin.person.index) + } + + return ( + + + + + + + + + + + + + + + + + + + + + + + { + setShowBenefactor(e.target.checked) + }} + /> + + {showBenefactor && ( + <> + + + + + + + + + + + + + + )} + + + + + + + ) +} diff --git a/src/components/common/person/grid/CreatePage.tsx b/src/components/common/person/grid/CreatePage.tsx index 752ef22d6..9633b6cce 100644 --- a/src/components/common/person/grid/CreatePage.tsx +++ b/src/components/common/person/grid/CreatePage.tsx @@ -4,7 +4,7 @@ import { Container } from '@mui/material' import AdminLayout from 'components/common/navigation/AdminLayout' import AdminContainer from 'components/common/navigation/AdminContainer' -import PersonForm from './PersonForm' +import CreateForm from './CreateForm' export default function CreatePage() { const { t } = useTranslation() @@ -13,7 +13,7 @@ export default function CreatePage() { - + diff --git a/src/components/common/person/grid/EditForm.tsx b/src/components/common/person/grid/EditForm.tsx new file mode 100644 index 000000000..8fc05ab7a --- /dev/null +++ b/src/components/common/person/grid/EditForm.tsx @@ -0,0 +1,287 @@ +import React, { useState } from 'react' +import * as yup from 'yup' +import { Grid } from '@mui/material' + +import GenericForm from 'components/common/form/GenericForm' +import { name, phone, email } from 'common/form/validation' +import SubmitButton from 'components/common/form/SubmitButton' +import FormTextField from 'components/common/form/FormTextField' +import EmailField from 'components/common/form/EmailField' +import { AdminPersonFormData, AdminPersonResponse, PersonResponse } from 'gql/person' +import { useMutation, UseQueryResult } from '@tanstack/react-query' +import { AxiosError, AxiosResponse } from 'axios' +import { ApiErrors } from 'service/apiErrors' +import { useCreatePerson, useEditPerson } from 'service/person' +import { AlertStore } from 'stores/AlertStore' +import { useRouter } from 'next/router' +import { useTranslation } from 'next-i18next' +import { routes } from 'common/routes' +import { usePerson } from 'common/hooks/person' +import CheckboxField from 'components/common/form/CheckboxField' +import { useCreateCoordinator, useDeleteCoordinator } from 'service/coordinator' +import { CoordinatorResponse, CoorinatorInput } from 'gql/coordinators' +import { createOrganizer, deleteOrganizer } from 'service/organizer' +import { OrganizerInput } from 'gql/organizer' +import { BeneficiaryFormData, BeneficiaryListResponse } from 'gql/beneficiary' +import { BeneficiaryType } from 'components/admin/beneficiary/BeneficiaryTypes' +import { useCreateBeneficiary, useEditBeneficiary, useRemoveBeneficiary } from 'service/beneficiary' +import OrganizerRelationSelect from 'components/admin/beneficiary/OrganizerRelationSelect' +import CountrySelect from 'components/admin/countries/CountrySelect' +import CitySelect from 'components/admin/cities/CitySelect' + +const validationSchema = yup + .object() + .defined() + .shape({ + firstName: name.required(), + lastName: name.required(), + email: email.required(), + phone: phone.notRequired(), + isCoordinator: yup.bool().required(), + isOrganizer: yup.bool().required(), + isBeneficiary: yup.bool().required(), + description: yup.string().notRequired(), + cityId: yup.string().when('isBeneficiary', { + is: true, + then: yup.string().required(), + otherwise: yup.string().notRequired(), + }), + countryCode: yup.string().when('isBeneficiary', { + is: true, + then: yup.string().required(), + otherwise: yup.string().notRequired(), + }), + organizerRelation: yup.string().when('isBeneficiary', { + is: true, + then: yup.string().required(), + otherwise: yup.string().notRequired(), + }), + }) + +export default function EditForm() { + const router = useRouter() + const { t } = useTranslation() + const editPersonId = router.query.id as string + const { data }: UseQueryResult = usePerson(editPersonId) + + const beneficiary = data?.beneficiaries?.at(0) + const [showBenefactor, setShowBenefactor] = useState(!!beneficiary) + + const initialValues = { + firstName: data?.firstName ?? '', + lastName: data?.lastName ?? '', + email: data?.email ?? '', + phone: data?.phone ?? '', + isCoordinator: !!data?.coordinators, + coordinatorId: data?.coordinators?.id, + coordinatorCampaigns: data?.coordinators?._count?.campaigns, + isOrganizer: !!data?.organizer, + organizerId: data?.organizer?.id, + organizerCampaigns: data?.organizer?._count?.campaigns, + isBeneficiary: !!data?.beneficiaries?.length, + beneficiaryId: beneficiary?.id, + beneficiaryCampaigns: beneficiary?._count?.campaigns, + countryCode: beneficiary?.countryCode ?? 'BG', + cityId: beneficiary?.cityId ?? '', + description: beneficiary?.description ?? '', + organizerRelation: beneficiary?.organizerRelation ?? 'none', + } + + const mutation = useMutation< + AxiosResponse, + AxiosError, + AdminPersonFormData + >({ + mutationFn: editPersonId ? useEditPerson(editPersonId) : useCreatePerson(), + onError: () => AlertStore.show(t('common:alerts.error'), 'error'), + }) + + const coordinatorCreateMutation = useMutation< + AxiosResponse, + AxiosError, + CoorinatorInput + >({ + mutationFn: useCreateCoordinator(), + onError: () => AlertStore.show(t('common:alerts.error'), 'error'), + }) + + const coordinatorDeleteMutation = useMutation< + AxiosResponse, + AxiosError, + string + >({ + mutationFn: useDeleteCoordinator(), + onError: () => AlertStore.show(t('common:alerts.error'), 'error'), + }) + + const organizerCreateMutation = useMutation< + AxiosResponse, + AxiosError, + OrganizerInput + >({ + mutationFn: createOrganizer(), + onError: () => AlertStore.show(t('common:alerts.error'), 'error'), + }) + + const organizerDeleteMutation = useMutation< + AxiosResponse, + AxiosError, + string + >({ + mutationFn: deleteOrganizer(), + onError: () => AlertStore.show(t('common:alerts.error'), 'error'), + }) + + const beneficiaryMutation = useMutation< + AxiosResponse, + AxiosError, + BeneficiaryFormData + >({ + mutationFn: beneficiary ? useEditBeneficiary(beneficiary.id) : useCreateBeneficiary(), + onError: () => AlertStore.show(t('common:alerts.error'), 'error'), + }) + + const beneficiaryDeleteMutation = useMutation< + AxiosResponse, + AxiosError, + string + >({ + mutationFn: useRemoveBeneficiary(), + onError: () => AlertStore.show(t('common:alerts.error'), 'error'), + }) + + async function handleSubmit(values: AdminPersonFormData & BeneficiaryFormData) { + const { data: userResponse } = await mutation.mutateAsync(values) + + if (values.isCoordinator !== initialValues.isCoordinator) { + !values.isCoordinator && initialValues.coordinatorId + ? coordinatorDeleteMutation.mutate(initialValues.coordinatorId) + : coordinatorCreateMutation.mutate({ personId: userResponse.id }) + } + + if (values.isOrganizer !== initialValues.isOrganizer) { + !values.isOrganizer && initialValues.organizerId + ? organizerDeleteMutation.mutate(initialValues.organizerId) + : organizerCreateMutation.mutate({ personId: userResponse.id }) + } + + !values.isBeneficiary && initialValues.beneficiaryId + ? beneficiaryDeleteMutation.mutate(initialValues.beneficiaryId) + : beneficiaryMutation.mutate({ + type: BeneficiaryType.individual, + personId: userResponse.id, + countryCode: values.countryCode, + cityId: values.cityId, + organizerRelation: values.organizerRelation, + description: values.description, + campaigns: values.campaigns, + }) + + router.push(routes.admin.person.index) + } + + return ( + + + + + + + + + + + + + + + + + + + + + + + { + setShowBenefactor(e.target.checked) + }} + /> + + {showBenefactor && ( + <> + + + + + + + + + + + + + + )} + + + + + + + ) +} diff --git a/src/components/common/person/grid/EditPage.tsx b/src/components/common/person/grid/EditPage.tsx index a4fcf8226..b4eb6026b 100644 --- a/src/components/common/person/grid/EditPage.tsx +++ b/src/components/common/person/grid/EditPage.tsx @@ -4,7 +4,7 @@ import { Container } from '@mui/material' import AdminLayout from 'components/common/navigation/AdminLayout' import AdminContainer from 'components/common/navigation/AdminContainer' -import PersonForm from './PersonForm' +import EditForm from './EditForm' export default function CreatePage() { const { t } = useTranslation() @@ -13,7 +13,7 @@ export default function CreatePage() { - + diff --git a/src/components/common/person/grid/PersonForm.tsx b/src/components/common/person/grid/PersonForm.tsx deleted file mode 100644 index 2251a9a5e..000000000 --- a/src/components/common/person/grid/PersonForm.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React from 'react' -import * as yup from 'yup' -import { Grid } from '@mui/material' - -import GenericForm from 'components/common/form/GenericForm' -import { name, phone, email } from 'common/form/validation' -import SubmitButton from 'components/common/form/SubmitButton' -import FormTextField from 'components/common/form/FormTextField' -import EmailField from 'components/common/form/EmailField' -import { AdminPersonFormData, AdminPersonResponse, PersonResponse } from 'gql/person' -import { useMutation, UseQueryResult } from '@tanstack/react-query' -import { AxiosError, AxiosResponse } from 'axios' -import { ApiErrors } from 'service/apiErrors' -import { useCreatePerson, useEditPerson } from 'service/person' -import { AlertStore } from 'stores/AlertStore' -import { useRouter } from 'next/router' -import { useTranslation } from 'next-i18next' -import { routes } from 'common/routes' -import { usePerson } from 'common/hooks/person' - -const validationSchema: yup.SchemaOf = yup.object().defined().shape({ - firstName: name.required(), - lastName: name.required(), - email: email.required(), - phone: phone.notRequired(), -}) - -const defaults: AdminPersonFormData = { - firstName: '', - lastName: '', - email: '', - phone: '', -} - -type FormProps = { - initialValues?: AdminPersonFormData -} - -export default function PersonForm({ initialValues = defaults }: FormProps) { - const router = useRouter() - const { t } = useTranslation() - let editPersonId = router.query.id as string - - const mutation = useMutation< - AxiosResponse, - AxiosError, - AdminPersonFormData - >({ - mutationFn: editPersonId ? useEditPerson(editPersonId) : useCreatePerson(), - onError: () => AlertStore.show(t('common:alerts.error'), 'error'), - onSuccess: () => { - AlertStore.show(t('common:alerts.success'), 'success') - router.push(routes.admin.person.index) - }, - }) - - function handleSubmit(values: AdminPersonFormData) { - mutation.mutate(values) - } - - if (editPersonId) { - editPersonId = String(editPersonId) - const { data }: UseQueryResult = usePerson(editPersonId) - - initialValues = { - firstName: data?.firstName ?? '', - lastName: data?.lastName ?? '', - email: data?.email ?? '', - phone: data?.phone ?? '', - } - } - - return ( - - - - - - - - - - - {/* TODO: */} - - - - - - - - - - - - - - ) -} diff --git a/src/gql/beneficiary.d.ts b/src/gql/beneficiary.d.ts index 399710002..14a62a945 100644 --- a/src/gql/beneficiary.d.ts +++ b/src/gql/beneficiary.d.ts @@ -31,6 +31,6 @@ export type BeneficiaryFormData = { description?: string publicData?: string privateData?: string - campaigns: [] + campaigns?: [] organizerRelation?: PersonRelation } diff --git a/src/gql/person.d.ts b/src/gql/person.d.ts index ee701f73e..3cb764cc4 100644 --- a/src/gql/person.d.ts +++ b/src/gql/person.d.ts @@ -1,4 +1,5 @@ import { UUID } from './types' +import { BeneficiaryFormData } from './beneficiary' export type PersonResponse = { id: string @@ -11,6 +12,27 @@ export type PersonResponse = { createdAt: string newsletter: boolean emailConfirmed: boolean + beneficiaries?: PersonBeneficiaryResponse[] + coordinators?: PersonRoleResponse + organizer?: PersonRoleResponse +} + +export type PersonRoleResponse = { + id: string + _count?: { + campaigns: number + } +} + +export type PersonBeneficiaryResponse = { + id: string + countryCode: string + cityId: string + description?: string + organizerRelation?: PersonRelation + _count?: { + campaigns: number + } } export type PersonPaginatedResponse = { @@ -28,6 +50,9 @@ export type PersonFormData = { companyNumber?: string legalPersonName?: string address?: string + isBeneficiary?: boolean + isCoordinator?: boolean + isOrganizer?: boolean } export type CreateBeneficiaryInput = { @@ -81,7 +106,11 @@ export type UpdateUserAccount = { password: string } -export type AdminPersonFormData = Pick +export type AdminPersonFormData = Pick< + PersonFormData, + 'firstName' | 'lastName' | 'email' | 'phone' | 'isBeneficiary' | 'isCoordinator' | 'isOrganizer' +> & + Pick export type AdminPersonResponse = Pick< PersonResponse, diff --git a/src/service/organizer.ts b/src/service/organizer.ts index 92a194318..365471781 100644 --- a/src/service/organizer.ts +++ b/src/service/organizer.ts @@ -18,11 +18,11 @@ export const createOrganizer = () => { } } -export const deleteOrganizer = (id: string) => { +export const deleteOrganizer = () => { const { data: session } = useSession() - return async () => { + return async (data: string) => { return await apiClient.delete>( - endpoints.organizer.removeOrganizer(id).url, + endpoints.organizer.removeOrganizer(data).url, authConfig(session?.accessToken), ) }