diff --git a/package.json b/package.json index 5621762de..6059144d7 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "jest": "^29.6.1", "jest-environment-jsdom": "^29.6.1", "lint-staged": "11.0.0", + "msw": "1", "next-sitemap": "^3.1.52", "prettier": "2.3.0", "shx": "^0.3.3", diff --git a/public/locales/bg/campaign-application.json b/public/locales/bg/campaign-application.json index 4d69d7341..1a075e329 100644 --- a/public/locales/bg/campaign-application.json +++ b/public/locales/bg/campaign-application.json @@ -40,12 +40,15 @@ } }, "photos": "Снимков/видео материал", - "documents": "Документи" + "documents": "Документи", + "disclaimer": "Приемат се до 10 документа. Всеки от документите може да бъде до 30МБ" }, "admin": { "title": "Администраторска редакция", "status": "Статус", - "external-url": "Външен линк" + "external-url": "Външен линк", + "archived": "Архивиран", + "organizer-edit-link": "Организатор може да редактира на" } }, "cta": { @@ -62,6 +65,26 @@ } }, "alerts": { - "successfully-created": "Успешно създадена кампания. " + "successfully-created": "Успешно създадена заявка за кампания. " + }, + "result": { + "created": "Успешно създадена заявка за кампания.", + "edited": "Успешно редактирана заявка за кампания.", + "editButton": "Редакция на заявка за кампания", + "uploadOk": "Добавени файлове", + "deleteOk": "Премахнати файлове", + "uploadFailed": "Неуспешно добавени файлове", + "deleteFailed": "Неуспешно премахнати файлове", + "uploadFailedWhat": "Какво мога да направя?", + "uploadFailedDirection": "Моля посетете страницата за редакция на заявката за кампания от бутона по-долу и опитайте отново да добавите/премахнете файловете" + }, + "status": { + "selectorLabel": "Статус", + "review": "Нова за ревю", + "requestInfo": "За още информация", + "forCommitteeReview": "За ревю от комисия", + "approved": "Одобрена", + "denied": "Отказана", + "abandoned": "Изоставена" } } diff --git a/public/locales/en/campaign-application.json b/public/locales/en/campaign-application.json index 3e32e7797..847b7f696 100644 --- a/public/locales/en/campaign-application.json +++ b/public/locales/en/campaign-application.json @@ -40,12 +40,15 @@ } }, "photos": "Photo/Video material", - "documents": "Documents" + "documents": "Documents", + "disclaimer": "Up to 10 documents accepted. Each of the documents can be up to 30MB is size" }, "admin": { "title": "Admin edit", "status": "Status", - "external-url": "External URL" + "external-url": "External URL", + "archived": "Archived", + "organizer-edit-link": "Organizer can edit at" } }, "cta": { @@ -63,5 +66,25 @@ }, "alerts": { "successfully-created": "Campaign application successfully created." + }, + "result": { + "created": "Successfully created campaign application", + "edited": "Successfully edited campaign application", + "editButton": "Edit campaign application", + "uploadOk": "Fails added", + "deleteOk": "Files removed", + "uploadFailed": "Failed file add", + "deleteFailed": "Failed to remove files", + "uploadFailedWhat": "What can I do?", + "uploadFailedDirection": "Please visit the campaign application edit page linked below and retry adding/removing the files." + }, + "status": { + "selectorLabel": "Status", + "review": "New - for review", + "requestInfo": "Request for more info", + "forCommitteeReview": "For committee review", + "approved": "Approved", + "denied": "Denied", + "abandoned": "Abandoned" } } diff --git a/src/common/routes.ts b/src/common/routes.ts index 6f03310d0..5de0f4367 100644 --- a/src/common/routes.ts +++ b/src/common/routes.ts @@ -92,6 +92,7 @@ export const routes = { index: '/campaigns', create: '/campaigns/create', application: 'campaigns/application', + applicationEdit: (id: string) => `/campaigns/application/${id}`, viewCampaignBySlug: (slug: string) => `/campaigns/${slug}`, viewExpenses: (slug: string) => `/campaigns/${slug}/expenses`, oneTimeDonation: (slug: string) => `/campaigns/donation/${slug}`, diff --git a/src/components/admin/campaign-applications/CampaignApplicationAdminPropsEdit.tsx b/src/components/admin/campaign-applications/CampaignApplicationAdminPropsEdit.tsx index b1a49e689..52171a6c7 100644 --- a/src/components/admin/campaign-applications/CampaignApplicationAdminPropsEdit.tsx +++ b/src/components/admin/campaign-applications/CampaignApplicationAdminPropsEdit.tsx @@ -1,33 +1,45 @@ import { useTranslation } from 'next-i18next' import { Grid } from '@mui/material' +import { StatusSelector } from 'components/client/campaign-application/helpers/campaign-application-status' import { StyledFormTextField, StyledStepHeading, } from 'components/client/campaign-application/helpers/campaignApplication.styled' +import { CamAppDetail } from 'components/client/campaign-application/steps/CampaignApplicationSummary' +import CheckboxField from 'components/common/form/CheckboxField' +import OrganizerCanEditAt from './CampaignApplicationOrganizerCanEditAt' -export default function CampaignApplicationAdminPropsEdit() { +export default function CampaignApplicationAdminPropsEdit({ id }: { id: string }) { const { t } = useTranslation('campaign-application') - return ( {t('steps.admin.title')} - - + + + + + - + + + } + /> + ) } diff --git a/src/components/admin/campaign-applications/CampaignApplicationOrganizerCanEditAt.tsx b/src/components/admin/campaign-applications/CampaignApplicationOrganizerCanEditAt.tsx new file mode 100644 index 000000000..71efd59d5 --- /dev/null +++ b/src/components/admin/campaign-applications/CampaignApplicationOrganizerCanEditAt.tsx @@ -0,0 +1,23 @@ +import { routes } from 'common/routes' +import { CopyTextButton } from 'components/common/CopyTextButton' +import getConfig from 'next/config' +import Copy from '@mui/icons-material/CopyAll' +import { Typography } from '@mui/material' +export type Props = { + id: string +} +const OrganizerCanEditAt = ({ id }: Props) => { + const { publicRuntimeConfig } = getConfig() + const url = `${publicRuntimeConfig?.APP_URL}${routes.campaigns.applicationEdit(id)}` + + return ( + <> + + {url} + + } text={url} title={`Copy ${url}`} /> + + ) +} + +export default OrganizerCanEditAt diff --git a/src/components/admin/campaign-applications/CampaignApplicationsGrid.tsx b/src/components/admin/campaign-applications/CampaignApplicationsGrid.tsx index a4e1e3473..4d230a824 100644 --- a/src/components/admin/campaign-applications/CampaignApplicationsGrid.tsx +++ b/src/components/admin/campaign-applications/CampaignApplicationsGrid.tsx @@ -1,12 +1,18 @@ import { useTranslation } from 'next-i18next' import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid' +import { useQuery } from '@tanstack/react-query' import { routes } from 'common/routes' import theme from 'common/theme' +import { CampaignApplicationAdminResponse } from 'gql/campaign-applications' +import { useSession } from 'next-auth/react' import Link from 'next/link' +import { endpoints } from 'service/apiEndpoints' +import { authQueryFnFactory } from 'service/restRequests' export default function CampaignApplicationsGrid() { - const { t, i18n } = useTranslation() + const { t } = useTranslation('campaign-application') + const { list } = useCampaignsList() const commonProps: Partial = { align: 'left', @@ -16,33 +22,34 @@ export default function CampaignApplicationsGrid() { const columns: GridColDef[] = [ { - field: 'status', + field: 'state', headerName: t('campaigns:status'), ...commonProps, align: 'left', width: 220, + renderCell: (cellValues: GridRenderCellParams) => t(`status.${cellValues.row.state}`), }, { - field: 'title', + field: 'campaignName', headerName: t('campaigns:title'), ...commonProps, align: 'left', width: 250, renderCell: (cellValues: GridRenderCellParams) => ( - {cellValues.row.title} + {cellValues.row.campaignName} ), }, { - field: 'essence', + field: 'goal', headerName: t('campaigns:essence'), ...commonProps, align: 'left', width: 250, }, { - field: 'organizer', + field: 'organizerName', headerName: t('campaigns:organizer'), ...commonProps, align: 'left', @@ -71,65 +78,12 @@ export default function CampaignApplicationsGrid() { }, ] - const data = [ - { - updatedAt: 'date', - createdAt: '2024-5-5', - beneficiary: 'beneficiary', - organizer: 'organizer', - essence: 'essence', - title: 'title', - id: '1', - status: 'нова', - }, - { - updatedAt: 'yesterday', - createdAt: '10 days ago', - beneficiary: 'beneficiary', - organizer: 'organizer', - essence: 'essence', - title: 'title', - id: '2', - status: 'очаква документи', - }, - { - updatedAt: '', - createdAt: '', - beneficiary: 'beneficiary', - organizer: 'organizer', - essence: 'essence', - title: 'title', - id: '3', - status: 'очаква решение на комисия', - }, - - { - updatedAt: '', - createdAt: '', - beneficiary: 'beneficiary', - organizer: 'organizer', - essence: 'essence', - title: 'title', - id: '4', - status: 'одобрена', - }, - { - updatedAt: '', - createdAt: '', - beneficiary: 'beneficiary', - organizer: 'organizer', - essence: 'essence', - title: 'title', - id: '4', - status: 'отказана', - }, - ] return ( ) } + +function fetchMutation() { + const { data } = useSession() + return useQuery( + [endpoints.campaignApplication.listAllForAdmin.url], + authQueryFnFactory(data?.accessToken), + { + cacheTime: 10 * 60 * 1000, + staleTime: 10 * 60 * 1000, + }, + ) +} + +export const useCampaignsList = () => { + const { data, isLoading } = fetchMutation() + + return { + list: data?.sort((a, b) => b?.updatedAt?.localeCompare(a?.updatedAt ?? '') ?? 0), + isLoading, + } +} diff --git a/src/components/admin/campaign-applications/EditPage.tsx b/src/components/admin/campaign-applications/EditPage.tsx index 30683f19a..7fdc80e11 100644 --- a/src/components/admin/campaign-applications/EditPage.tsx +++ b/src/components/admin/campaign-applications/EditPage.tsx @@ -1,44 +1,121 @@ -import { Box } from '@mui/material' -import CampaignApplication from 'components/client/campaign-application/steps/CampaignApplication' +import { Box, CircularProgress, Grid, Typography } from '@mui/material' +import { red } from '@mui/material/colors' +import { CampaignApplicationFormData } from 'components/client/campaign-application/helpers/campaignApplication.types' +import { ActionSubmitButton } from 'components/client/campaign-application/helpers/campaignApplicationFormActions.styled' +import CampaignApplicationBasic from 'components/client/campaign-application/steps/CampaignApplicationBasic' import CampaignApplicationDetails from 'components/client/campaign-application/steps/CampaignApplicationDetails' import CampaignApplicationOrganizer from 'components/client/campaign-application/steps/CampaignApplicationOrganizer' +import CampaignApplicationSummary, { + CamAppDetail, +} from 'components/client/campaign-application/steps/CampaignApplicationSummary' import GenericForm from 'components/common/form/GenericForm' import AdminContainer from 'components/common/navigation/AdminContainer' import AdminLayout from 'components/common/navigation/AdminLayout' +import { CampaignApplicationExisting } from 'gql/campaign-applications' +import { useTranslation } from 'next-i18next' +import NotFoundPage from 'pages/404' +import { + mapCreateOrEditInput, + useCreateOrEditApplication, + useViewCampaignApplicationCached, +} from 'service/campaign-application' import CampaignApplicationAdminPropsEdit from './CampaignApplicationAdminPropsEdit' -import { CampaignApplicationAdminEdit } from './campaignApplicationAdmin.types' -import { useState } from 'react' +import OrganizerCanEditAt from './CampaignApplicationOrganizerCanEditAt' +import { campaignApplicationAdminValidationSchema } from 'components/client/campaign-application/helpers/validation-schema' -export default function EditPage() { - const [files, setFiles] = useState([]) - - const initialValues = { - organizer: { - name: 'Some organizer', - phone: '+35999999', - email: 'aReal@Email.com', - }, - - status: 'review', - ticketUrl: 'https://trello.com/this-campaign-application', - } as CampaignApplicationAdminEdit +export type Props = { + id: string +} +export function EditLoadedCampaign({ campaign }: { campaign: CampaignApplicationExisting }) { + const { createOrUpdateApplication, ...c } = useCreateOrEditApplication({ + isEdit: true, + campaignApplication: campaign, + }) + const { t } = useTranslation('campaign-application') return ( - - onSubmit={(v) => console.log(v)} - initialValues={initialValues}> - -
.
- -
.
- -
.
- - - + {c.createOrUpdateSuccessful ? ( + + + + + Admin props / Административни подробности + + + } + /> + + + + + + } + /> + ) : ( + + onSubmit={async (v) => { + const request = mapCreateOrEditInput(v) + await createOrUpdateApplication(request) + }} + initialValues={c.initialValues} + validationSchema={campaignApplicationAdminValidationSchema.defined()}> + + + + + + {c.error && + c.error.map((e, i) => ( + + {e} + + ))} + + )} +
) } + +export default function EditPage({ id }: Props) { + const { data, isLoading, isError } = useViewCampaignApplicationCached(id, 60 * 1000) + + if (isLoading) { + return ( + + + + ) + } + + if (isError) { + return + } + + return +} diff --git a/src/components/admin/campaign-applications/campaignApplicationAdmin.types.ts b/src/components/admin/campaign-applications/campaignApplicationAdmin.types.ts deleted file mode 100644 index 66722ada4..000000000 --- a/src/components/admin/campaign-applications/campaignApplicationAdmin.types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { CampaignApplicationFormData } from 'components/client/campaign-application/helpers/campaignApplication.types' - -export type CampaignApplicationAdminEdit = CampaignApplicationFormData & { - status: string - ticketUrl?: string -} diff --git a/src/components/client/campaign-application/CampaignApplicationForm.tsx b/src/components/client/campaign-application/CampaignApplicationForm.tsx index 59521a002..42e7e6c54 100644 --- a/src/components/client/campaign-application/CampaignApplicationForm.tsx +++ b/src/components/client/campaign-application/CampaignApplicationForm.tsx @@ -1,6 +1,6 @@ import { Grid, StepLabel } from '@mui/material' import { Person } from 'gql/person' -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { CampaignApplicationFormData, @@ -8,40 +8,36 @@ import { Steps, } from './helpers/campaignApplication.types' +import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos' +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos' import GenericForm from 'components/common/form/GenericForm' -import CampaignApplicationFormActions from './CampaignApplicationFormActions' -import CampaignApplicationRemark from './CampaignApplicationRemark' -import CampaignApplicationStepperIcon from './CampaignApplicationStepperIcon' -import CampaignApplication from './steps/CampaignApplication' +import CampaignApplicationBasic from './steps/CampaignApplicationBasic' import CampaignApplicationDetails from './steps/CampaignApplicationDetails' import CampaignApplicationOrganizer from './steps/CampaignApplicationOrganizer' +import CampaignApplicationRemark from './steps/CampaignApplicationRemark' +import CampaignApplicationStepperIcon from './steps/CampaignApplicationStepperIcon' import { validationSchema } from './helpers/validation-schema' -import { useMutation } from '@tanstack/react-query' -import { AxiosError, AxiosResponse, isAxiosError } from 'axios' +import { routes } from 'common/routes' import { FormikHelpers } from 'formik' -import { - CreateCampaignApplicationInput, - CreateCampaignApplicationResponse, - UploadCampaignApplicationFilesRequest, - UploadCampaignApplicationFilesResponse, -} from 'gql/campaign-applications' -import { CampaignTypesResponse } from 'gql/campaign-types' -import { t } from 'i18next' +import { CampaignApplicationExisting } from 'gql/campaign-applications' import { useTranslation } from 'next-i18next' -import { ApiErrors, matchValidator } from 'service/apiErrors' -import { - useCreateCampaignApplication, - useUploadCampaignApplicationFiles, -} from 'service/campaign-application' -import { useCampaignTypesList } from 'service/campaignTypes' +import { useRouter } from 'next/router' +import { mapCreateOrEditInput, useCreateOrEditApplication } from 'service/campaign-application' import { AlertStore } from 'stores/AlertStore' import { StyledCampaignApplicationStep, StyledCampaignApplicationStepper, StyledStepConnector, } from './helpers/campaignApplication.styled' +import { + ActionButton, + ActionLinkButton, + ActionSubmitButton, + Root, +} from './helpers/campaignApplicationFormActions.styled' +import CampaignApplicationSummary from './steps/CampaignApplicationSummary' const steps: StepType[] = [ { @@ -57,73 +53,50 @@ const steps: StepType[] = [ type Props = { person?: Person + isEdit?: boolean + campaignApplication?: CampaignApplicationExisting } -export default function CampaignApplicationForm({ person }: Props) { +export default function CampaignApplicationForm({ + person, + isEdit, + campaignApplication: existing, +}: Props) { const { t } = useTranslation('campaign-application') - const [activeStep, setActiveStep] = useState(Steps.ORGANIZER) - const isLast = activeStep === Steps.CAMPAIGN_DETAILS - const [submitting, setSubmitting] = useState(false) - - const initialValues: CampaignApplicationFormData = { - organizer: { - name: `${person?.firstName} ${person?.lastName}` ?? '', - phone: person?.phone ?? '', - email: person?.email ?? '', - acceptTermsAndConditions: false, - transparencyTermsAccepted: false, - personalInformationProcessingAccepted: false, - }, - application: { - title: '', - beneficiaryNames: '', - campaignType: '', - funds: 0, - campaignEnd: '', - }, - details: { - campaignGuarantee: '', - cause: '', - currentStatus: '', - description: '', - documents: [], - links: [], - organizerBeneficiaryRelationship: '-', - otherFinancialSources: '', - }, - } - - const [files, setFiles] = useState([]) + const router = useRouter() + + const { + createOrUpdateApplication, + createOrUpdateSuccessful: applicationCreated, + submitting, + uploadedFiles, + error: createCampaignError, + campaignApplicationResult: camApp, + files, + setFiles, + initialValues, + deletedFiles, + } = useCreateOrEditApplication({ + person, + isEdit, + campaignApplication: existing, + }) - const { data } = useCampaignTypesList() - const create = useCreateApplication() const handleSubmit = async ( formData: CampaignApplicationFormData, - { setFieldError, resetForm }: FormikHelpers, + { resetForm }: FormikHelpers, ) => { - if (isLast) { - if (submitting) { - return + if (activeStep === Steps.CREATED_DETAILS && camApp?.id != null) { + router.push(routes.campaigns.applicationEdit(camApp?.id)) // go to the edit page + if (isEdit) { + router.reload() // in case we are re-editing refresh the whole page to reset all the things } - setSubmitting(true) - try { - const createInput = mapCreateInput(formData, data ?? []) - await create(createInput, files) - + } else if (shouldSubmit) { + const createOrEdit = mapCreateOrEditInput(formData) + await createOrUpdateApplication(createOrEdit) + if (applicationCreated) { resetForm() - setSubmitting(false) AlertStore.show(t('alerts.successfully-created'), 'success') - - // take user to next campaign application page - } catch (error) { - setSubmitting(false) - console.error(error) - if (isAxiosError(error)) { - const { response } = error as AxiosError - response?.data.message.map(({ property, constraints }) => { - setFieldError(property, t(matchValidator(constraints))) - }) - } } } else { setActiveStep((prevActiveStep) => prevActiveStep + 1) @@ -134,6 +107,16 @@ export default function CampaignApplicationForm({ person }: Props) { setActiveStep((prevActiveStep) => prevActiveStep - 1) }, []) + const [activeStep, setActiveStep] = useState(Steps.ORGANIZER) + const shouldSubmit = activeStep === Steps.CAMPAIGN_DETAILS + + // move to last step after campaign application created successfully + useEffect(() => { + if (applicationCreated && camApp?.id) { + setActiveStep(Steps.CREATED_DETAILS) + } + }, [applicationCreated]) + return ( <> @@ -152,81 +135,79 @@ export default function CampaignApplicationForm({ person }: Props) { {activeStep === Steps.ORGANIZER && } - {activeStep === Steps.CAMPAIGN && } + {activeStep === Steps.CAMPAIGN_BASIC && } {activeStep === Steps.CAMPAIGN_DETAILS && ( )} + {activeStep === Steps.CREATED_DETAILS && ( + + )} - + + + {activeStep === Steps.ORGANIZER ? ( + }> + {t('cta.back')} + + ) : ( + } + disabled={ + applicationCreated /**after campaign application is created disable going back and editing */ + }> + {t('cta.back')} + + )} + + + } + disabled={submitting} + /> + + + {(activeStep === Steps.ORGANIZER || activeStep === Steps.CAMPAIGN_BASIC) && ( + + )} + {/* campaign errors */} + {createCampaignError && ( + <> + Errors: + {createCampaignError?.map((e, i) => ( +

{e}

+ ))} + + )} - {(activeStep === Steps.ORGANIZER || activeStep === Steps.CAMPAIGN) && ( - - )} ) } - -const useCreateApplication = () => { - const create = useMutation< - AxiosResponse, - AxiosError, - CreateCampaignApplicationInput - >({ - mutationFn: useCreateCampaignApplication(), - onError: () => AlertStore.show(t('common:alerts.error'), 'error'), - onSuccess: () => AlertStore.show(t('common:alerts.message-sent'), 'success'), - }) - - const fileUpload = useMutation< - AxiosResponse, - AxiosError, - UploadCampaignApplicationFilesRequest - >({ - mutationFn: useUploadCampaignApplicationFiles(), - }) - - return async (i: CreateCampaignApplicationInput, files: File[]) => { - const { - data: { id }, - } = await create.mutateAsync(i) - - await fileUpload.mutateAsync({ campaignApplicationId: id, files }) - - return { id } - } -} - -function mapCreateInput( - i: CampaignApplicationFormData, - types: CampaignTypesResponse[], -): CreateCampaignApplicationInput { - return { - acceptTermsAndConditions: i.organizer.acceptTermsAndConditions, - personalInformationProcessingAccepted: i.organizer.personalInformationProcessingAccepted, - transparencyTermsAccepted: i.organizer.transparencyTermsAccepted, - - organizerName: i.organizer.name, - organizerEmail: i.organizer.email, - organizerPhone: i.organizer.phone, - - beneficiary: i.application.beneficiaryNames, - - campaignName: i.application.title, - amount: i.application.funds?.toString() ?? '', - goal: i.details.cause, - category: types.find((c) => c.id === i.application.campaignType)?.category, - description: i.details.description, - organizerBeneficiaryRel: i.details.organizerBeneficiaryRelationship ?? '-', - campaignGuarantee: i.details.campaignGuarantee, - history: i.details.currentStatus, - otherFinanceSources: i.details.otherFinancialSources, - } -} diff --git a/src/components/client/campaign-application/CampaignApplicationFormActions.tsx b/src/components/client/campaign-application/CampaignApplicationFormActions.tsx deleted file mode 100644 index e1443040d..000000000 --- a/src/components/client/campaign-application/CampaignApplicationFormActions.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { MouseEvent } from 'react' - -import { useTranslation } from 'next-i18next' - -import { Grid } from '@mui/material' -import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos' -import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos' - -import { - ActionButton, - ActionLinkButton, - ActionSubmitButton, - Root, -} from './helpers/campaignApplicationFormActions.styled' - -type CampaignApplicationFormActionsProps = { - activeStep: number - onBack?: (event: MouseEvent) => void - isLast: boolean - submitting: boolean -} - -export default function CampaignApplicationFormActions({ - onBack, - activeStep, - isLast, - submitting, -}: CampaignApplicationFormActionsProps) { - const { t } = useTranslation('campaign-application') - - return ( - - - {activeStep === 0 ? ( - }> - {t('cta.back')} - - ) : ( - }> - {t('cta.back')} - - )} - - - } - disabled={submitting} - /> - - - ) -} diff --git a/src/components/client/campaign-application/EditCampaignApplicationPage.tsx b/src/components/client/campaign-application/EditCampaignApplicationPage.tsx new file mode 100644 index 000000000..2326a4845 --- /dev/null +++ b/src/components/client/campaign-application/EditCampaignApplicationPage.tsx @@ -0,0 +1,31 @@ +import { CircularProgress, Grid } from '@mui/material' +import NotFoundPage from 'pages/404' +import { useViewCampaignApplicationCached } from 'service/campaign-application' +import Layout from '../layout/Layout' +import CampaignApplicationForm from './CampaignApplicationForm' + +interface EditProps { + id: string +} + +export default function EditCampaignApplicationPage({ id }: EditProps) { + const { data, isLoading, isError } = useViewCampaignApplicationCached(id) + + if (isLoading) { + return ( + + + + ) + } + + if (isError) { + return + } + + return ( + + + + ) +} diff --git a/src/components/client/campaign-application/helpers/campaign-application-status.tsx b/src/components/client/campaign-application/helpers/campaign-application-status.tsx new file mode 100644 index 000000000..31d440d26 --- /dev/null +++ b/src/components/client/campaign-application/helpers/campaign-application-status.tsx @@ -0,0 +1,27 @@ +import { FormControl, FormControlProps, InputLabel, MenuItem, Select } from '@mui/material' +import { useField } from 'formik' +import { useTranslation } from 'next-i18next' +import { allStates } from './campaignApplication.types' + +export type Props = { name: string } & FormControlProps + +export const StatusSelector = ({ name, ...control }: Props) => { + const [field] = useField(name) + const { t } = useTranslation('campaign-application') + return ( + + {t('status.selectorLabel')} + + + ) +} diff --git a/src/components/client/campaign-application/helpers/campaignApplication.types.ts b/src/components/client/campaign-application/helpers/campaignApplication.types.ts index c97b19852..cba46e6d5 100644 --- a/src/components/client/campaign-application/helpers/campaignApplication.types.ts +++ b/src/components/client/campaign-application/helpers/campaignApplication.types.ts @@ -5,8 +5,9 @@ export type Step = { export enum Steps { NONE = -1, ORGANIZER = 0, - CAMPAIGN = 1, + CAMPAIGN_BASIC = 1, CAMPAIGN_DETAILS = 2, + CREATED_DETAILS = 3, } export type CampaignApplicationOrganizer = { @@ -18,27 +19,51 @@ export type CampaignApplicationOrganizer = { personalInformationProcessingAccepted: boolean } -export type CampaignApplication = { +export type CampaignApplicationBasic = { beneficiaryNames: string title: string campaignType: string funds: number campaignEnd: string + campaignEndDate?: string +} + +export type CampaignApplicationDetails = { + cause: string + organizerBeneficiaryRelationship?: string + description?: string + currentStatus?: string +} + +// keep in sync with api repo/podkrepi.dbml -> Enum CampaignApplicationState +export type CampaignApplicationState = + | 'review' + | 'requestInfo' + | 'forCommitteeReview' + | 'approved' + | 'denied' + | 'abandoned' + +export const allStates: CampaignApplicationState[] = [ + 'review', + 'requestInfo', + 'forCommitteeReview', + 'approved', + 'denied', + 'abandoned', +] + +export type CampaignApplicationAdmin = { + state: CampaignApplicationState + ticketURL?: string + archived?: boolean } export type CampaignApplicationFormData = { organizer: CampaignApplicationOrganizer - application: CampaignApplication - details: { - organizerBeneficiaryRelationship: string - campaignGuarantee: string | undefined - otherFinancialSources: string | undefined - description: string - currentStatus: string - cause: string - links: string[] - documents: string[] - } + applicationBasic: CampaignApplicationBasic + applicationDetails: CampaignApplicationDetails + admin?: CampaignApplicationAdmin } export type CampaignApplicationFormDataSteps = { diff --git a/src/components/client/campaign-application/helpers/validation-schema.ts b/src/components/client/campaign-application/helpers/validation-schema.ts index 12b46930e..58f1d7598 100644 --- a/src/components/client/campaign-application/helpers/validation-schema.ts +++ b/src/components/client/campaign-application/helpers/validation-schema.ts @@ -1,29 +1,82 @@ import * as yup from 'yup' -import { name, phone, email } from 'common/form/validation' +import { email, name, phone } from 'common/form/validation' import { - CampaignApplicationFormDataSteps, + CampaignApplicationAdmin, + CampaignApplicationBasic, + CampaignApplicationDetails, + CampaignApplicationFormData, CampaignApplicationOrganizer, + CampaignApplicationState, Steps, } from './campaignApplication.types' -const organizer: yup.SchemaOf = yup - .object() - .shape({ - name: name.required(), - phone: phone.required(), - email: email.required(), - }) - .defined() +const organizerSchema: yup.SchemaOf = yup.object().shape({ + name: name.required(), + phone: phone.required(), + email: email.required(), + acceptTermsAndConditions: yup.bool().oneOf([true], 'validation:terms-of-use').required(), + transparencyTermsAccepted: yup.bool().oneOf([true], 'validation:required').required(), + personalInformationProcessingAccepted: yup + .bool() + .oneOf([true], 'validation:terms-of-service') + .required(), +}) + +const basicSchema: yup.SchemaOf = yup.object().shape({ + beneficiaryNames: yup.string().required(), + campaignEnd: yup.string().required(), + campaignType: yup.string().required(), + funds: yup.number().required(), + title: yup.string().required(), + campaignEndDate: yup.string().optional(), +}) + +const detailsSchema: yup.SchemaOf = yup.object().shape({ + cause: yup.string().required(), + campaignGuarantee: yup.string().optional(), + currentStatus: yup.string().optional(), + description: yup.string().optional(), + documents: yup.array().optional(), + links: yup.array().optional(), + organizerBeneficiaryRelationship: yup.string().optional(), + otherFinancialSources: yup.string().optional(), +}) + +const adminPropsSchema: yup.SchemaOf = yup.object().shape({ + state: yup + .mixed() + .oneOf(['review', 'requestInfo', 'forCommitteeReview', 'approved', 'denied', 'abandoned']) + .required(), + ticketURL: yup.string().optional(), + archived: yup.bool().optional(), +}) export const validationSchema: { - [key in Steps]?: - | yup.SchemaOf - | yup.SchemaOf + [Steps.NONE]: undefined + [Steps.ORGANIZER]: yup.SchemaOf> + [Steps.CAMPAIGN_BASIC]: yup.SchemaOf> + [Steps.CAMPAIGN_DETAILS]: yup.SchemaOf> + [Steps.CREATED_DETAILS]: undefined } = { [Steps.NONE]: undefined, + [Steps.CREATED_DETAILS]: undefined, [Steps.ORGANIZER]: yup.object().shape({ - organizer: organizer.required(), + organizer: organizerSchema.defined(), + }), + [Steps.CAMPAIGN_BASIC]: yup.object().shape({ + applicationBasic: basicSchema.defined(), + }), + [Steps.CAMPAIGN_DETAILS]: yup.object().shape({ + applicationDetails: detailsSchema.defined(), }), } + +export const campaignApplicationAdminValidationSchema: yup.SchemaOf = + yup.object().shape({ + organizer: organizerSchema.defined(), + applicationBasic: basicSchema.defined(), + applicationDetails: detailsSchema.defined(), + admin: adminPropsSchema.nullable().defined(), + }) diff --git a/src/components/client/campaign-application/steps/CampaignApplication.tsx b/src/components/client/campaign-application/steps/CampaignApplicationBasic.tsx similarity index 63% rename from src/components/client/campaign-application/steps/CampaignApplication.tsx rename to src/components/client/campaign-application/steps/CampaignApplicationBasic.tsx index b39b482fc..864dcd8f9 100644 --- a/src/components/client/campaign-application/steps/CampaignApplication.tsx +++ b/src/components/client/campaign-application/steps/CampaignApplicationBasic.tsx @@ -1,17 +1,33 @@ import { FormControl, Grid, Typography } from '@mui/material' -import { Field, useField } from 'formik' +import { Field, useFormikContext } from 'formik' import { useTranslation } from 'next-i18next' -import { StyledFormTextField, StyledStepHeading } from '../helpers/campaignApplication.styled' -import { CampaignEndTypes } from '../helpers/campaignApplication.types' import CampaignTypeSelect from 'components/client/campaigns/CampaignTypeSelect' import FormDatePicker from 'components/common/form/FormDatePicker' +import { StyledFormTextField, StyledStepHeading } from '../helpers/campaignApplication.styled' +import { CampaignApplicationFormData, CampaignEndTypes } from '../helpers/campaignApplication.types' import theme from 'common/theme' +import { useEffect, useState } from 'react' -export default function CampaignApplication() { +export default function CampaignApplicationBasic() { const { t } = useTranslation('campaign-application') - const [campaignEnd] = useField('application.campaignEnd') + const { values, setFieldValue } = useFormikContext() + // if user selects the date we'll fill in the previously selected (or new Date()) or remove that in case they chose another option + const [selectedDate, setSelectedDate] = useState( + values?.applicationBasic?.campaignEndDate ?? new Date().toString(), + ) + useEffect(() => { + const endDate = values.applicationBasic?.campaignEndDate + if (endDate != null && endDate != selectedDate) { + setSelectedDate(endDate) + } + setFieldValue( + 'applicationBasic.campaignEndDate', + values?.applicationBasic?.campaignEnd === CampaignEndTypes.DATE ? selectedDate : undefined, + false, + ) + }, [values?.applicationBasic?.campaignEnd]) return ( @@ -23,7 +39,7 @@ export default function CampaignApplication() { @@ -31,24 +47,24 @@ export default function CampaignApplication() {
- + @@ -65,7 +81,7 @@ export default function CampaignApplication() { {t('steps.application.campaign-end.options.funds')} @@ -76,7 +92,7 @@ export default function CampaignApplication() { {t('steps.application.campaign-end.options.ongoing')} @@ -87,18 +103,19 @@ export default function CampaignApplication() { {t('steps.application.campaign-end.options.date')}
- {campaignEnd.value === CampaignEndTypes.DATE && ( - - - - )} + {values?.applicationBasic?.campaignEnd === CampaignEndTypes.DATE && + values?.applicationBasic?.campaignEndDate != null && ( + + + + )} diff --git a/src/components/client/campaign-application/steps/CampaignApplicationDetails.tsx b/src/components/client/campaign-application/steps/CampaignApplicationDetails.tsx index 733974aa8..6fa0945f6 100644 --- a/src/components/client/campaign-application/steps/CampaignApplicationDetails.tsx +++ b/src/components/client/campaign-application/steps/CampaignApplicationDetails.tsx @@ -4,7 +4,6 @@ import { useTranslation } from 'next-i18next' import FormTextField from 'components/common/form/FormTextField' import { StyledStepHeading } from '../helpers/campaignApplication.styled' -import theme from 'common/theme' import FileList from 'components/common/file-upload/FileList' import FileUpload from 'components/common/file-upload/FileUpload' import { Dispatch, SetStateAction } from 'react' @@ -26,8 +25,8 @@ export default function CampaignApplicationDetails({ files, setFiles }: Props) { @@ -35,68 +34,31 @@ export default function CampaignApplicationDetails({ files, setFiles }: Props) { - - - {t('steps.details.links.label')} - - - - - - - - - - - - - - - - { setFiles((prevFiles) => [...prevFiles, ...newFiles]) }} + accept="text/plain,application/json,application/pdf,image/png,image/jpeg,application/xml,text/xml,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" /> + {t('steps.details.disclaimer')} diff --git a/src/components/client/campaign-application/steps/CampaignApplicationOrganizer.tsx b/src/components/client/campaign-application/steps/CampaignApplicationOrganizer.tsx index 5a975dc94..a96556b3d 100644 --- a/src/components/client/campaign-application/steps/CampaignApplicationOrganizer.tsx +++ b/src/components/client/campaign-application/steps/CampaignApplicationOrganizer.tsx @@ -8,7 +8,11 @@ import AcceptPrivacyPolicyField from 'components/common/form/AcceptPrivacyPolicy import { StyledStepHeading, StyledFormTextField } from '../helpers/campaignApplication.styled' -export default function CampaignApplicationOrganizer() { +type Props = { + isAdmin?: boolean +} + +export default function CampaignApplicationOrganizer({ isAdmin }: Props) { const { t } = useTranslation('campaign-application') return ( @@ -46,7 +50,7 @@ export default function CampaignApplicationOrganizer() { - + @@ -55,11 +59,15 @@ export default function CampaignApplicationOrganizer() { label={ {t('steps.organizer.transparencyTerms')} } + disabled={isAdmin} /> - + diff --git a/src/components/client/campaign-application/CampaignApplicationRemark.tsx b/src/components/client/campaign-application/steps/CampaignApplicationRemark.tsx similarity index 100% rename from src/components/client/campaign-application/CampaignApplicationRemark.tsx rename to src/components/client/campaign-application/steps/CampaignApplicationRemark.tsx diff --git a/src/components/client/campaign-application/CampaignApplicationStepperIcon.tsx b/src/components/client/campaign-application/steps/CampaignApplicationStepperIcon.tsx similarity index 81% rename from src/components/client/campaign-application/CampaignApplicationStepperIcon.tsx rename to src/components/client/campaign-application/steps/CampaignApplicationStepperIcon.tsx index 930065e59..4b86a72ca 100644 --- a/src/components/client/campaign-application/CampaignApplicationStepperIcon.tsx +++ b/src/components/client/campaign-application/steps/CampaignApplicationStepperIcon.tsx @@ -1,5 +1,5 @@ import { StepIconProps } from '@mui/material/StepIcon' -import { StyledCampaignApplicationStepperIcon } from './helpers/campaignApplication.styled' +import { StyledCampaignApplicationStepperIcon } from '../helpers/campaignApplication.styled' export default function CampaignApplicationStepperIcon(props: StepIconProps) { const icons: { [index: string]: React.ReactElement } = { diff --git a/src/components/client/campaign-application/steps/CampaignApplicationSummary.tsx b/src/components/client/campaign-application/steps/CampaignApplicationSummary.tsx new file mode 100644 index 000000000..f320c102b --- /dev/null +++ b/src/components/client/campaign-application/steps/CampaignApplicationSummary.tsx @@ -0,0 +1,145 @@ +import { Grid, Typography } from '@mui/material' +import { green, orange, red } from '@mui/material/colors' +import { CampaignApplicationResponse } from 'gql/campaign-applications' +import { useTranslation } from 'next-i18next' +import { CampaignEndTypes } from '../helpers/campaignApplication.types' + +export interface SummaryProps { + uploadedFiles: Record + camApp?: CampaignApplicationResponse + deletedFiles?: Record + isEdit?: boolean + prependChildren?: JSX.Element +} + +function FilesDetail({ + label, + files, + type, +}: { + label: string + files?: string[] + type?: 'success' | 'failure' | 'successful-delete' +}) { + return ( + Number(files?.length) > 0 && ( + <> + + {label} + + + {files?.map((f) => ( + + {f} + + ))} + + + ) + ) +} + +export function CamAppDetail({ label, value }: { label: string; value?: string | JSX.Element }) { + const normalized = + typeof value === 'string' && value.trim() != '' ? value : value != null ? value : '-' + return ( + <> + + {label} + + + {normalized} + + + ) +} + +export default function CampaignApplicationSummary({ + uploadedFiles, + camApp, + deletedFiles, + isEdit, + prependChildren, +}: SummaryProps) { + const { t } = useTranslation('campaign-application') + + return ( + <> + {t(isEdit ? 'result.edited' : 'result.created')} + + + + {prependChildren} + + + {uploadedFiles.failed.length > 0 && ( + <> + +

{t('result.uploadFailedDirection')}

+ + )} + {deletedFiles && deletedFiles.failed.length > 0 && ( + <> + +

{t('result.uploadFailedDirection')}

+ + )} + + + + + + + + + + + +
+
+
+ + ) +} diff --git a/src/components/common/file-upload/FileUpload.tsx b/src/components/common/file-upload/FileUpload.tsx index db3b59444..8ad2af498 100644 --- a/src/components/common/file-upload/FileUpload.tsx +++ b/src/components/common/file-upload/FileUpload.tsx @@ -3,9 +3,11 @@ import { Button } from '@mui/material' function FileUpload({ onUpload, buttonLabel, + ...rest }: { onUpload: (files: File[]) => void buttonLabel: string + accept?: string }) { return (