1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-01 21:52:19 +01:00

(feat) add supporting document for child

This commit is contained in:
Du Peng 2023-05-24 19:56:48 +02:00
parent b473a7cd35
commit ecc4fde47f
42 changed files with 384 additions and 204 deletions

View File

@ -50,6 +50,9 @@ class API::ChildrenController < API::APIController
end
def child_params
params.require(:child).permit(:first_name, :last_name, :email, :phone, :birthday, :user_id)
params.require(:child).permit(:first_name, :last_name, :email, :phone, :birthday, :user_id,
supporting_document_files_attributes: %i[id supportable_id supportable_type
supporting_document_type_id
attachment _destroy])
end
end

View File

@ -48,6 +48,6 @@ class API::SupportingDocumentFilesController < API::APIController
# Never trust parameters from the scary internet, only allow the white list through.
def supporting_document_file_params
params.required(:supporting_document_file).permit(:supporting_document_type_id, :attachment, :user_id)
params.required(:supporting_document_file).permit(:supporting_document_type_id, :attachment, :supportable_id, :supportable_type)
end
end

View File

@ -27,6 +27,7 @@ class API::SupportingDocumentRefusalsController < API::APIController
# Never trust parameters from the scary internet, only allow the white list through.
def supporting_document_refusal_params
params.required(:supporting_document_refusal).permit(:message, :operator_id, :user_id, supporting_document_type_ids: [])
params.required(:supporting_document_refusal).permit(:message, :operator_id, :supportable_id, :supportable_type,
supporting_document_type_ids: [])
end
end

View File

@ -45,6 +45,6 @@ class API::SupportingDocumentTypesController < API::APIController
end
def supporting_document_type_params
params.require(:supporting_document_type).permit(:name, group_ids: [])
params.require(:supporting_document_type).permit(:name, :document_type, group_ids: [])
end
end

View File

