From b9b1342a5fd3e1d2fef29c536ec3da41d7baefdc Mon Sep 17 00:00:00 2001 From: amrit Date: Tue, 8 Sep 2026 10:46:27 +0545 Subject: [PATCH 1/3] fix(cms): delete buttons updated --- app/components/CategoryModal/index.tsx | 50 +++++++++++++++++++--- app/components/EditDeleteActions/index.tsx | 25 +++++++++-- app/views/Users/index.tsx | 3 +- backend | 2 +- 4 files changed, 69 insertions(+), 11 deletions(-) diff --git a/app/components/CategoryModal/index.tsx b/app/components/CategoryModal/index.tsx index 34f6034..0cf1507 100644 --- a/app/components/CategoryModal/index.tsx +++ b/app/components/CategoryModal/index.tsx @@ -132,6 +132,7 @@ function CategoryModal(props: Props) { const [categoryName, setCategoryName] = useState(); const [editingId, setEditingId] = useState(); + const [deletingId, setDeletingId] = useState(); const [{ fetching, data, error }, reExecuteQuery] = useThematicAreasQuery({ variables: { @@ -201,13 +202,25 @@ function CategoryModal(props: Props) { setCategoryName(undefined); }, []); - const handleDelete = useCallback((id: string) => { - deleteThematicArea({ id }).then((resp) => { + const handleDeleteClick = useCallback((id: string) => { + setDeletingId(id); + }, []); + + const handleDeleteCancel = useCallback(() => { + setDeletingId(undefined); + }, []); + + const handleDeleteConfirm = useCallback(() => { + if (isNotDefined(deletingId)) { + return; + } + setDeletingId(undefined); + deleteThematicArea({ id: deletingId }).then((resp) => { handleResult(resp.data?.deleteThematicArea, 'Category deleted successfully'); }).catch(() => { alert.show(errorMessage, { variant: 'danger' }); }); - }, [deleteThematicArea, handleResult, alert]); + }, [deletingId, deleteThematicArea, handleResult, alert]); const columns = useMemo(() => [ createStringColumn( @@ -222,12 +235,12 @@ function CategoryModal(props: Props) { (_, datum) => ({ id: datum.id, onEdit: handleEdit, - onDelete: handleDelete, + onDelete: handleDeleteClick, disabled: actionPending, }), { columnWidth: 100 }, ), - ], [handleEdit, handleDelete, actionPending]); + ], [handleEdit, handleDeleteClick, actionPending]); const isEditing = isDefined(editingId); @@ -292,6 +305,33 @@ function CategoryModal(props: Props) { onActivePageChange={setPage} /> + {isDefined(deletingId) && ( + + + + + )} + > + {`Are you sure you want to delete "${categories?.find((item) => item.id === deletingId)?.name || 'this category'}"? This action cannot be undone.`} + + )} ); } diff --git a/app/components/EditDeleteActions/index.tsx b/app/components/EditDeleteActions/index.tsx index 3c3be5a..66e6e72 100644 --- a/app/components/EditDeleteActions/index.tsx +++ b/app/components/EditDeleteActions/index.tsx @@ -23,8 +23,22 @@ export interface Props { onDelete: (id: string) => void; itemTitle: string; to: keyof RoutesMap; + deleteMode?: 'delete' | 'deactivate'; } +const deleteCopy = { + delete: { + actionLabel: 'Delete', + heading: 'Delete item?', + message: (title: string) => `Are you sure you want to delete "${title}"? This action cannot be undone.`, + }, + deactivate: { + actionLabel: 'Deactivate', + heading: 'Deactivate user?', + message: (title: string) => `Are you sure you want to deactivate "${title}"? They will lose access, and you can reactivate them later.`, + }, +}; + function EditDeleteActions(props: Props) { const { id, @@ -33,8 +47,11 @@ function EditDeleteActions(props: Props) { to, member, dashboard, + deleteMode = 'delete', } = props; + const copy = deleteCopy[deleteMode]; + const navigate = useRouting(); const [showDeleteModal, setShowDeleteModal] = useState(false); @@ -77,14 +94,14 @@ function EditDeleteActions(props: Props) { {showDeleteModal && ( - Delete + {copy.actionLabel} )} > - {`Are you sure you want to delete "${itemTitle || 'this item'}"? This action cannot be undone.`} + {copy.message(itemTitle || 'this item')} )} diff --git a/app/views/Users/index.tsx b/app/views/Users/index.tsx index 9154d65..62cf991 100644 --- a/app/views/Users/index.tsx +++ b/app/views/Users/index.tsx @@ -136,7 +136,7 @@ function UsersList() { const result = resp.data?.deleteUser; if (result?.ok) { reExecuteQuery(); - alert.show('User deleted successfully', { variant: 'success' }); + alert.show('User deactivated successfully', { variant: 'success' }); } else { alert.show(errorMessage, { variant: 'danger' }); } @@ -204,6 +204,7 @@ function UsersList() { onDelete: onDeleteClick, itemTitle: datum.fullName, to: 'editUser', + deleteMode: 'deactivate', }), { columnWidth: 150 }, ), diff --git a/backend b/backend index 1d579a0..6041aee 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit 1d579a0c7a7f8867f05689a026b55304380192d1 +Subproject commit 6041aeed7a6ffc78f5e73e0ec09d0c7908a54a0c From ed0db290501ae4fdf6118d380308f1799140b65e Mon Sep 17 00:00:00 2001 From: amrit Date: Tue, 8 Sep 2026 11:05:27 +0545 Subject: [PATCH 2/3] feat(pmer): add pmer table and form --- app/Root/config/routes.ts | 19 ++ app/Root/index.tsx | 2 + app/Root/query.ts | 8 + app/contexts/GlobalEnumsContext.ts | 6 + app/utils/common.ts | 1 + app/views/Pmer/PmerFilters/index.tsx | 79 ++++++ app/views/Pmer/PmerForm/index.tsx | 403 +++++++++++++++++++++++++++ app/views/Pmer/index.tsx | 213 ++++++++++++++ app/views/Pmer/query.ts | 90 ++++++ app/views/PrivateLayout/index.tsx | 5 + backend | 2 +- 11 files changed, 827 insertions(+), 1 deletion(-) create mode 100644 app/views/Pmer/PmerFilters/index.tsx create mode 100644 app/views/Pmer/PmerForm/index.tsx create mode 100644 app/views/Pmer/index.tsx create mode 100644 app/views/Pmer/query.ts diff --git a/app/Root/config/routes.ts b/app/Root/config/routes.ts index eaa524f..74760b9 100644 --- a/app/Root/config/routes.ts +++ b/app/Root/config/routes.ts @@ -188,6 +188,22 @@ const editDocument: RouteConfig = { load: () => import('#views/Documents/DocumentsForm'), visibility: 'is-authenticated', }; +const pmer: RouteConfig = { + index: true, + path: '/pmer', + load: () => import('#views/Pmer'), + visibility: 'is-authenticated', +}; +const createPmer: RouteConfig = { + path: '/pmer/new', + load: () => import('#views/Pmer/PmerForm'), + visibility: 'is-authenticated', +}; +const editPmer: RouteConfig = { + path: '/pmer/:id/edit', + load: () => import('#views/Pmer/PmerForm'), + visibility: 'is-authenticated', +}; const onlineInteractive: RouteConfig = { index: true, path: '/online-interactive', @@ -274,6 +290,9 @@ const routes = { documents, createDocument, editDocument, + pmer, + createPmer, + editPmer, onlineInteractive, createOnlineInteractive, editOnlineInteractive, diff --git a/app/Root/index.tsx b/app/Root/index.tsx index 299fd91..cff4626 100644 --- a/app/Root/index.tsx +++ b/app/Root/index.tsx @@ -88,6 +88,8 @@ function RootContent() { dashboardPage: globalEnumsData?.enums.DashboardPage, reportContentType: globalEnumsData?.enums.ReportContentType, reportVisibility: globalEnumsData?.enums.ReportVisibility, + pmerReportCategory: globalEnumsData?.enums.PmerReportCategory, + pmerReportDocumentType: globalEnumsData?.enums.PmerReportDocumentType, }), [globalEnumsData]); return ( diff --git a/app/Root/query.ts b/app/Root/query.ts index 33d1a21..1e4a2ba 100644 --- a/app/Root/query.ts +++ b/app/Root/query.ts @@ -32,6 +32,14 @@ export const GLOBAL_ENUMS = gql` key label } + PmerReportCategory { + key + label + } + PmerReportDocumentType { + key + label + } } } `; diff --git a/app/contexts/GlobalEnumsContext.ts b/app/contexts/GlobalEnumsContext.ts index 62809e9..91766a9 100644 --- a/app/contexts/GlobalEnumsContext.ts +++ b/app/contexts/GlobalEnumsContext.ts @@ -3,6 +3,8 @@ import { createContext } from 'react'; import type { AppEnumCollectionDashboardPage, AppEnumCollectionLinkType, + AppEnumCollectionPmerReportCategory, + AppEnumCollectionPmerReportDocumentType, AppEnumCollectionReportContentType, AppEnumCollectionReportType, AppEnumCollectionReportVisibility, @@ -18,6 +20,8 @@ export interface GlobalEnumsContextInterface { dashboardPage: AppEnumCollectionDashboardPage[] | undefined; reportContentType: AppEnumCollectionReportContentType[] | undefined; reportVisibility: AppEnumCollectionReportVisibility[] | undefined; + pmerReportCategory: AppEnumCollectionPmerReportCategory[] | undefined; + pmerReportDocumentType: AppEnumCollectionPmerReportDocumentType[] | undefined; } const GlobalEnumsContext = createContext({ @@ -28,6 +32,8 @@ const GlobalEnumsContext = createContext({ dashboardPage: undefined, reportContentType: undefined, reportVisibility: undefined, + pmerReportCategory: undefined, + pmerReportDocumentType: undefined, }); export default GlobalEnumsContext; diff --git a/app/utils/common.ts b/app/utils/common.ts index ac89218..7298f27 100644 --- a/app/utils/common.ts +++ b/app/utils/common.ts @@ -64,6 +64,7 @@ export function getReadableFileSize(bytes: number | null | undefined): string { } export const ACCEPTED_REPORT_FILE_TYPES = '.pdf,.doc,.docx,.png,.jpg,.jpeg'; +export const ACCEPTED_PMER_FILE_TYPES = '.docx,.pdf,.xls,.xlsx,.csv,.png'; export const ACCEPTED_IMAGE_TYPES = 'image/*'; export const ACCEPTED_IMPORT_FILE_TYPES = '.xlsx,.xlsm'; export const MAX_REPORT_FILE_SIZE = 5 * 1024 * 1024; // 5MB diff --git a/app/views/Pmer/PmerFilters/index.tsx b/app/views/Pmer/PmerFilters/index.tsx new file mode 100644 index 0000000..43076a4 --- /dev/null +++ b/app/views/Pmer/PmerFilters/index.tsx @@ -0,0 +1,79 @@ +import { + Button, + SelectInput, + TextInput, +} from '@ifrc-go/ui'; +import { type EntriesAsList } from '@togglecorp/toggle-form'; + +import RegionSelectInput from '#components/RegionSelectInput'; +import { AdminAreaLevel } from '#generated/types/graphql'; +import useGlobalEnums from '#hooks/useGlobalEnums'; +import { + keySelector, + labelSelector, +} from '#utils/common'; + +import type { PmerFilterType } from '..'; + +export interface Props { + value: PmerFilterType; + onChange: (...args: EntriesAsList) => void; + onReset: () => void; + filtered: boolean; +} + +function PmerFilters({ + value, onChange, onReset, filtered, +}: Props) { + const { + pmerReportCategory: categoryOptions, + pmerReportDocumentType: reportTypeOptions, + } = useGlobalEnums(); + + return ( + <> + + + + + + + + ); +} + +export default PmerFilters; diff --git a/app/views/Pmer/PmerForm/index.tsx b/app/views/Pmer/PmerForm/index.tsx new file mode 100644 index 0000000..6f00ef8 --- /dev/null +++ b/app/views/Pmer/PmerForm/index.tsx @@ -0,0 +1,403 @@ +import { + useCallback, + useEffect, + useMemo, +} from 'react'; +import { useParams } from 'react-router'; +import { + BlockLoading, + Button, + Container, + InputSection, + ListView, + RadioInput, + SelectInput, + TextArea, + TextInput, +} from '@ifrc-go/ui'; +import { + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; +import { + createSubmitHandler, + getErrorObject, + type ObjectSchema, + type PartialForm, + removeNull, + requiredStringCondition, + useForm, +} from '@togglecorp/toggle-form'; + +import FileInput from '#components/FileInput'; +import NonFieldError from '#components/NonFieldError'; +import RegionSelectInput from '#components/RegionSelectInput'; +import { + AdminAreaLevel, + type PmerReportCreateInput, + PmerReportDocumentType, + type PmerReportUpdateInput, + ReportVisibility, + useCreatePmerReportMutation, + usePmerReportDetailQuery, + useUpdatePmerReportMutation, +} from '#generated/types/graphql'; +import useAlert from '#hooks/useAlert'; +import useGlobalEnums from '#hooks/useGlobalEnums'; +import useRouting from '#hooks/useRouting'; +import { + ACCEPTED_PMER_FILE_TYPES, + errorMessage, + keySelector, + labelSelector, + transformToFormError, +} from '#utils/common'; + +type PartialFormType = PartialForm; +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +function getPmerSchema(isEditing: boolean): FormSchema { + return { + fields: (): FormSchemaFields => ({ + title: { + required: true, + requiredValidation: requiredStringCondition, + }, + description: {}, + category: { + required: true, + }, + reportType: { + required: true, + }, + file: { + required: !isEditing, + }, + visibility: { + required: true, + }, + department: {}, + region: { + required: true, + }, + project: {}, + }), + }; +} + +const defaultFormValue: PartialFormType = { + visibility: ReportVisibility.Public, +}; + +function PmerForm() { + const { id } = useParams(); + const navigate = useRouting(); + const alert = useAlert(); + + const isEditing = isDefined(id); + + const pmerSchema = useMemo(() => getPmerSchema(isEditing), [isEditing]); + + const { + setFieldValue, + error: formError, + value, + validate, + setError, + setValue, + } = useForm(pmerSchema, { value: defaultFormValue }); + + const [{ data: detailData, fetching: detailFetching }] = usePmerReportDetailQuery({ + variables: { id: isDefined(id) ? id : '' }, + pause: isNotDefined(id), + }); + + const [{ fetching: creating }, createPmerReport] = useCreatePmerReportMutation(); + const [{ fetching: updating }, updatePmerReport] = useUpdatePmerReportMutation(); + + const pending = creating || updating || detailFetching; + + const { + reportVisibility: visibilityOptions, + pmerReportCategory: categoryOptions, + pmerReportDocumentType: reportTypeOptions, + } = useGlobalEnums(); + + const fileName = value.file instanceof File + ? value.file.name + : detailData?.pmerReport?.file?.name?.split('/').pop(); + + const handleReportTypeChange = useCallback( + (reportType: PmerReportDocumentType | undefined, name: 'reportType') => { + setFieldValue(reportType, name); + setFieldValue( + reportType === PmerReportDocumentType.AnnualPlan + || reportType === PmerReportDocumentType.AnnualReport + ? ReportVisibility.Private + : ReportVisibility.Public, + 'visibility', + ); + }, + [setFieldValue], + ); + + const handleFileChange = useCallback( + (file: File | undefined, name: 'file') => { + setFieldValue(file, name); + }, + [setFieldValue], + ); + + const handleResult = useCallback(( + result: { + ok?: boolean | null; + errors?: Parameters[0] | null; + } | undefined | null, + successMessage: string, + ) => { + if (isDefined(result) && result.ok) { + navigate('pmer'); + alert.show(successMessage, { variant: 'success' }); + } else if (isDefined(result) && isDefined(result.errors)) { + setError(transformToFormError(result.errors)); + alert.show(errorMessage, { variant: 'danger' }); + } else { + alert.show(errorMessage, { variant: 'danger' }); + } + }, [navigate, alert, setError]); + + const handleCreate = useCallback(async (formValues: PartialFormType) => { + const { file, ...rest } = formValues; + + const res = await createPmerReport({ + data: { + ...removeNull(rest), + ...(isDefined(file) ? { file } : {}), + } as PmerReportCreateInput, + }); + + handleResult(res.data?.createPmerReport, 'PMER created successfully'); + }, [createPmerReport, handleResult]); + + const handleUpdate = useCallback(async (formValues: PartialFormType) => { + if (isNotDefined(id)) { + return; + } + const { file, ...rest } = formValues; + + const res = await updatePmerReport({ + id, + data: { + ...removeNull(rest), + ...(isDefined(file) ? { file } : {}), + } as PmerReportUpdateInput, + }); + + handleResult(res.data?.updatePmerReport, 'PMER updated successfully'); + }, [id, updatePmerReport, handleResult]); + + const handleFormSubmit = useCallback( + () => createSubmitHandler( + validate, + setError, + isEditing ? handleUpdate : handleCreate, + )(), + [validate, setError, isEditing, handleUpdate, handleCreate], + ); + + const handleCancel = useCallback(() => { + navigate('pmer'); + }, [navigate]); + + const error = getErrorObject(formError); + + useEffect(() => { + if (isNotDefined(detailData?.pmerReport)) { + return; + } + const { + region, + ...otherValues + } = removeNull(detailData.pmerReport); + + delete (otherValues as { file?: unknown }).file; + setValue({ + ...otherValues, + region: region?.id, + }); + }, [detailData, setValue]); + + if (detailFetching) { + return ( + + ); + } + + return ( + + + + + )} + > + + + + + + +