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:
parent
b473a7cd35
commit
ecc4fde47f
@ -50,6 +50,9 @@ class API::ChildrenController < API::APIController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def child_params
|
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
|
||||||
end
|
end
|
||||||
|
@ -48,6 +48,6 @@ class API::SupportingDocumentFilesController < API::APIController
|
|||||||
|
|
||||||
# Never trust parameters from the scary internet, only allow the white list through.
|
# Never trust parameters from the scary internet, only allow the white list through.
|
||||||
def supporting_document_file_params
|
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
|
||||||
end
|
end
|
||||||
|
@ -27,6 +27,7 @@ class API::SupportingDocumentRefusalsController < API::APIController
|
|||||||
|
|
||||||
# Never trust parameters from the scary internet, only allow the white list through.
|
# Never trust parameters from the scary internet, only allow the white list through.
|
||||||
def supporting_document_refusal_params
|
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
|
||||||
end
|
end
|
||||||
|
@ -45,6 +45,6 @@ class API::SupportingDocumentTypesController < API::APIController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def supporting_document_type_params
|
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
|
||||||
end
|
end
|
||||||
|
@ -15,12 +15,22 @@ export default class ChildAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async create (child: Child): Promise<Child> {
|
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;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async update (child: Child): Promise<Child> {
|
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;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,33 +1,34 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Child } from '../../models/child';
|
import { Child } from '../../models/child';
|
||||||
import { TDateISODate } from '../../typings/date-iso';
|
|
||||||
import { FormInput } from '../form/form-input';
|
import { FormInput } from '../form/form-input';
|
||||||
import { FabButton } from '../base/fab-button';
|
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 {
|
interface ChildFormProps {
|
||||||
child: Child;
|
child: Child;
|
||||||
onChange: (field: string, value: string | TDateISODate) => void;
|
|
||||||
onSubmit: (data: Child) => void;
|
onSubmit: (data: Child) => void;
|
||||||
|
supportingDocumentsTypes: Array<SupportingDocumentType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A form for creating or editing a child.
|
* 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 { t } = useTranslation('public');
|
||||||
|
|
||||||
const { register, formState, handleSubmit } = useForm<Child>({
|
const { register, formState, handleSubmit, setValue, control } = useForm<Child>({
|
||||||
defaultValues: child
|
defaultValues: child
|
||||||
});
|
});
|
||||||
|
const output = useWatch<Child>({ control }); // eslint-disable-line
|
||||||
|
|
||||||
/**
|
const getSupportingDocumentsTypeName = (id: number): string => {
|
||||||
* Handle the change of a child form field
|
const supportingDocumentType = supportingDocumentsTypes.find((supportingDocumentType) => supportingDocumentType.id === id);
|
||||||
*/
|
return supportingDocumentType ? supportingDocumentType.name : '';
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
|
||||||
onChange(event.target.id, event.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -41,14 +42,12 @@ export const ChildForm: React.FC<ChildFormProps> = ({ child, onChange, onSubmit
|
|||||||
rules={{ required: true }}
|
rules={{ required: true }}
|
||||||
formState={formState}
|
formState={formState}
|
||||||
label={t('app.public.child_form.first_name')}
|
label={t('app.public.child_form.first_name')}
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
/>
|
||||||
<FormInput id="last_name"
|
<FormInput id="last_name"
|
||||||
register={register}
|
register={register}
|
||||||
rules={{ required: true }}
|
rules={{ required: true }}
|
||||||
formState={formState}
|
formState={formState}
|
||||||
label={t('app.public.child_form.last_name')}
|
label={t('app.public.child_form.last_name')}
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
/>
|
||||||
<FormInput id="birthday"
|
<FormInput id="birthday"
|
||||||
register={register}
|
register={register}
|
||||||
@ -57,22 +56,31 @@ export const ChildForm: React.FC<ChildFormProps> = ({ child, onChange, onSubmit
|
|||||||
label={t('app.public.child_form.birthday')}
|
label={t('app.public.child_form.birthday')}
|
||||||
type="date"
|
type="date"
|
||||||
max={moment().subtract(18, 'year').format('YYYY-MM-DD')}
|
max={moment().subtract(18, 'year').format('YYYY-MM-DD')}
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
/>
|
||||||
<FormInput id="phone"
|
<FormInput id="phone"
|
||||||
register={register}
|
register={register}
|
||||||
formState={formState}
|
formState={formState}
|
||||||
label={t('app.public.child_form.phone')}
|
label={t('app.public.child_form.phone')}
|
||||||
onChange={handleChange}
|
|
||||||
type="tel"
|
type="tel"
|
||||||
/>
|
/>
|
||||||
<FormInput id="email"
|
<FormInput id="email"
|
||||||
register={register}
|
register={register}
|
||||||
rules={{ required: true }}
|
|
||||||
formState={formState}
|
formState={formState}
|
||||||
label={t('app.public.child_form.email')}
|
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">
|
<div className="actions">
|
||||||
<FabButton type="button" onClick={handleSubmit(onSubmit)}>
|
<FabButton type="button" onClick={handleSubmit(onSubmit)}>
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FabModal, ModalSize } from '../base/fab-modal';
|
import { FabModal, ModalSize } from '../base/fab-modal';
|
||||||
import { Child } from '../../models/child';
|
import { Child } from '../../models/child';
|
||||||
import { TDateISODate } from '../../typings/date-iso';
|
|
||||||
import ChildAPI from '../../api/child';
|
import ChildAPI from '../../api/child';
|
||||||
import { ChildForm } from './child-form';
|
import { ChildForm } from './child-form';
|
||||||
|
import { SupportingDocumentType } from '../../models/supporting-document-type';
|
||||||
|
|
||||||
interface ChildModalProps {
|
interface ChildModalProps {
|
||||||
child?: Child;
|
child?: Child;
|
||||||
@ -13,23 +12,19 @@ interface ChildModalProps {
|
|||||||
toggleModal: () => void;
|
toggleModal: () => void;
|
||||||
onSuccess: (child: Child) => void;
|
onSuccess: (child: Child) => void;
|
||||||
onError: (error: string) => void;
|
onError: (error: string) => void;
|
||||||
|
supportingDocumentsTypes: Array<SupportingDocumentType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A modal for creating or editing a child.
|
* 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 { t } = useTranslation('public');
|
||||||
const [data, setData] = useState<Child>(child);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setData(child);
|
|
||||||
}, [child]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the child to the API
|
* Save the child to the API
|
||||||
*/
|
*/
|
||||||
const handleSaveChild = async (): Promise<void> => {
|
const handleSaveChild = async (data: Child): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
if (child?.id) {
|
if (child?.id) {
|
||||||
await ChildAPI.update(data);
|
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 (
|
return (
|
||||||
<FabModal title={t(`app.public.child_modal.${child?.id ? 'edit' : 'new'}_child`)}
|
<FabModal title={t(`app.public.child_modal.${child?.id ? 'edit' : 'new'}_child`)}
|
||||||
width={ModalSize.large}
|
width={ModalSize.large}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
toggleModal={toggleModal}
|
toggleModal={toggleModal}
|
||||||
closeButton={true}
|
closeButton={true}
|
||||||
confirmButton={false}
|
confirmButton={false} >
|
||||||
onConfirm={handleSaveChild} >
|
<ChildForm child={child} onSubmit={handleSaveChild} supportingDocumentsTypes={supportingDocumentsTypes}/>
|
||||||
<ChildForm child={child} onChange={handleChildChanged} onSubmit={handleSaveChild} />
|
|
||||||
</FabModal>
|
</FabModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -9,6 +9,8 @@ import { IApplication } from '../../models/application';
|
|||||||
import { ChildModal } from './child-modal';
|
import { ChildModal } from './child-modal';
|
||||||
import { ChildItem } from './child-item';
|
import { ChildItem } from './child-item';
|
||||||
import { FabButton } from '../base/fab-button';
|
import { FabButton } from '../base/fab-button';
|
||||||
|
import { SupportingDocumentType } from '../../models/supporting-document-type';
|
||||||
|
import SupportingDocumentTypeAPI from '../../api/supporting-document-type';
|
||||||
|
|
||||||
declare const Application: IApplication;
|
declare const Application: IApplication;
|
||||||
|
|
||||||
@ -27,9 +29,13 @@ export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser, onError
|
|||||||
const [children, setChildren] = useState<Array<Child>>([]);
|
const [children, setChildren] = useState<Array<Child>>([]);
|
||||||
const [isOpenChildModal, setIsOpenChildModal] = useState<boolean>(false);
|
const [isOpenChildModal, setIsOpenChildModal] = useState<boolean>(false);
|
||||||
const [child, setChild] = useState<Child>();
|
const [child, setChild] = useState<Child>();
|
||||||
|
const [supportingDocumentsTypes, setSupportingDocumentsTypes] = useState<Array<SupportingDocumentType>>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ChildAPI.index({ user_id: currentUser.id }).then(setChildren);
|
ChildAPI.index({ user_id: currentUser.id }).then(setChildren);
|
||||||
|
SupportingDocumentTypeAPI.index({ document_type: 'Child' }).then(tData => {
|
||||||
|
setSupportingDocumentsTypes(tData);
|
||||||
|
});
|
||||||
}, [currentUser]);
|
}, [currentUser]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,7 +43,12 @@ export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser, onError
|
|||||||
*/
|
*/
|
||||||
const addChild = () => {
|
const addChild = () => {
|
||||||
setIsOpenChildModal(true);
|
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) => {
|
const editChild = (child: Child) => {
|
||||||
setIsOpenChildModal(true);
|
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} />
|
<ChildItem key={child.id} child={child} onEdit={editChild} onDelete={deleteChild} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -19,12 +19,13 @@ type FormFileUploadProps<TFieldValues> = FormComponent<TFieldValues> & AbstractF
|
|||||||
accept?: string,
|
accept?: string,
|
||||||
onFileChange?: (value: FileType) => void,
|
onFileChange?: (value: FileType) => void,
|
||||||
onFileRemove?: () => void,
|
onFileRemove?: () => void,
|
||||||
|
showRemoveButton?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component allows to upload file, in forms managed by react-hook-form.
|
* 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 { t } = useTranslation('shared');
|
||||||
|
|
||||||
const [file, setFile] = useState<FileType>(defaultFile);
|
const [file, setFile] = useState<FileType>(defaultFile);
|
||||||
@ -100,7 +101,7 @@ export const FormFileUpload = <TFieldValues extends FieldValues>({ id, label, re
|
|||||||
id={`${id}[attachment_files]`}
|
id={`${id}[attachment_files]`}
|
||||||
onChange={onFileSelected}
|
onChange={onFileSelected}
|
||||||
placeholder={placeholder()}/>
|
placeholder={placeholder()}/>
|
||||||
{hasFile() &&
|
{showRemoveButton && hasFile() &&
|
||||||
<FabButton onClick={onRemoveFile} icon={<Trash size={20} weight="fill" />} className="is-main" />
|
<FabButton onClick={onRemoveFile} icon={<Trash size={20} weight="fill" />} className="is-main" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,7 +49,7 @@ export const SupportingDocumentsFiles: React.FC<SupportingDocumentsFilesProps> =
|
|||||||
SupportingDocumentTypeAPI.index({ group_id: currentUser.group_id }).then(tData => {
|
SupportingDocumentTypeAPI.index({ group_id: currentUser.group_id }).then(tData => {
|
||||||
setSupportingDocumentsTypes(tData);
|
setSupportingDocumentsTypes(tData);
|
||||||
});
|
});
|
||||||
SupportingDocumentFileAPI.index({ user_id: currentUser.id }).then(fData => {
|
SupportingDocumentFileAPI.index({ supportable_id: currentUser.id, supportable_type: 'User' }).then(fData => {
|
||||||
setSupportingDocumentsFiles(fData);
|
setSupportingDocumentsFiles(fData);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
@ -106,7 +106,8 @@ export const SupportingDocumentsFiles: React.FC<SupportingDocumentsFilesProps> =
|
|||||||
for (const proofOfIdentityTypeId of Object.keys(files)) {
|
for (const proofOfIdentityTypeId of Object.keys(files)) {
|
||||||
const formData = new FormData();
|
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[supporting_document_type_id]', proofOfIdentityTypeId);
|
||||||
formData.append('supporting_document_file[attachment]', files[proofOfIdentityTypeId]);
|
formData.append('supporting_document_file[attachment]', files[proofOfIdentityTypeId]);
|
||||||
const proofOfIdentityFile = getSupportingDocumentsFileByType(parseInt(proofOfIdentityTypeId, 10));
|
const proofOfIdentityFile = getSupportingDocumentsFileByType(parseInt(proofOfIdentityTypeId, 10));
|
||||||
@ -117,7 +118,7 @@ export const SupportingDocumentsFiles: React.FC<SupportingDocumentsFilesProps> =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Object.keys(files).length > 0) {
|
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);
|
setSupportingDocumentsFiles(fData);
|
||||||
setFiles({});
|
setFiles({});
|
||||||
onSuccess(t('app.logged.dashboard.supporting_documents_files.file_successfully_uploaded'));
|
onSuccess(t('app.logged.dashboard.supporting_documents_files.file_successfully_uploaded'));
|
||||||
|
@ -27,7 +27,8 @@ export const SupportingDocumentsRefusalModal: React.FC<SupportingDocumentsRefusa
|
|||||||
const [data, setData] = useState<SupportingDocumentRefusal>({
|
const [data, setData] = useState<SupportingDocumentRefusal>({
|
||||||
id: null,
|
id: null,
|
||||||
operator_id: operator.id,
|
operator_id: operator.id,
|
||||||
user_id: member.id,
|
supportable_id: member.id,
|
||||||
|
supportable_type: 'User',
|
||||||
supporting_document_type_ids: [],
|
supporting_document_type_ids: [],
|
||||||
message: ''
|
message: ''
|
||||||
});
|
});
|
||||||
|
@ -63,13 +63,15 @@ export const SupportingDocumentsTypeForm: React.FC<SupportingDocumentsTypeFormPr
|
|||||||
{t('app.admin.settings.account.supporting_documents_type_form.type_form_info')}
|
{t('app.admin.settings.account.supporting_documents_type_form.type_form_info')}
|
||||||
</div>
|
</div>
|
||||||
<form name="supportingDocumentTypeForm">
|
<form name="supportingDocumentTypeForm">
|
||||||
<div className="field">
|
{supportingDocumentType?.document_type === 'User' &&
|
||||||
<Select defaultValue={groupsValues()}
|
<div className="field">
|
||||||
placeholder={t('app.admin.settings.account.supporting_documents_type_form.select_group')}
|
<Select defaultValue={groupsValues()}
|
||||||
onChange={handleGroupsChange}
|
placeholder={t('app.admin.settings.account.supporting_documents_type_form.select_group')}
|
||||||
options={buildOptions()}
|
onChange={handleGroupsChange}
|
||||||
isMulti />
|
options={buildOptions()}
|
||||||
</div>
|
isMulti />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<FabInput id="supporting_document_type_name"
|
<FabInput id="supporting_document_type_name"
|
||||||
icon={<i className="fa fa-edit" />}
|
icon={<i className="fa fa-edit" />}
|
||||||
|
@ -14,18 +14,19 @@ interface SupportingDocumentsTypeModalProps {
|
|||||||
onError: (message: string) => void,
|
onError: (message: string) => void,
|
||||||
groups: Array<Group>,
|
groups: Array<Group>,
|
||||||
proofOfIdentityType?: SupportingDocumentType,
|
proofOfIdentityType?: SupportingDocumentType,
|
||||||
|
documentType: 'User' | 'Child',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal dialog to create/edit a supporting documents type
|
* 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 { 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(() => {
|
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]);
|
}, [proofOfIdentityType]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,7 +64,7 @@ export const SupportingDocumentsTypeModal: React.FC<SupportingDocumentsTypeModal
|
|||||||
* Check if the form is valid (not empty)
|
* Check if the form is valid (not empty)
|
||||||
*/
|
*/
|
||||||
const isPreventedSaveType = (): boolean => {
|
const isPreventedSaveType = (): boolean => {
|
||||||
return !data.name || data.group_ids.length === 0;
|
return !data.name || (documentType === 'User' && data.group_ids.length === 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -21,12 +21,13 @@ declare const Application: IApplication;
|
|||||||
interface SupportingDocumentsTypesListProps {
|
interface SupportingDocumentsTypesListProps {
|
||||||
onSuccess: (message: string) => void,
|
onSuccess: (message: string) => void,
|
||||||
onError: (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.)
|
* 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');
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
// list of displayed supporting documents type
|
// list of displayed supporting documents type
|
||||||
@ -48,7 +49,7 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
GroupAPI.index({ disabled: false }).then(data => {
|
GroupAPI.index({ disabled: false }).then(data => {
|
||||||
setGroups(data);
|
setGroups(data);
|
||||||
SupportingDocumentTypeAPI.index().then(pData => {
|
SupportingDocumentTypeAPI.index({ document_type: documentType }).then(pData => {
|
||||||
setSupportingDocumentsTypes(pData);
|
setSupportingDocumentsTypes(pData);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -91,7 +92,7 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
|
|||||||
*/
|
*/
|
||||||
const onSaveTypeSuccess = (message: string): void => {
|
const onSaveTypeSuccess = (message: string): void => {
|
||||||
setModalIsOpen(false);
|
setModalIsOpen(false);
|
||||||
SupportingDocumentTypeAPI.index().then(pData => {
|
SupportingDocumentTypeAPI.index({ document_type: documentType }).then(pData => {
|
||||||
setSupportingDocumentsTypes(orderTypes(pData, supportingDocumentsTypeOrder));
|
setSupportingDocumentsTypes(orderTypes(pData, supportingDocumentsTypeOrder));
|
||||||
onSuccess(message);
|
onSuccess(message);
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@ -121,7 +122,7 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
|
|||||||
*/
|
*/
|
||||||
const onDestroySuccess = (message: string): void => {
|
const onDestroySuccess = (message: string): void => {
|
||||||
setDestroyModalIsOpen(false);
|
setDestroyModalIsOpen(false);
|
||||||
SupportingDocumentTypeAPI.index().then(pData => {
|
SupportingDocumentTypeAPI.index({ document_type: documentType }).then(pData => {
|
||||||
setSupportingDocumentsTypes(pData);
|
setSupportingDocumentsTypes(pData);
|
||||||
setSupportingDocumentsTypes(orderTypes(pData, supportingDocumentsTypeOrder));
|
setSupportingDocumentsTypes(orderTypes(pData, supportingDocumentsTypeOrder));
|
||||||
onSuccess(message);
|
onSuccess(message);
|
||||||
@ -190,83 +191,150 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
|
|||||||
window.location.href = '/#!/admin/members?tabs=1';
|
window.location.href = '/#!/admin/members?tabs=1';
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
if (documentType === 'User') {
|
||||||
<FabPanel className="supporting-documents-types-list" header={<div>
|
return (
|
||||||
<span>{t('app.admin.settings.account.supporting_documents_types_list.add_supporting_documents_types')}</span>
|
<FabPanel className="supporting-documents-types-list" header={<div>
|
||||||
</div>}>
|
<span>{t('app.admin.settings.account.supporting_documents_types_list.add_supporting_documents_types')}</span>
|
||||||
<div className="types-list">
|
</div>}>
|
||||||
<div className="groups">
|
<div className="types-list">
|
||||||
<p>{t('app.admin.settings.account.supporting_documents_types_list.supporting_documents_type_info')}</p>
|
<div className="groups">
|
||||||
<FabAlert level="warning">
|
<p>{t('app.admin.settings.account.supporting_documents_types_list.supporting_documents_type_info')}</p>
|
||||||
<HtmlTranslate trKey="app.admin.settings.account.supporting_documents_types_list.no_groups_info" />
|
<FabAlert level="warning">
|
||||||
<FabButton onClick={addGroup}>{t('app.admin.settings.account.supporting_documents_types_list.create_groups')}</FabButton>
|
<HtmlTranslate trKey="app.admin.settings.account.supporting_documents_types_list.no_groups_info" />
|
||||||
</FabAlert>
|
<FabButton onClick={addGroup}>{t('app.admin.settings.account.supporting_documents_types_list.create_groups')}</FabButton>
|
||||||
|
</FabAlert>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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="group-name">
|
||||||
|
<a onClick={setTypeOrder('group_name')}>
|
||||||
|
{t('app.admin.settings.account.supporting_documents_types_list.group_name')}
|
||||||
|
<i className={orderClassName('group_name')} />
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<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>{getGroupsNames(poit.group_ids)}</td>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
|
||||||
<div className="title">
|
<SupportingDocumentsTypeModal isOpen={modalIsOpen}
|
||||||
<h3>{t('app.admin.settings.account.supporting_documents_types_list.supporting_documents_type_title')}</h3>
|
groups={groups}
|
||||||
<FabButton onClick={addType}>{t('app.admin.settings.account.supporting_documents_types_list.add_type')}</FabButton>
|
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>
|
||||||
|
|
||||||
<SupportingDocumentsTypeModal isOpen={modalIsOpen}
|
|
||||||
groups={groups}
|
|
||||||
proofOfIdentityType={supportingDocumentsType}
|
|
||||||
toggleModal={toggleCreateAndEditModal}
|
|
||||||
onSuccess={onSaveTypeSuccess}
|
|
||||||
onError={onError} />
|
|
||||||
<DeleteSupportingDocumentsTypeModal isOpen={destroyModalIsOpen}
|
|
||||||
proofOfIdentityTypeId={supportingDocumentsTypeId}
|
|
||||||
toggleModal={toggleDestroyModal}
|
|
||||||
onSuccess={onDestroySuccess}
|
|
||||||
onError={onError}/>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="group-name">
|
|
||||||
<a onClick={setTypeOrder('group_name')}>
|
|
||||||
{t('app.admin.settings.account.supporting_documents_types_list.group_name')}
|
|
||||||
<i className={orderClassName('group_name')} />
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
<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>{getGroupsNames(poit.group_ids)}</td>
|
|
||||||
<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>
|
||||||
</FabPanel>
|
);
|
||||||
);
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const SupportingDocumentsTypesListWrapper: React.FC<SupportingDocumentsTypesListProps> = (props) => {
|
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']));
|
||||||
|
@ -39,7 +39,7 @@ const SupportingDocumentsValidation: React.FC<SupportingDocumentsValidationProps
|
|||||||
SupportingDocumentTypeAPI.index({ group_id: member.group_id }).then(tData => {
|
SupportingDocumentTypeAPI.index({ group_id: member.group_id }).then(tData => {
|
||||||
setDocumentsTypes(tData);
|
setDocumentsTypes(tData);
|
||||||
});
|
});
|
||||||
SupportingDocumentFileAPI.index({ user_id: member.id }).then(fData => {
|
SupportingDocumentFileAPI.index({ supportable_id: member.id, supportable_type: 'User' }).then(fData => {
|
||||||
setDocumentsFiles(fData);
|
setDocumentsFiles(fData);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -35,6 +35,9 @@ export default class ApiLib {
|
|||||||
if (file?.is_main) {
|
if (file?.is_main) {
|
||||||
data.set(`${name}[${attr}][${i}][is_main]`, file.is_main.toString());
|
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 {
|
} else {
|
||||||
if (object[attr]?.attachment_files && object[attr]?.attachment_files[0]) {
|
if (object[attr]?.attachment_files && object[attr]?.attachment_files[0]) {
|
||||||
|
@ -12,5 +12,15 @@ export interface Child {
|
|||||||
email?: string,
|
email?: string,
|
||||||
phone?: string,
|
phone?: string,
|
||||||
birthday: TDateISODate,
|
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
|
||||||
|
}>,
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { ApiFilter } from './api';
|
import { ApiFilter } from './api';
|
||||||
|
|
||||||
export interface SupportingDocumentFileIndexFilter extends ApiFilter {
|
export interface SupportingDocumentFileIndexFilter extends ApiFilter {
|
||||||
user_id: number,
|
supportable_id: number,
|
||||||
|
supportable_type?: 'User' | 'Child',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SupportingDocumentFile {
|
export interface SupportingDocumentFile {
|
||||||
id?: number,
|
id?: number,
|
||||||
attachment?: string,
|
attachment?: string,
|
||||||
user_id?: number,
|
supportable_id?: number,
|
||||||
|
supportable_type?: 'User' | 'Child',
|
||||||
supporting_document_type_id: number,
|
supporting_document_type_id: number,
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { ApiFilter } from './api';
|
import { ApiFilter } from './api';
|
||||||
|
|
||||||
export interface SupportingDocumentRefusalIndexFilter extends ApiFilter {
|
export interface SupportingDocumentRefusalIndexFilter extends ApiFilter {
|
||||||
user_id: number,
|
supportable_id: number,
|
||||||
|
supportable_type: 'User' | 'Child',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SupportingDocumentRefusal {
|
export interface SupportingDocumentRefusal {
|
||||||
id: number,
|
id: number,
|
||||||
message: string,
|
message: string,
|
||||||
user_id: number,
|
supportable_id: number,
|
||||||
|
supportable_type: 'User' | 'Child',
|
||||||
operator_id: number,
|
operator_id: number,
|
||||||
supporting_document_type_ids: Array<number>,
|
supporting_document_type_ids: Array<number>,
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,12 @@ import { ApiFilter } from './api';
|
|||||||
|
|
||||||
export interface SupportingDocumentTypeIndexfilter extends ApiFilter {
|
export interface SupportingDocumentTypeIndexfilter extends ApiFilter {
|
||||||
group_id?: number,
|
group_id?: number,
|
||||||
|
document_type?: 'User' | 'Child'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SupportingDocumentType {
|
export interface SupportingDocumentType {
|
||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
group_ids: Array<number>
|
group_ids: Array<number>,
|
||||||
|
document_type: 'User' | 'Child'
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,12 @@
|
|||||||
on-error="onError">
|
on-error="onError">
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="row">
|
||||||
<h3 class="m-l" translate>{{ 'app.admin.settings.captcha' }}</h3>
|
<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>
|
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.captcha_info_html' | translate"></p>
|
||||||
@ -167,4 +173,4 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<supporting-documents-types-list on-success="onSuccess" on-error="onError"/>
|
<supporting-documents-types-list on-success="onSuccess" on-error="onError" document-type="'User'" />
|
||||||
|
@ -4,9 +4,13 @@
|
|||||||
class Child < ApplicationRecord
|
class Child < ApplicationRecord
|
||||||
belongs_to :user
|
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 :first_name, presence: true
|
||||||
validates :last_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
|
validate :validate_age
|
||||||
|
|
||||||
# birthday should less than 18 years ago
|
# birthday should less than 18 years ago
|
||||||
|
@ -6,7 +6,7 @@ class SupportingDocumentFile < ApplicationRecord
|
|||||||
mount_uploader :attachment, SupportingDocumentFileUploader
|
mount_uploader :attachment, SupportingDocumentFileUploader
|
||||||
|
|
||||||
belongs_to :supporting_document_type
|
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 }
|
validates :attachment, file_size: { maximum: ENV.fetch('MAX_SUPPORTING_DOCUMENT_FILE_SIZE', 5.megabytes).to_i }
|
||||||
end
|
end
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# An admin can mark an uploaded document as refused, this will notify the member
|
# An admin can mark an uploaded document as refused, this will notify the member
|
||||||
class SupportingDocumentRefusal < ApplicationRecord
|
class SupportingDocumentRefusal < ApplicationRecord
|
||||||
belongs_to :user
|
belongs_to :supportable, polymorphic: true
|
||||||
belongs_to :operator, class_name: 'User', inverse_of: :supporting_document_refusals
|
belongs_to :operator, class_name: 'User', inverse_of: :supporting_document_refusals
|
||||||
has_many :supporting_document_refusals_types, dependent: :destroy
|
has_many :supporting_document_refusals_types, dependent: :destroy
|
||||||
has_many :supporting_document_types, through: :supporting_document_refusals_types
|
has_many :supporting_document_types, through: :supporting_document_refusals_types
|
||||||
|
@ -8,4 +8,6 @@ class SupportingDocumentType < ApplicationRecord
|
|||||||
|
|
||||||
has_many :supporting_document_refusals_types, dependent: :destroy
|
has_many :supporting_document_refusals_types, dependent: :destroy
|
||||||
has_many :supporting_document_refusals, through: :supporting_document_refusals_types
|
has_many :supporting_document_refusals, through: :supporting_document_refusals_types
|
||||||
|
|
||||||
|
validates :document_type, presence: true, inclusion: { in: %w[User Child] }
|
||||||
end
|
end
|
||||||
|
@ -47,8 +47,8 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
has_many :accounting_periods, foreign_key: 'closed_by', dependent: :nullify, inverse_of: :user
|
has_many :accounting_periods, foreign_key: 'closed_by', dependent: :nullify, inverse_of: :user
|
||||||
|
|
||||||
has_many :supporting_document_files, dependent: :destroy
|
has_many :supporting_document_files, as: :supportable, dependent: :destroy
|
||||||
has_many :supporting_document_refusals, dependent: :destroy
|
has_many :supporting_document_refusals, as: :supportable, dependent: :destroy
|
||||||
|
|
||||||
has_many :notifications, as: :receiver, dependent: :destroy
|
has_many :notifications, as: :receiver, dependent: :destroy
|
||||||
has_many :notification_preferences, dependent: :destroy
|
has_many :notification_preferences, dependent: :destroy
|
||||||
|
@ -6,15 +6,11 @@ class SupportingDocumentFilePolicy < ApplicationPolicy
|
|||||||
user.privileged?
|
user.privileged?
|
||||||
end
|
end
|
||||||
|
|
||||||
def create?
|
%w[create update download].each do |action|
|
||||||
user.privileged? or record.user_id == user.id
|
define_method "#{action}?" do
|
||||||
end
|
user.privileged? ||
|
||||||
|
(record.supportable_type == 'User' && record.supportable_id.to_i == user.id) ||
|
||||||
def update?
|
(record.supportable_type == 'Child' && user.children.exists?(id: record.supportable_id.to_i))
|
||||||
user.privileged? or record.user_id == user.id
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def download?
|
|
||||||
user.privileged? or record.user_id == user.id
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -4,23 +4,32 @@
|
|||||||
class SupportingDocumentFileService
|
class SupportingDocumentFileService
|
||||||
def self.list(operator, filters = {})
|
def self.list(operator, filters = {})
|
||||||
files = []
|
files = []
|
||||||
if filters[:user_id].present? && (operator.privileged? || filters[:user_id].to_i == operator.id)
|
if filters[:supportable_id].present? && can_list?(operator, filters[:supportable_id], filters[:supportable_type])
|
||||||
files = SupportingDocumentFile.where(user_id: filters[:user_id])
|
files = SupportingDocumentFile.where(supportable_id: filters[:supportable_id], supportable_type: filters[:supportable_type])
|
||||||
end
|
end
|
||||||
files
|
files
|
||||||
end
|
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)
|
def self.create(supporting_document_file)
|
||||||
saved = supporting_document_file.save
|
saved = supporting_document_file.save
|
||||||
|
|
||||||
if saved
|
if saved
|
||||||
user = User.find(supporting_document_file.user_id)
|
|
||||||
all_files_are_upload = true
|
all_files_are_upload = true
|
||||||
user.group.supporting_document_types.each do |type|
|
if supporting_document_file.supportable_type == 'User'
|
||||||
file = type.supporting_document_files.find_by(user_id: supporting_document_file.user_id)
|
user = supporting_document_file.supportable
|
||||||
all_files_are_upload = false unless file
|
user.group.supporting_document_types.each do |type|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
if all_files_are_upload
|
if all_files_are_upload && (supporting_document_file.supportable_type == 'User')
|
||||||
NotificationCenter.call type: 'notify_admin_user_supporting_document_files_created',
|
NotificationCenter.call type: 'notify_admin_user_supporting_document_files_created',
|
||||||
receiver: User.admins_and_managers,
|
receiver: User.admins_and_managers,
|
||||||
attached_object: user
|
attached_object: user
|
||||||
@ -32,13 +41,16 @@ class SupportingDocumentFileService
|
|||||||
def self.update(supporting_document_file, params)
|
def self.update(supporting_document_file, params)
|
||||||
updated = supporting_document_file.update(params)
|
updated = supporting_document_file.update(params)
|
||||||
if updated
|
if updated
|
||||||
user = supporting_document_file.user
|
|
||||||
all_files_are_upload = true
|
all_files_are_upload = true
|
||||||
user.group.supporting_document_types.each do |type|
|
if supporting_document_file.supportable_type == 'User'
|
||||||
file = type.supporting_document_files.find_by(user_id: supporting_document_file.user_id)
|
user = supporting_document_file.supportable
|
||||||
all_files_are_upload = false unless file
|
user.group.supporting_document_types.each do |type|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
if all_files_are_upload
|
if all_files_are_upload && (supporting_document_file.supportable_type == 'User')
|
||||||
NotificationCenter.call type: 'notify_admin_user_supporting_document_files_updated',
|
NotificationCenter.call type: 'notify_admin_user_supporting_document_files_updated',
|
||||||
receiver: User.admins_and_managers,
|
receiver: User.admins_and_managers,
|
||||||
attached_object: supporting_document_file
|
attached_object: supporting_document_file
|
||||||
|
@ -4,19 +4,22 @@
|
|||||||
class SupportingDocumentRefusalService
|
class SupportingDocumentRefusalService
|
||||||
def self.list(filters = {})
|
def self.list(filters = {})
|
||||||
refusals = []
|
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
|
refusals
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.create(supporting_document_refusal)
|
def self.create(supporting_document_refusal)
|
||||||
saved = supporting_document_refusal.save
|
saved = supporting_document_refusal.save
|
||||||
|
|
||||||
if saved
|
if saved && supporting_document_refusal.supportable_type == 'User'
|
||||||
NotificationCenter.call type: 'notify_admin_user_supporting_document_refusal',
|
NotificationCenter.call type: 'notify_admin_user_supporting_document_refusal',
|
||||||
receiver: User.admins_and_managers,
|
receiver: User.admins_and_managers,
|
||||||
attached_object: supporting_document_refusal
|
attached_object: supporting_document_refusal
|
||||||
NotificationCenter.call type: 'notify_user_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
|
attached_object: supporting_document_refusal
|
||||||
end
|
end
|
||||||
saved
|
saved
|
||||||
|
@ -9,7 +9,7 @@ class SupportingDocumentTypeService
|
|||||||
|
|
||||||
group.supporting_document_types.includes(:groups)
|
group.supporting_document_types.includes(:groups)
|
||||||
else
|
else
|
||||||
SupportingDocumentType.all
|
SupportingDocumentType.where(document_type: filters[:document_type] || 'User')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
json.extract! child, :id, :first_name, :last_name, :email, :birthday, :phone, :user_id
|
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
|
||||||
|
@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
json.title notification.notification_type
|
json.title notification.notification_type
|
||||||
json.description t('.supporting_document_files_uploaded',
|
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'))
|
||||||
|
@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
json.title notification.notification_type
|
json.title notification.notification_type
|
||||||
json.description t('.refusal',
|
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'))
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# frozen_string_literal: true
|
# 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
|
json.attachment supporting_document_file.attachment.file.filename
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
# frozen_string_literal: true
|
# 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
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
json.extract! supporting_document_type, :id, :name, :group_ids
|
json.extract! supporting_document_type, :id, :name, :group_ids, :document_type
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= t('.body.user_update_supporting_document_file',
|
<%= 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>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><%= @attached_object.supporting_document_type.name %></li>
|
<li><%= @attached_object.supporting_document_type.name %></li>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= t('.body.user_supporting_document_files_refusal',
|
<%= 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')) %>
|
OPERATOR: @attached_object&.operator&.profile&.full_name || t('api.notifications.deleted_user')) %>
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -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
|
@ -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
|
@ -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
|
@ -3746,10 +3746,11 @@ ALTER SEQUENCE public.subscriptions_id_seq OWNED BY public.subscriptions.id;
|
|||||||
CREATE TABLE public.supporting_document_files (
|
CREATE TABLE public.supporting_document_files (
|
||||||
id bigint NOT NULL,
|
id bigint NOT NULL,
|
||||||
supporting_document_type_id bigint,
|
supporting_document_type_id bigint,
|
||||||
user_id bigint,
|
supportable_id bigint,
|
||||||
attachment character varying,
|
attachment character varying,
|
||||||
created_at timestamp without time zone NOT NULL,
|
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 (
|
CREATE TABLE public.supporting_document_refusals (
|
||||||
id bigint NOT NULL,
|
id bigint NOT NULL,
|
||||||
user_id bigint,
|
supportable_id bigint,
|
||||||
operator_id integer,
|
operator_id integer,
|
||||||
message text,
|
message text,
|
||||||
created_at timestamp without time zone NOT NULL,
|
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,
|
id bigint NOT NULL,
|
||||||
name character varying,
|
name character varying,
|
||||||
created_at timestamp without time zone NOT NULL,
|
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);
|
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: -
|
-- 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);
|
CREATE INDEX index_supporting_document_refusals_on_supportable_id ON public.supporting_document_refusals USING btree (supportable_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);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
@ -8144,7 +8147,7 @@ ALTER TABLE ONLY public.orders
|
|||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public.supporting_document_refusals
|
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'),
|
('20230509161557'),
|
||||||
('20230510141305'),
|
('20230510141305'),
|
||||||
('20230511080650'),
|
('20230511080650'),
|
||||||
('20230511081018');
|
('20230511081018'),
|
||||||
|
('20230524080448'),
|
||||||
|
('20230524083558'),
|
||||||
|
('20230524110215');
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user