diff --git a/public/locales/bg/marketing.json b/public/locales/bg/marketing.json new file mode 100644 index 000000000..54cfa08cd --- /dev/null +++ b/public/locales/bg/marketing.json @@ -0,0 +1,11 @@ +{ + "admin": { + "marketing": "Маркетинг", + "sendConsentEmail": "Изпращане на емайл за съгласие", + "common": { + "templateId": "Идентификатор на Sendgrid шаблон", + "listId": "Идентифицатор на Sendgrid списък с контакти", + "subject": "Тема на емайл" + } + } +} diff --git a/public/locales/en/marketing.json b/public/locales/en/marketing.json new file mode 100644 index 000000000..43bc2206b --- /dev/null +++ b/public/locales/en/marketing.json @@ -0,0 +1,11 @@ +{ + "admin": { + "marketing": "Marketing", + "sendConsentEmail": "Send newsletter consent email", + "common": { + "templateId": "ID of Sendgrid template", + "listId": "ID of Sendgrid contact list", + "subject": "Email subject" + } + } +} diff --git a/src/common/routes.ts b/src/common/routes.ts index 322cd7df6..c567b64e6 100644 --- a/src/common/routes.ts +++ b/src/common/routes.ts @@ -249,6 +249,10 @@ export const routes = { company: { create: '/admin/companies/create', }, + marketing: { + index: '/admin/marketing/', + newsLetterConsent: '/admin/marketing/newsletter-consent', + }, }, dev: { openData: '/open-data', diff --git a/src/components/admin/cities/CreateForm.tsx b/src/components/admin/cities/CreateForm.tsx index 9d46544ec..4866935e8 100644 --- a/src/components/admin/cities/CreateForm.tsx +++ b/src/components/admin/cities/CreateForm.tsx @@ -9,7 +9,7 @@ import { Box, Button, Grid, Typography } from '@mui/material' import { CityFormData, CityInput, CityResponse } from 'gql/cities' import { routes } from 'common/routes' -import { ApiErrors, handleUniqueViolation } from 'service/apiErrors' +import { ApiErrors, Message, handleUniqueViolation } from 'service/apiErrors' import { useCreateCity } from 'service/city' import { AlertStore } from 'stores/AlertStore' import GenericForm from 'components/common/form/GenericForm' @@ -42,7 +42,9 @@ export default function EditForm() { const error = e.response if (error?.status === 409) { - const message = error.data.message.map((el) => handleUniqueViolation(el.constraints, t)) + const message = (error.data.message as Message[]).map((el) => + handleUniqueViolation(el.constraints, t), + ) return AlertStore.show(message.join('/n'), 'error') } diff --git a/src/components/admin/marketing/EmailConsent/SendEmailConsentForm.tsx b/src/components/admin/marketing/EmailConsent/SendEmailConsentForm.tsx new file mode 100644 index 000000000..0e8b5fa53 --- /dev/null +++ b/src/components/admin/marketing/EmailConsent/SendEmailConsentForm.tsx @@ -0,0 +1,117 @@ +import { Button, Grid, Typography } from '@mui/material' +import { useMutation } from '@tanstack/react-query' +import { AxiosError, AxiosResponse } from 'axios' +import { routes } from 'common/routes' +import FormDatePicker from 'components/common/form/FormDatePicker' +import FormTextField from 'components/common/form/FormTextField' +import GenericForm from 'components/common/form/GenericForm' +import SubmitButton from 'components/common/form/SubmitButton' +import { FormikHelpers } from 'formik' +import { NewsLetterConsentResponse, SendNewsLetterConsent } from 'gql/marketing' +import { useTranslation } from 'next-i18next' +import Link from 'next/link' +import { ApiError } from 'service/apiErrors' +import { useSendConsentEmail } from 'service/marketing' +import { AlertStore } from 'stores/AlertStore' +import * as yup from 'yup' + +export default function SendConsentEmailForm() { + const { t } = useTranslation('marketing') + + const initialValues: SendNewsLetterConsent = { + templateId: '', + listId: '', + subject: '', + dateThreshold: new Date().toISOString(), + } + + const validationSchema: yup.SchemaOf = yup.object().defined().shape({ + templateId: yup.string().required(), + listId: yup.string().required(), + subject: yup.string().required(), + dateThreshold: yup.string().optional(), + }) + + const mutationFn = useSendConsentEmail() + + const handleError = (e: AxiosError) => { + const error = e.response as AxiosResponse + AlertStore.show(error.data.message, 'error') + } + + const mutation = useMutation< + AxiosResponse, + AxiosError, + SendNewsLetterConsent + >({ + mutationFn, + onError: (error) => handleError(error), + onSuccess: (data) => { + const response = data.data + AlertStore.show( + t(`Съобщението беше изпратен успешно на ${response.contactCount} емайла.`), + 'success', + ) + }, + }) + + async function onSubmit( + values: SendNewsLetterConsent, + formikHelpers: FormikHelpers, + ) { + const data: SendNewsLetterConsent = { + templateId: values.templateId, + listId: values.listId, + subject: values.subject, + dateThreshold: values.dateThreshold, + } + await mutation.mutateAsync(data) + if (mutation.isSuccess && !mutation.isLoading) { + formikHelpers.resetForm({ values: initialValues }) + } + } + + return ( + + + + {t('admin.sendConsentEmail')} + + + + + + + + + + + + + + + + Премахване от списък на потребители регистрирани след: + + + + + + + + + + + + + ) +} diff --git a/src/components/admin/marketing/EmailConsent/SendEmailConsentPage.tsx b/src/components/admin/marketing/EmailConsent/SendEmailConsentPage.tsx new file mode 100644 index 000000000..db670ca40 --- /dev/null +++ b/src/components/admin/marketing/EmailConsent/SendEmailConsentPage.tsx @@ -0,0 +1,19 @@ +import AdminContainer from 'components/common/navigation/AdminContainer' +import AdminLayout from 'components/common/navigation/AdminLayout' +import React from 'react' +import SendEmailConsentForm from './SendEmailConsentForm' +import { useTranslation } from 'next-i18next' +import { Container } from '@mui/material' + +export default function SendEmailConsentPage() { + const { t } = useTranslation('marketing') + return ( + + + + + + + + ) +} diff --git a/src/components/admin/marketing/MarketingPage.tsx b/src/components/admin/marketing/MarketingPage.tsx new file mode 100644 index 000000000..394f9587d --- /dev/null +++ b/src/components/admin/marketing/MarketingPage.tsx @@ -0,0 +1,58 @@ +import { Box, Button, CardContent, Container, Grid, Typography } from '@mui/material' +import AdminContainer from 'components/common/navigation/AdminContainer' +import AdminLayout from 'components/common/navigation/AdminLayout' +import React from 'react' +import { useTranslation } from 'next-i18next' +import Link from 'next/link' +import { marketingCards } from './navigation/marketingCards' + +const colors = ['#0179a8', '#346cb0', '#5f4b8b', '#b76ba3', '#a7c796', '#00a28a', '#3686a0'] +export default function MarketingPage() { + const { t } = useTranslation('marketing') + return ( + + + + + {marketingCards.map(({ label, href, icon: Icon, disabled }, index) => ( + + + + ))} + + + + + ) +} diff --git a/src/components/admin/marketing/navigation/marketingCards.ts b/src/components/admin/marketing/navigation/marketingCards.ts new file mode 100644 index 000000000..321c9d95d --- /dev/null +++ b/src/components/admin/marketing/navigation/marketingCards.ts @@ -0,0 +1,18 @@ +import { routes } from 'common/routes' +import ThumbUpAltIcon from '@mui/icons-material/ThumbUpAlt' +import SendIcon from '@mui/icons-material/Send' + +export const marketingCards = [ + { + label: 'Изпращане на емайл за съгласие', + icon: ThumbUpAltIcon, + href: routes.admin.marketing.newsLetterConsent, + disabled: false, + }, + { + label: 'Изпращане на маркетинг емайл', + icon: SendIcon, + href: routes.admin.marketing.newsLetterConsent, + disabled: true, + }, +] diff --git a/src/components/common/navigation/adminMenu.tsx b/src/components/common/navigation/adminMenu.tsx index e9217bc1c..cb13f0f32 100644 --- a/src/components/common/navigation/adminMenu.tsx +++ b/src/components/common/navigation/adminMenu.tsx @@ -14,6 +14,7 @@ import { DisplaySettings, RequestQuote, ArticleOutlined, + BroadcastOnPersonal, } from '@mui/icons-material' import VolunteerActivismOutlinedIcon from '@mui/icons-material/VolunteerActivismOutlined' import LocationCityRoundedIcon from '@mui/icons-material/LocationCityRounded' @@ -105,4 +106,9 @@ export const adminCards = [ icon: HandshakeIcon, href: routes.admin.affiliates, }, + { + label: 'Маркетинг', + icon: BroadcastOnPersonal, + href: routes.admin.marketing.index, + }, ] diff --git a/src/gql/marketing.ts b/src/gql/marketing.ts new file mode 100644 index 000000000..949c66f30 --- /dev/null +++ b/src/gql/marketing.ts @@ -0,0 +1,13 @@ +export type SendMarketingEmail = { + templateId: string + listId: string + subject: string +} + +export type SendNewsLetterConsent = SendMarketingEmail & { + dateThreshold?: string +} + +export type NewsLetterConsentResponse = { + contactCount: number +} diff --git a/src/pages/admin/marketing/index.tsx b/src/pages/admin/marketing/index.tsx new file mode 100644 index 000000000..6453df300 --- /dev/null +++ b/src/pages/admin/marketing/index.tsx @@ -0,0 +1,6 @@ +import MarketingPage from 'components/admin/marketing/MarketingPage' +import { securedAdminProps } from 'middleware/auth/securedProps' + +export const getServerSideProps = securedAdminProps(['common', 'auth', 'validation', 'marketing']) + +export default MarketingPage diff --git a/src/pages/admin/marketing/newsletter-consent/index.tsx b/src/pages/admin/marketing/newsletter-consent/index.tsx new file mode 100644 index 000000000..efcbe5b35 --- /dev/null +++ b/src/pages/admin/marketing/newsletter-consent/index.tsx @@ -0,0 +1,6 @@ +import SendEmailConsentPage from 'components/admin/marketing/EmailConsent/SendEmailConsentPage' +import { securedAdminProps } from 'middleware/auth/securedProps' + +export const getServerSideProps = securedAdminProps(['common', 'auth', 'validation', 'marketing']) + +export default SendEmailConsentPage diff --git a/src/service/apiEndpoints.ts b/src/service/apiEndpoints.ts index b4fad72e2..629235b41 100644 --- a/src/service/apiEndpoints.ts +++ b/src/service/apiEndpoints.ts @@ -65,6 +65,7 @@ export const endpoints = { notifications: { sendConfirmationEmail: { url: '/notifications/send-confirm-email', method: 'POST' }, subscribePublicEmail: { url: '/notifications/public/subscribe', method: 'POST' }, + sendNewsLetterConsentEmail: { url: '/notifications/send-newsletter-consent' }, unsubscribePublicEmail: { url: '/notifications/public/unsubscribe', method: 'POST' }, subscribeEmail: { url: '/notifications/subscribe', method: 'POST' }, unsubscribeEmail: { url: '/notifications/unsubscribe', method: 'POST' }, diff --git a/src/service/marketing.ts b/src/service/marketing.ts new file mode 100644 index 000000000..91e3bc211 --- /dev/null +++ b/src/service/marketing.ts @@ -0,0 +1,16 @@ +import { NewsLetterConsentResponse, SendNewsLetterConsent } from 'gql/marketing' +import { useSession } from 'next-auth/react' +import { authConfig } from './restRequests' +import { endpoints } from './apiEndpoints' +import { apiClient } from './apiClient' + +export function useSendConsentEmail() { + const { data: session } = useSession() + return async (data: SendNewsLetterConsent) => { + return await apiClient.post( + endpoints.notifications.sendNewsLetterConsentEmail.url, + data, + authConfig(session?.accessToken), + ) + } +}