@ -15,12 +15,22 @@ export default class ChildAPI {
}
static async create (child: Child): Promise<Child> {
const res: AxiosResponse<Child> = await apiClient.post('/api/children', { child });
const data = ApiLib.serializeAttachments(child, 'child', ['supporting_document_files_attributes']);
const res: AxiosResponse<Child> = await apiClient.post('/api/children', data, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
return res?.data;
}
static async update (child: Child): Promise<Child> {
const res: AxiosResponse<Child> = await apiClient.patch(`/api/children/${child.id}`, { child });
const data = ApiLib.serializeAttachments(child, 'child', ['supporting_document_files_attributes']);
const res: AxiosResponse<Child> = await apiClient.put(`/api/children/${child.id}`, data, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
return res?.data;
}

View File

@ -1,33 +1,34 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import moment from 'moment';
import { Child } from '../../models/child';
import { TDateISODate } from '../../typings/date-iso';
import { FormInput } from '../form/form-input';
import { FabButton } from '../base/fab-button';
import { FormFileUpload } from '../form/form-file-upload';
import { FileType } from '../../models/file';
import { SupportingDocumentType } from '../../models/supporting-document-type';
interface ChildFormProps {
child: Child;
onChange: (field: string, value: string | TDateISODate) => void;
onSubmit: (data: Child) => void;
supportingDocumentsTypes: Array<SupportingDocumentType>;
}
/**
* A form for creating or editing a child.
*/
export const ChildForm: React.FC<ChildFormProps> = ({ child, onChange, onSubmit }) => {
export const ChildForm: React.FC<ChildFormProps> = ({ child, onSubmit, supportingDocumentsTypes }) => {
const { t } = useTranslation('public');
const { register, formState, handleSubmit } = useForm<Child>({
const { register, formState, handleSubmit, setValue, control } = useForm<Child>({
defaultValues: child
});
const output = useWatch<Child>({ control }); // eslint-disable-line
/**
* Handle the change of a child form field
*/
const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
onChange(event.target.id, event.target.value);
const getSupportingDocumentsTypeName = (id: number): string => {
const supportingDocumentType = supportingDocumentsTypes.find((supportingDocumentType) => supportingDocumentType.id === id);
return supportingDocumentType ? supportingDocumentType.name : '';
};
return (
@ -41,14 +42,12 @@ export const ChildForm: React.FC<ChildFormProps> = ({ child, onChange, onSubmit
rules={{ required: true }}
formState={formState}
label={t('app.public.child_form.first_name')}
onChange={handleChange}
/>
<FormInput id="last_name"
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.public.child_form.last_name')}
onChange={handleChange}
/>
<FormInput id="birthday"
register={register}
@ -57,22 +56,31 @@ export const ChildForm: React.FC<ChildFormProps> = ({ child, onChange, onSubmit
label={t('app.public.child_form.birthday')}
type="date"
max={moment().subtract(18, 'year').format('YYYY-MM-DD')}
onChange={handleChange}
/>
<FormInput id="phone"
register={register}
formState={formState}
label={t('app.public.child_form.phone')}
onChange={handleChange}
type="tel"
/>
<FormInput id="email"
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.public.child_form.email')}
onChange={handleChange}
/>
{output.supporting_document_files_attributes.map((sf, index) => {
return (
<FormFileUpload key={index}
defaultFile={sf as FileType}
id={`supporting_document_files_attributes.${index}`}
accept="application/pdf"
setValue={setValue}
label={getSupportingDocumentsTypeName(sf.supporting_document_type_id)}
showRemoveButton={false}
register={register}
formState={formState} />
);
})}
<div className="actions">
<FabButton type="button" onClick={handleSubmit(onSubmit)}>

View File

@ -1,11 +1,10 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FabModal, ModalSize } from '../base/fab-modal';
import { Child } from '../../models/child';
import { TDateISODate } from '../../typings/date-iso';
import ChildAPI from '../../api/child';
import { ChildForm } from './child-form';
import { SupportingDocumentType } from '../../models/supporting-document-type';
interface ChildModalProps {
child?: Child;
@ -13,23 +12,19 @@ interface ChildModalProps {
toggleModal: () => void;
onSuccess: (child: Child) => void;
onError: (error: string) => void;
supportingDocumentsTypes: Array<SupportingDocumentType>;
}
/**
* A modal for creating or editing a child.
*/
export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleModal, onSuccess, onError }) => {
export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleModal, onSuccess, onError, supportingDocumentsTypes }) => {
const { t } = useTranslation('public');
const [data, setData] = useState<Child>(child);
useEffect(() => {
setData(child);
}, [child]);
/**
* Save the child to the API
*/
const handleSaveChild = async (): Promise<void> => {
const handleSaveChild = async (data: Child): Promise<void> => {
try {
if (child?.id) {
await ChildAPI.update(data);
@ -43,25 +38,14 @@ export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleMod
}
};
/**
* Handle the change of a child form field
*/
const handleChildChanged = (field: string, value: string | TDateISODate): void => {
setData({
...data,
[field]: value
});
};
return (
<FabModal title={t(`app.public.child_modal.${child?.id ? 'edit' : 'new'}_child`)}
width={ModalSize.large}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={true}
confirmButton={false}
onConfirm={handleSaveChild} >
<ChildForm child={child} onChange={handleChildChanged} onSubmit={handleSaveChild} />
confirmButton={false} >
<ChildForm child={child} onSubmit={handleSaveChild} supportingDocumentsTypes={supportingDocumentsTypes}/>
</FabModal>
);
};

View File

@ -9,6 +9,8 @@ import { IApplication } from '../../models/application';
import { ChildModal } from './child-modal';
import { ChildItem } from './child-item';
import { FabButton } from '../base/fab-button';
import { SupportingDocumentType } from '../../models/supporting-document-type';
import SupportingDocumentTypeAPI from '../../api/supporting-document-type';
declare const Application: IApplication;
@ -27,9 +29,13 @@ export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser, onError
const [children, setChildren] = useState<Array<Child>>([]);
const [isOpenChildModal, setIsOpenChildModal] = useState<boolean>(false);
const [child, setChild] = useState<Child>();
const [supportingDocumentsTypes, setSupportingDocumentsTypes] = useState<Array<SupportingDocumentType>>([]);
useEffect(() => {
ChildAPI.index({ user_id: currentUser.id }).then(setChildren);
SupportingDocumentTypeAPI.index({ document_type: 'Child' }).then(tData => {
setSupportingDocumentsTypes(tData);
});
}, [currentUser]);
/**
@ -37,7 +43,12 @@ export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser, onError
*/
const addChild = () => {
setIsOpenChildModal(true);
setChild({ user_id: currentUser.id } as Child);
setChild({
user_id: currentUser.id,
supporting_document_files_attributes: supportingDocumentsTypes.map(t => {
return { supporting_document_type_id: t.id };
})
} as Child);
};
/**
@ -45,7 +56,13 @@ export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser, onError
*/
const editChild = (child: Child) => {
setIsOpenChildModal(true);
setChild(child);
setChild({
...child,
supporting_document_files_attributes: supportingDocumentsTypes.map(t => {
const file = child.supporting_document_files_attributes.find(f => f.supporting_document_type_id === t.id);
return file || { supporting_document_type_id: t.id };
})
} as Child);
};
/**
@ -78,7 +95,7 @@ export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser, onError
<ChildItem key={child.id} child={child} onEdit={editChild} onDelete={deleteChild} />
))}
</div>
<ChildModal child={child} isOpen={isOpenChildModal} toggleModal={() => setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} />
<ChildModal child={child} isOpen={isOpenChildModal} toggleModal={() => setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} supportingDocumentsTypes={supportingDocumentsTypes} />
</section>
);
};

View File

@ -19,12 +19,13 @@ type FormFileUploadProps<TFieldValues> = FormComponent<TFieldValues> & AbstractF
accept?: string,
onFileChange?: (value: FileType) => void,
onFileRemove?: () => void,
showRemoveButton?: boolean,
}
/**
* This component allows to upload file, in forms managed by react-hook-form.
*/
export const FormFileUpload = <TFieldValues extends FieldValues>({ id, label, register, defaultFile, className, rules, disabled, error, warning, formState, onFileChange, onFileRemove, accept, setValue }: FormFileUploadProps<TFieldValues>) => {
export const FormFileUpload = <TFieldValues extends FieldValues>({ id, label, register, defaultFile, className, rules, disabled, error, warning, formState, onFileChange, onFileRemove, accept, setValue, showRemoveButton = true }: FormFileUploadProps<TFieldValues>) => {
const { t } = useTranslation('shared');
const [file, setFile] = useState<FileType>(defaultFile);
@ -100,7 +101,7 @@ export const FormFileUpload = <TFieldValues extends FieldValues>({ id, label, re
id={`${id}[attachment_files]`}
onChange={onFileSelected}
placeholder={placeholder()}/>
{hasFile() &&
{showRemoveButton && hasFile() &&
<FabButton onClick={onRemoveFile} icon={<Trash size={20} weight="fill" />} className="is-main" />
}
</div>

View File

@ -49,7 +49,7 @@ export const SupportingDocumentsFiles: React.FC<SupportingDocumentsFilesProps> =
SupportingDocumentTypeAPI.index({ group_id: currentUser.group_id }).then(tData => {
setSupportingDocumentsTypes(tData);
});
SupportingDocumentFileAPI.index({ user_id: currentUser.id }).then(fData => {
SupportingDocumentFileAPI.index({ supportable_id: currentUser.id, supportable_type: 'User' }).then(fData => {
setSupportingDocumentsFiles(fData);
});
}, []);
@ -106,7 +106,8 @@ export const SupportingDocumentsFiles: React.FC<SupportingDocumentsFilesProps> =
for (const proofOfIdentityTypeId of Object.keys(files)) {
const formData = new FormData();
formData.append('supporting_document_file[user_id]', currentUser.id.toString());
formData.append('supporting_document_file[supportable_id]', currentUser.id.toString());
formData.append('supporting_document_file[supportable_type]', 'User');
formData.append('supporting_document_file[supporting_document_type_id]', proofOfIdentityTypeId);
formData.append('supporting_document_file[attachment]', files[proofOfIdentityTypeId]);
const proofOfIdentityFile = getSupportingDocumentsFileByType(parseInt(proofOfIdentityTypeId, 10));
@ -117,7 +118,7 @@ export const SupportingDocumentsFiles: React.FC<SupportingDocumentsFilesProps> =
}
}
if (Object.keys(files).length > 0) {
SupportingDocumentFileAPI.index({ user_id: currentUser.id }).then(fData => {
SupportingDocumentFileAPI.index({ supportable_id: currentUser.id, supportable_type: 'User' }).then(fData => {
setSupportingDocumentsFiles(fData);
setFiles({});
onSuccess(t('app.logged.dashboard.supporting_documents_files.file_successfully_uploaded'));

View File

@ -27,7 +27,8 @@ export const SupportingDocumentsRefusalModal: React.FC<SupportingDocumentsRefusa
const [data, setData] = useState<SupportingDocumentRefusal>({
id: null,
operator_id: operator.id,
user_id: member.id,
supportable_id: member.id,
supportable_type: 'User',
supporting_document_type_ids: [],
message: ''
});

View File

@ -63,6 +63,7 @@ export const SupportingDocumentsTypeForm: React.FC<SupportingDocumentsTypeFormPr
{t('app.admin.settings.account.supporting_documents_type_form.type_form_info')}
</div>
<form name="supportingDocumentTypeForm">
{supportingDocumentType?.document_type === 'User' &&
<div className="field">
<Select defaultValue={groupsValues()}
placeholder={t('app.admin.settings.account.supporting_documents_type_form.select_group')}
@ -70,6 +71,7 @@ export const SupportingDocumentsTypeForm: React.FC<SupportingDocumentsTypeFormPr
options={buildOptions()}
isMulti />
</div>
}
<div className="field">
<FabInput id="supporting_document_type_name"
icon={<i className="fa fa-edit" />}

View File

@ -14,18 +14,19 @@ interface SupportingDocumentsTypeModalProps {
onError: (message: string) => void,
groups: Array<Group>,
proofOfIdentityType?: SupportingDocumentType,
documentType: 'User' | 'Child',
}
/**
* Modal dialog to create/edit a supporting documents type
*/
export const SupportingDocumentsTypeModal: React.FC<SupportingDocumentsTypeModalProps> = ({ isOpen, toggleModal, onSuccess, onError, proofOfIdentityType, groups }) => {
export const SupportingDocumentsTypeModal: React.FC<SupportingDocumentsTypeModalProps> = ({ isOpen, toggleModal, onSuccess, onError, proofOfIdentityType, groups, documentType }) => {
const { t } = useTranslation('admin');
const [data, setData] = useState<SupportingDocumentType>({ id: proofOfIdentityType?.id, group_ids: proofOfIdentityType?.group_ids || [], name: proofOfIdentityType?.name || '' });
const [data, setData] = useState<SupportingDocumentType>({ id: proofOfIdentityType?.id, group_ids: proofOfIdentityType?.group_ids || [], name: proofOfIdentityType?.name || '', document_type: documentType });
useEffect(() => {
setData({ id: proofOfIdentityType?.id, group_ids: proofOfIdentityType?.group_ids || [], name: proofOfIdentityType?.name || '' });
setData({ id: proofOfIdentityType?.id, group_ids: proofOfIdentityType?.group_ids || [], name: proofOfIdentityType?.name || '', document_type: documentType });
}, [proofOfIdentityType]);
/**
@ -63,7 +64,7 @@ export const SupportingDocumentsTypeModal: React.FC<SupportingDocumentsTypeModal
* Check if the form is valid (not empty)
*/
const isPreventedSaveType = (): boolean => {
return !data.name || data.group_ids.length === 0;
return !data.name || (documentType === 'User' && data.group_ids.length === 0);
};
return (

View File

@ -21,12 +21,13 @@ declare const Application: IApplication;
interface SupportingDocumentsTypesListProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
documentType: 'User' | 'Child',
}
/**
* This component shows a list of all types of supporting documents (e.g. student ID, Kbis extract, etc.)
*/
const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps> = ({ onSuccess, onError }) => {
const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps> = ({ onSuccess, onError, documentType }) => {
const { t } = useTranslation('admin');
// list of displayed supporting documents type
@ -48,7 +49,7 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
useEffect(() => {
GroupAPI.index({ disabled: false }).then(data => {
setGroups(data);
SupportingDocumentTypeAPI.index().then(pData => {
SupportingDocumentTypeAPI.index({ document_type: documentType }).then(pData => {
setSupportingDocumentsTypes(pData);
});
});
@ -91,7 +92,7 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
*/
const onSaveTypeSuccess = (message: string): void => {
setModalIsOpen(false);
SupportingDocumentTypeAPI.index().then(pData => {
SupportingDocumentTypeAPI.index({ document_type: documentType }).then(pData => {
setSupportingDocumentsTypes(orderTypes(pData, supportingDocumentsTypeOrder));
onSuccess(message);
}).catch((error) => {
@ -121,7 +122,7 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
*/
const onDestroySuccess = (message: string): void => {
setDestroyModalIsOpen(false);
SupportingDocumentTypeAPI.index().then(pData => {
SupportingDocumentTypeAPI.index({ document_type: documentType }).then(pData => {
setSupportingDocumentsTypes(pData);
setSupportingDocumentsTypes(orderTypes(pData, supportingDocumentsTypeOrder));
onSuccess(message);
@ -190,6 +191,7 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
window.location.href = '/#!/admin/members?tabs=1';
};
if (documentType === 'User') {
return (
<FabPanel className="supporting-documents-types-list" header={<div>
<span>{t('app.admin.settings.account.supporting_documents_types_list.add_supporting_documents_types')}</span>
@ -211,6 +213,7 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
<SupportingDocumentsTypeModal isOpen={modalIsOpen}
groups={groups}
proofOfIdentityType={supportingDocumentsType}
documentType={documentType}
toggleModal={toggleCreateAndEditModal}
onSuccess={onSaveTypeSuccess}
onError={onError} />
@ -267,6 +270,71 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
</div>
</FabPanel>
);
} else if (documentType === 'Child') {
return (
<div className="supporting-documents-types-list">
<div className="types-list">
<div className="title">
<h3>{t('app.admin.settings.account.supporting_documents_types_list.supporting_documents_type_title')}</h3>
<FabButton onClick={addType}>{t('app.admin.settings.account.supporting_documents_types_list.add_type')}</FabButton>
</div>
<SupportingDocumentsTypeModal isOpen={modalIsOpen}
groups={groups}
proofOfIdentityType={supportingDocumentsType}
documentType={documentType}
toggleModal={toggleCreateAndEditModal}
onSuccess={onSaveTypeSuccess}
onError={onError} />
<DeleteSupportingDocumentsTypeModal isOpen={destroyModalIsOpen}
proofOfIdentityTypeId={supportingDocumentsTypeId}
toggleModal={toggleDestroyModal}
onSuccess={onDestroySuccess}
onError={onError}/>
<table>
<thead>
<tr>
<th className="name">
<a onClick={setTypeOrder('name')}>
{t('app.admin.settings.account.supporting_documents_types_list.name')}
<i className={orderClassName('name')} />
</a>
</th>
<th className="actions"></th>
</tr>
</thead>
<tbody>
{supportingDocumentsTypes.map(poit => {
return (
<tr key={poit.id}>
<td>{poit.name}</td>
<td>
<div className="buttons">
<FabButton className="edit-btn" onClick={editType(poit)}>
<i className="fa fa-edit" />
</FabButton>
<FabButton className="delete-btn" onClick={destroyType(poit.id)}>
<i className="fa fa-trash" />
</FabButton>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
{!hasTypes() && (
<p className="no-types-info">
<HtmlTranslate trKey="app.admin.settings.account.supporting_documents_types_list.no_types" />
</p>
)}
</div>
</div>
);
} else {
return null;
}
};
const SupportingDocumentsTypesListWrapper: React.FC<SupportingDocumentsTypesListProps> = (props) => {
@ -277,4 +345,4 @@ const SupportingDocumentsTypesListWrapper: React.FC<SupportingDocumentsTypesList
);
};
Application.Components.component('supportingDocumentsTypesList', react2angular(SupportingDocumentsTypesListWrapper, ['onSuccess', 'onError']));
Application.Components.component('supportingDocumentsTypesList', react2angular(SupportingDocumentsTypesListWrapper, ['onSuccess', 'onError', 'documentType']));

View File

@ -39,7 +39,7 @@ const SupportingDocumentsValidation: React.FC<SupportingDocumentsValidationProps
SupportingDocumentTypeAPI.index({ group_id: member.group_id }).then(tData => {
setDocumentsTypes(tData);
});
SupportingDocumentFileAPI.index({ user_id: member.id }).then(fData => {
SupportingDocumentFileAPI.index({ supportable_id: member.id, supportable_type: 'User' }).then(fData => {
setDocumentsFiles(fData);
});
}, []);

View File

@ -35,6 +35,9 @@ export default class ApiLib {
if (file?.is_main) {
data.set(`${name}[${attr}][${i}][is_main]`, file.is_main.toString());
}
if (file?.supporting_document_type_id) {
data.set(`${name}[${attr}][${i}][supporting_document_type_id]`, file.supporting_document_type_id.toString());
}
});
} else {
if (object[attr]?.attachment_files && object[attr]?.attachment_files[0]) {

View File

@ -12,5 +12,15 @@ export interface Child {
email?: string,
phone?: string,
birthday: TDateISODate,
user_id: number
user_id: number,
supporting_document_files_attributes?: Array<{
id?: number,
supportable_id?: number,
supportable_type?: 'User' | 'Child',
supporting_document_type_id: number,
attachment?: File,
attachment_name?: string,
attachment_url?: string,
_destroy?: boolean
}>,
}

View File

@ -1,12 +1,14 @@
import { ApiFilter } from './api';
export interface SupportingDocumentFileIndexFilter extends ApiFilter {
user_id: number,
supportable_id: number,
supportable_type?: 'User' | 'Child',
}
export interface SupportingDocumentFile {
id?: number,
attachment?: string,
user_id?: number,
supportable_id?: number,
supportable_type?: 'User' | 'Child',
supporting_document_type_id: number,
}

View File

@ -1,13 +1,15 @@
import { ApiFilter } from './api';
export interface SupportingDocumentRefusalIndexFilter extends ApiFilter {
user_id: number,
supportable_id: number,
supportable_type: 'User' | 'Child',
}
export interface SupportingDocumentRefusal {
id: number,
message: string,
user_id: number,
supportable_id: number,
supportable_type: 'User' | 'Child',
operator_id: number,
supporting_document_type_ids: Array<number>,
}

View File

@ -2,10 +2,12 @@ import { ApiFilter } from './api';
export interface SupportingDocumentTypeIndexfilter extends ApiFilter {
group_id?: number,
document_type?: 'User' | 'Child'
}
export interface SupportingDocumentType {
id: number,
name: string,
group_ids: Array<number>
group_ids: Array<number>,
document_type: 'User' | 'Child'
}

View File

@ -62,6 +62,12 @@
on-error="onError">
</div>
</div>
<div class="row">
<div class="col-md-12">
<supporting-documents-types-list on-success="onSuccess" on-error="onError" document-type="'Child'" />
</div>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.captcha' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.captcha_info_html' | translate"></p>
@ -167,4 +173,4 @@
</div>
<supporting-documents-types-list on-success="onSuccess" on-error="onError"/>
<supporting-documents-types-list on-success="onSuccess" on-error="onError" document-type="'User'" />

View File

@ -4,9 +4,13 @@
class Child < ApplicationRecord
belongs_to :user
has_many :supporting_document_files, as: :supportable, dependent: :destroy
accepts_nested_attributes_for :supporting_document_files, allow_destroy: true, reject_if: :all_blank
has_many :supporting_document_refusals, as: :supportable, dependent: :destroy
validates :first_name, presence: true
validates :last_name, presence: true
validates :email, presence: true, format: { with: Devise.email_regexp }
# validates :email, presence: true, format: { with: Devise.email_regexp }
validate :validate_age
# birthday should less than 18 years ago

View File

@ -6,7 +6,7 @@ class SupportingDocumentFile < ApplicationRecord
mount_uploader :attachment, SupportingDocumentFileUploader
belongs_to :supporting_document_type
belongs_to :user
belongs_to :supportable, polymorphic: true
validates :attachment, file_size: { maximum: ENV.fetch('MAX_SUPPORTING_DOCUMENT_FILE_SIZE', 5.megabytes).to_i }
end

View File

@ -2,7 +2,7 @@
# An admin can mark an uploaded document as refused, this will notify the member
class SupportingDocumentRefusal < ApplicationRecord
belongs_to :user
belongs_to :supportable, polymorphic: true
belongs_to :operator, class_name: 'User', inverse_of: :supporting_document_refusals
has_many :supporting_document_refusals_types, dependent: :destroy
has_many :supporting_document_types, through: :supporting_document_refusals_types

View File

@ -8,4 +8,6 @@ class SupportingDocumentType < ApplicationRecord
has_many :supporting_document_refusals_types, dependent: :destroy
has_many :supporting_document_refusals, through: :supporting_document_refusals_types
validates :document_type, presence: true, inclusion: { in: %w[User Child] }
end

View File

@ -47,8 +47,8 @@ class User < ApplicationRecord
has_many :accounting_periods, foreign_key: 'closed_by', dependent: :nullify, inverse_of: :user
has_many :supporting_document_files, dependent: :destroy
has_many :supporting_document_refusals, dependent: :destroy
has_many :supporting_document_files, as: :supportable, dependent: :destroy
has_many :supporting_document_refusals, as: :supportable, dependent: :destroy
has_many :notifications, as: :receiver, dependent: :destroy
has_many :notification_preferences, dependent: :destroy

View File

@ -6,15 +6,11 @@ class SupportingDocumentFilePolicy < ApplicationPolicy
user.privileged?
end
def create?
user.privileged? or record.user_id == user.id
end
def update?
user.privileged? or record.user_id == user.id
end
def download?
user.privileged? or record.user_id == user.id
%w[create update download].each do |action|
define_method "#{action}?" do
user.privileged? ||
(record.supportable_type == 'User' && record.supportable_id.to_i == user.id) ||
(record.supportable_type == 'Child' && user.children.exists?(id: record.supportable_id.to_i))
end
end
end

View File

@ -4,23 +4,32 @@
class SupportingDocumentFileService
def self.list(operator, filters = {})
files = []
if filters[:user_id].present? && (operator.privileged? || filters[:user_id].to_i == operator.id)
files = SupportingDocumentFile.where(user_id: filters[:user_id])
if filters[:supportable_id].present? && can_list?(operator, filters[:supportable_id], filters[:supportable_type])
files = SupportingDocumentFile.where(supportable_id: filters[:supportable_id], supportable_type: filters[:supportable_type])
end
files
end
def self.can_list?(operator, supportable_id, supportable_type)
operator.privileged? ||
(supportable_type == 'User' && supportable_id.to_i == operator.id) ||
(supportable_type == 'Child' && operator.children.exists?(id: supportable_id.to_i))
end
def self.create(supporting_document_file)
saved = supporting_document_file.save
if saved
user = User.find(supporting_document_file.user_id)
all_files_are_upload = true
if supporting_document_file.supportable_type == 'User'
user = supporting_document_file.supportable
user.group.supporting_document_types.each do |type|
file = type.supporting_document_files.find_by(user_id: supporting_document_file.user_id)
file = type.supporting_document_files.find_by(supportable_id: supporting_document_file.supportable_id,
supportable_type: supporting_document_file.supportable_type)
all_files_are_upload = false unless file
end
if all_files_are_upload
end
if all_files_are_upload && (supporting_document_file.supportable_type == 'User')
NotificationCenter.call type: 'notify_admin_user_supporting_document_files_created',
receiver: User.admins_and_managers,
attached_object: user
@ -32,13 +41,16 @@ class SupportingDocumentFileService
def self.update(supporting_document_file, params)
updated = supporting_document_file.update(params)
if updated
user = supporting_document_file.user
all_files_are_upload = true
if supporting_document_file.supportable_type == 'User'
user = supporting_document_file.supportable
user.group.supporting_document_types.each do |type|
file = type.supporting_document_files.find_by(user_id: supporting_document_file.user_id)
file = type.supporting_document_files.find_by(supportable_id: supporting_document_file.supportable_id,
supportable_type: supporting_document_file.supportable_type)
all_files_are_upload = false unless file
end
if all_files_are_upload
end
if all_files_are_upload && (supporting_document_file.supportable_type == 'User')
NotificationCenter.call type: 'notify_admin_user_supporting_document_files_updated',
receiver: User.admins_and_managers,
attached_object: supporting_document_file

View File

@ -4,19 +4,22 @@
class SupportingDocumentRefusalService
def self.list(filters = {})
refusals = []
refusals = SupportingDocumentRefusal.where(user_id: filters[:user_id]) if filters[:user_id].present?
if filters[:supportable_id].present?
refusals = SupportingDocumentRefusal.where(supportable_id: filters[:supportable_id],
supportable_type: filters[:supportable_type])
end
refusals
end
def self.create(supporting_document_refusal)
saved = supporting_document_refusal.save
if saved
if saved && supporting_document_refusal.supportable_type == 'User'
NotificationCenter.call type: 'notify_admin_user_supporting_document_refusal',
receiver: User.admins_and_managers,
attached_object: supporting_document_refusal
NotificationCenter.call type: 'notify_user_supporting_document_refusal',
receiver: supporting_document_refusal.user,
receiver: supporting_document_refusal.supportable,
attached_object: supporting_document_refusal
end
saved

View File

@ -9,7 +9,7 @@ class SupportingDocumentTypeService
group.supporting_document_types.includes(:groups)
else
SupportingDocumentType.all
SupportingDocumentType.where(document_type: filters[:document_type] || 'User')
end
end
end

View File

@ -1,3 +1,12 @@
# frozen_string_literal: true
json.extract! child, :id, :first_name, :last_name, :email, :birthday, :phone, :user_id
json.supporting_document_files_attributes child.supporting_document_files do |f|
json.id f.id
json.supportable_id f.supportable_id
json.supportable_type f.supportable_type
json.supporting_document_type_id f.supporting_document_type_id
json.attachment f.attachment.file.filename
json.attachment_name f.attachment_identifier
json.attachment_url f.attachment_url
end

View File

@ -2,4 +2,4 @@
json.title notification.notification_type
json.description t('.supporting_document_files_uploaded',
NAME: notification.attached_object&.user&.profile&.full_name || t('api.notifications.deleted_user'))
NAME: notification.attached_object&.supportable&.profile&.full_name || t('api.notifications.deleted_user'))

View File

@ -2,4 +2,4 @@
json.title notification.notification_type
json.description t('.refusal',
NAME: notification.attached_object&.user&.profile&.full_name || t('api.notifications.deleted_user'))
NAME: notification.attached_object&.supportable&.profile&.full_name || t('api.notifications.deleted_user'))

View File

@ -1,4 +1,4 @@
# frozen_string_literal: true
json.extract! supporting_document_file, :id, :user_id, :supporting_document_type_id
json.extract! supporting_document_file, :id, :supportable_id, :supportable_type, :supporting_document_type_id
json.attachment supporting_document_file.attachment.file.filename

View File

@ -1,3 +1,3 @@
# frozen_string_literal: true
json.extract! supporting_document_refusal, :id, :user_id, :operator_id, :supporting_document_type_ids, :message
json.extract! supporting_document_refusal, :id, :supportable_id, :supportable_type, :operator_id, :supporting_document_type_ids, :message

View File

@ -1,3 +1,3 @@
# frozen_string_literal: true
json.extract! supporting_document_type, :id, :name, :group_ids
json.extract! supporting_document_type, :id, :name, :group_ids, :document_type

View File

@ -2,7 +2,7 @@
<p>
<%= t('.body.user_update_supporting_document_file',
NAME: @attached_object&.user&.profile&.full_name || t('api.notifications.deleted_user')) %>
NAME: @attached_object&.supportable&.profile&.full_name || t('api.notifications.deleted_user')) %>
</p>
<ul>
<li><%= @attached_object.supporting_document_type.name %></li>

View File

@ -2,7 +2,7 @@
<p>
<%= t('.body.user_supporting_document_files_refusal',
NAME: @attached_object&.user&.profile&.full_name || t('api.notifications.deleted_user'),
NAME: @attached_object&.supportable&.profile&.full_name || t('api.notifications.deleted_user'),
OPERATOR: @attached_object&.operator&.profile&.full_name || t('api.notifications.deleted_user')) %>
</p>
<ul>

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
# add document_type to supporting_document_type
class AddDucumentTypeToSupportingDocumentType < ActiveRecord::Migration[7.0]
def change
add_column :supporting_document_types, :document_type, :string, default: 'User'
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
# change user_id to supportable from supporting_document_file
class ChangeUserIdToSupportableFromSupportingDocumentFile < ActiveRecord::Migration[7.0]
def change
rename_column :supporting_document_files, :user_id, :supportable_id
add_column :supporting_document_files, :supportable_type, :string, default: 'User'
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
# change user_id to supportable from supporting_document_refusal
class ChangeUserIdToSupportableFromSupportingDocumentRefusal < ActiveRecord::Migration[7.0]
def change
rename_column :supporting_document_refusals, :user_id, :supportable_id
add_column :supporting_document_refusals, :supportable_type, :string, default: 'User'
end
end

View File

@ -3746,10 +3746,11 @@ ALTER SEQUENCE public.subscriptions_id_seq OWNED BY public.subscriptions.id;
CREATE TABLE public.supporting_document_files (
id bigint NOT NULL,
supporting_document_type_id bigint,
user_id bigint,
supportable_id bigint,
attachment character varying,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
updated_at timestamp without time zone NOT NULL,
supportable_type character varying DEFAULT 'User'::character varying
);
@ -3778,11 +3779,12 @@ ALTER SEQUENCE public.supporting_document_files_id_seq OWNED BY public.supportin
CREATE TABLE public.supporting_document_refusals (
id bigint NOT NULL,
user_id bigint,
supportable_id bigint,
operator_id integer,
message text,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
updated_at timestamp without time zone NOT NULL,
supportable_type character varying DEFAULT 'User'::character varying
);
@ -3823,7 +3825,8 @@ CREATE TABLE public.supporting_document_types (
id bigint NOT NULL,
name character varying,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
updated_at timestamp without time zone NOT NULL,
document_type character varying DEFAULT 'User'::character varying
);
@ -7320,6 +7323,13 @@ CREATE INDEX index_subscriptions_on_plan_id ON public.subscriptions USING btree
CREATE INDEX index_subscriptions_on_statistic_profile_id ON public.subscriptions USING btree (statistic_profile_id);
--
-- Name: index_supporting_document_files_on_supportable_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_supporting_document_files_on_supportable_id ON public.supporting_document_files USING btree (supportable_id);
--
-- Name: index_supporting_document_files_on_supporting_document_type_id; Type: INDEX; Schema: public; Owner: -
--
@ -7328,17 +7338,10 @@ CREATE INDEX index_supporting_document_files_on_supporting_document_type_id ON p
--
-- Name: index_supporting_document_files_on_user_id; Type: INDEX; Schema: public; Owner: -
-- Name: index_supporting_document_refusals_on_supportable_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_supporting_document_files_on_user_id ON public.supporting_document_files USING btree (user_id);
--
-- Name: index_supporting_document_refusals_on_user_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_supporting_document_refusals_on_user_id ON public.supporting_document_refusals USING btree (user_id);
CREATE INDEX index_supporting_document_refusals_on_supportable_id ON public.supporting_document_refusals USING btree (supportable_id);
--
@ -8144,7 +8147,7 @@ ALTER TABLE ONLY public.orders
--
ALTER TABLE ONLY public.supporting_document_refusals
ADD CONSTRAINT fk_rails_91d424352e FOREIGN KEY (user_id) REFERENCES public.users(id);
ADD CONSTRAINT fk_rails_91d424352e FOREIGN KEY (supportable_id) REFERENCES public.users(id);
--
@ -8929,6 +8932,9 @@ INSERT INTO "schema_migrations" (version) VALUES
('20230509161557'),
('20230510141305'),
('20230511080650'),
('20230511081018');
('20230511081018'),
('20230524080448'),
('20230524083558'),
('20230524110215');