diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/DocumentController.php b/BACKEND/Modules/Admin/app/Http/Controllers/DocumentController.php index 5807331..8bd9290 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/DocumentController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/DocumentController.php @@ -46,42 +46,48 @@ class DocumentController extends Controller public function create(Request $request) { $request->validate([ - 'title' => 'required|string|max:255', 'type' => 'required|in:file,link', - 'uri' => 'nullable|array', - 'uri.*' => 'nullable|url', 'files' => 'nullable|array', - 'files.*' => 'file|mimes:doc,docx,xls,xlsx,pdf|max:20480', + 'files.*.title' => 'required|string|max:255', + 'files.*.file' => 'required|file|mimes:doc,docx,xls,xlsx,pdf|max:20480', + 'links' => 'nullable|array', + 'links.*.title' => 'required|string|max:255', + 'links.*.uri' => 'required|string|url', 'is_active' => 'required|boolean', ]); - if($request->type == "file") { - $uploadedFiles = []; - if ($request->hasFile('files')) { - foreach ($request->file('files') as $file) { - $path = $file->store('uploads', options: 'public'); - $uploadedFiles[] = "storage/{$path}"; - } + $documents = []; + + if ($request->type === "file") { + foreach ($request->file('files') as $index => $file) { + $path = $file['file']->store('uploads', 'public'); + $documents[] = [ + 'title' => $request->input("files.$index.title"), + 'type' => 'file', + 'uri' => "storage/{$path}", + 'is_active' => $request->is_active, + 'created_at' => now(), + 'updated_at' => now(), + ]; + } + } else { + foreach ($request->links as $link) { + $documents[] = [ + 'title' => $link['title'], + 'type' => 'link', + 'uri' => $link['uri'], + 'is_active' => $request->is_active, + 'created_at' => now(), + 'updated_at' => now(), + ]; } - - $document = Document::create([ - 'title' => $request->title, - 'type' => $request->type, - 'uri' => implode(',', $uploadedFiles), - 'is_active' => $request->is_active, - ]); - - return AbstractController::ResultSuccess($document, "Document created successfully!"); } - $document = Document::create([ - 'title' => $request->title, - 'type' => $request->type, - 'uri' => implode(',', $request->uri), - 'is_active' => $request->is_active, - ]); + if (!empty($documents)) { + Document::insert($documents); + } - return AbstractController::ResultSuccess($document, "Document created successfully!"); + return AbstractController::ResultSuccess($documents, "Documents created successfully!"); } public function update(Request $request) @@ -90,11 +96,9 @@ class DocumentController extends Controller 'id' => 'required|exists:documents,id', 'title' => 'required|string|max:255', 'type' => 'required|in:file,link', - 'uri' => 'nullable|array', - 'uri.*' => 'nullable|url', - 'files' => 'nullable|array', - 'files.*' => 'file|mimes:doc,docx,xls,xlsx,pdf|max:20480', - 'existing_files' => 'nullable|array', + 'uri' => 'nullable|url', + 'file' => 'nullable|file|mimes:doc,docx,xls,xlsx,pdf|max:20480', + 'existing_file' => 'nullable|string', 'is_active' => 'required|boolean', ]); @@ -104,29 +108,22 @@ class DocumentController extends Controller } if ($request->type === "file") { - $existingFiles = explode(',', $document->uri); - $selectedExistingFiles = $request->existing_files ?? []; + $uri = $request->existing_file; - $filesToDelete = array_diff($existingFiles, $selectedExistingFiles); - foreach ($filesToDelete as $file) { - $filePath = str_replace('storage/', 'public/', $file); + if ($request->hasFile('file')) { + $filePath = str_replace('storage/', 'public/', $request->existing_file); if (Storage::exists($filePath)) { Storage::delete($filePath); } - } - $uploadedFiles = $selectedExistingFiles; - if ($request->hasFile('files')) { - foreach ($request->file('files') as $file) { - $path = $file->store('uploads', 'public'); - $uploadedFiles[] = "storage/{$path}"; - } + $path = $request->file('file')->store('uploads', 'public'); + $uri = "storage/{$path}"; } $document->update([ 'title' => $request->title, 'type' => $request->type, - 'uri' => implode(',', $uploadedFiles), + 'uri' => $uri, 'is_active' => $request->is_active, ]); @@ -136,7 +133,7 @@ class DocumentController extends Controller $document->update([ 'title' => $request->title, 'type' => $request->type, - 'uri' => implode(',', $request->uri), + 'uri' => $request->uri, 'is_active' => $request->is_active, ]); @@ -152,12 +149,9 @@ class DocumentController extends Controller } if ($document->type === "file") { - $existingFiles = explode(',', $document->uri); - foreach ($existingFiles as $file) { - $filePath = str_replace('storage/', 'public/', $file); - if (Storage::exists($filePath)) { - Storage::delete($filePath); - } + $filePath = str_replace('storage/', 'public/', $document->uri); + if (Storage::exists($filePath)) { + Storage::delete($filePath); } } diff --git a/FRONTEND/src/global.d.ts b/FRONTEND/src/global.d.ts index bcf83d5..a2beb17 100755 --- a/FRONTEND/src/global.d.ts +++ b/FRONTEND/src/global.d.ts @@ -3,3 +3,8 @@ declare module '*.css' declare module '@codemirror/lang-javascript' declare const __VITE_BACKEND_URL__: string + +declare module 'react-file-viewer' { + const FileViewer: any + export default FileViewer +} diff --git a/FRONTEND/src/pages/Document/Document.tsx b/FRONTEND/src/pages/Document/Document.tsx index 40f4316..3da430f 100644 --- a/FRONTEND/src/pages/Document/Document.tsx +++ b/FRONTEND/src/pages/Document/Document.tsx @@ -3,24 +3,24 @@ import { get } from '@/rtk/helpers/apiService' import { deleteDocument, listDocument } from '@/api/Admin' import { Xdelete } from '@/rtk/helpers/CRUD' -import { - Anchor, - Badge, - Box, - Button, - Dialog, - Group, - Loader, - Text, -} from '@mantine/core' +import { Anchor, Box, Button, Dialog, Group, Loader, Text } from '@mantine/core' import { useDisclosure } from '@mantine/hooks' import { notifications } from '@mantine/notifications' -import { IconEdit, IconX } from '@tabler/icons-react' +import { + IconEdit, + IconFileTypeDoc, + IconFileTypePdf, + IconFileTypeXls, + IconLink, + IconX, +} from '@tabler/icons-react' import DataTableAll from '@/components/DataTable/DataTable' -import ModalAddEditDocument from './ModalAddEditDocument' import ModalFileDocument from './ModalFileDocument' import classes from './Document.module.css' +import ModalAddDocument from './ModalAddDocument' +import ModalEditDocument from './ModalEditDocument' +import { checkPermissions } from '@/utils/checkRoles' interface TDocument { id: number @@ -39,17 +39,16 @@ type RequestPagination = { const Document = () => { const [loader, setLoader] = useState(false) - const [action, setAction] = useState('') const [rows, setRows] = useState({ data: [], status: true, }) const [selectDataRow, setSelectDataRow] = useState({}) - const [ - openedModalAddEdit, - { open: openModalAddEdit, close: closeModalAddEdit }, - ] = useDisclosure(false) + const [openedModalAdd, { open: openModalAdd, close: closeModalAdd }] = + useDisclosure(false) + const [openedModalEdit, { open: openModalEdit, close: closeModalEdit }] = + useDisclosure(false) const [openedModalFile, { open: openModalFile, close: closeModalFile }] = useDisclosure(false) @@ -92,6 +91,22 @@ const Document = () => { } } + const getFileTypeIcon = (uri: string) => { + if (!uri) return null + + const extension = uri.split('.').pop()?.toLowerCase() + + if (['doc', 'docx'].includes(extension!)) { + return + } + + if (['xls', 'xlsx'].includes(extension!)) { + return + } + + return + } + const columns = [ { name: 'id', @@ -103,7 +118,7 @@ const Document = () => { }, { name: 'title', - size: '30%', + size: '50%', header: 'Title', render: (row: TDocument) => { return {row?.title} @@ -111,44 +126,35 @@ const Document = () => { }, { name: 'uri', - size: '60%', + size: '40%', header: 'URI', render: (row: TDocument) => { return ( {row.type === 'file' ? ( - - {row?.uri && - row?.uri.split(',')?.map((uriItem) => ( - { - setSelectDataRow({ ...row, uri: uriItem }) - openModalFile() - }} - color="orange" - > - {uriItem.replace('storage/uploads/', '')} - - ))} - + { + setSelectDataRow(row) + openModalFile() + }} + title={`File ${row?.uri + .split('.') + .pop() + ?.toLowerCase()} detail`} + > + {getFileTypeIcon(row?.uri)} + ) : ( - - {row?.uri && - row?.uri.split(',')?.map((uriItem) => ( - - - {uriItem} - - - ))} - + + + )} ) @@ -159,29 +165,32 @@ const Document = () => { size: '5%', header: 'Action', render: (row: TDocument) => { - return ( - - { - setAction('edit') - setSelectDataRow(row) - openModalAddEdit() - }} - width={20} - height={20} - /> - { - setOpenedDialogDelete(true) - setSelectDataRow(row) - }} - width={20} - height={20} - /> - - ) + if (checkPermissions('admin')) { + return ( + + { + setSelectDataRow(row) + openModalEdit() + }} + width={20} + height={20} + /> + { + setOpenedDialogDelete(true) + setSelectDataRow(row) + }} + width={20} + height={20} + /> + + ) + } + + return }, }, ] @@ -193,30 +202,17 @@ const Document = () => { Admin/ Documents - + + {checkPermissions('admin') ? ( + + ) : ( + '' + )} - - Note: - - - Links - - - - Files - - - {loader ? ( @@ -240,12 +236,18 @@ const Document = () => { /> )} - {openedModalAddEdit && ( - + )} + + {openedModalEdit && ( + void + getAllData: () => void +} + +const ModalAddDocument = ({ opened, close, getAllData }: MProps) => { + const [loadingSubmit, setLoadingSubmit] = useState(false) + + const form = useForm({ + initialValues: { + type: 'file', + files: [] as { title: string; file: File }[], + links: [] as { title: string; uri: string }[], + is_active: true, + }, + }) + + useEffect(() => { + form.reset() + }, []) + + const handleCreate = async (values: any) => { + try { + if (values.type === 'file' && values.files.length === 0) { + notifications.show({ + title: 'Error', + message: 'No files uploaded!!!', + color: 'red', + }) + return + } + + if (values.type === 'link' && values.links.length === 0) { + notifications.show({ + title: 'Error', + message: 'No links provided!!!', + color: 'red', + }) + return + } + + setLoadingSubmit(true) + const header = await getHeaderInfo() + const formData = new FormData() + header.headers['Content-Type'] = 'multipart/form-data' + + formData.append('type', values.type) + formData.append('is_active', values.is_active ? '1' : '0') + + if (values.type === 'file') { + values.files.forEach((item: any, index: number) => { + formData.append(`files[${index}][title]`, item.title) + formData.append(`files[${index}][file]`, item.file) + }) + } else { + values.links.forEach((item: any, index: number) => { + formData.append(`links[${index}][title]`, item.title) + formData.append(`links[${index}][uri]`, item.uri) + }) + } + + const res = await create(createDocument, formData, getAllData, header) + if (res === true) { + resetForm() + } + } catch (error) { + console.error(error) + } finally { + setLoadingSubmit(false) + } + } + + const resetForm = () => { + close() + form.reset() + } + + return ( + + Add Document + + } + > +
+ + form.setFieldValue( + 'type', + event.currentTarget.checked ? 'file' : 'link', + ) + } + mb={'md'} + disabled={loadingSubmit} + /> + + {form.values.type === 'file' ? ( + + file.file)} + onChange={(files) => { + if (files) { + const newFiles = files.map((file) => ({ + title: file.name.split('.')[0], + file, + })) + form.setFieldValue('files', newFiles) + } + }} + disabled={loadingSubmit} + required + /> + + {form.values.files.map((item, index) => ( + + + form.setFieldValue( + `files.${index}.title`, + event.currentTarget.value, + ) + } + maxLength={255} + disabled={loadingSubmit} + required + /> + + {item.file?.name} + + + ))} + + ) : ( + + {form.values.links.map((_, index) => ( + + + form.setFieldValue( + `links.${index}.title`, + event.currentTarget.value, + ) + } + maxLength={255} + disabled={loadingSubmit} + required + /> + + form.setFieldValue( + `links.${index}.uri`, + event.currentTarget.value, + ) + } + disabled={loadingSubmit} + required + /> + + form.setFieldValue( + 'links', + form.values.links.filter((_, i) => i !== index), + ) + } + disabled={loadingSubmit} + mt={24} + > + + + + ))} + + + + )} + + + form.setFieldValue('is_active', event.currentTarget.checked) + } + disabled={loadingSubmit} + /> + + + + + +
+ ) +} + +export default ModalAddDocument diff --git a/FRONTEND/src/pages/Document/ModalAddEditDocument.tsx b/FRONTEND/src/pages/Document/ModalAddEditDocument.tsx deleted file mode 100644 index 6765ade..0000000 --- a/FRONTEND/src/pages/Document/ModalAddEditDocument.tsx +++ /dev/null @@ -1,299 +0,0 @@ -import { useEffect, useState } from 'react' -import { useForm } from '@mantine/form' -import { - Modal, - Button, - TextInput, - Text, - Box, - Switch, - Checkbox, - FileInput, - TagsInput, - Group, -} from '@mantine/core' - -import { create, update } from '@/rtk/helpers/CRUD' -import { createDocument, updateDocument } from '@/api/Admin' -import { getHeaderInfo } from '@/rtk/helpers/tokenCreator' -import { notifications } from '@mantine/notifications' - -type MProps = { - opened: boolean - close: () => void - setAction: (arg0: any) => void - selectDataRow: any - setSelectDataRow: any - action: string - getAllData: () => void -} - -const ModalAddEditDocument = ({ - opened, - close, - setAction, - selectDataRow, - setSelectDataRow, - action, - getAllData, -}: MProps) => { - const [loadingSubmit, setLoadingSubmit] = useState(false) - - const form = useForm({ - initialValues: { - title: '', - type: true, - files: [] as File[], - uri: [], - is_active: true, - }, - }) - - useEffect(() => { - form.setValues({ - title: selectDataRow?.title ?? '', - type: selectDataRow?.type ? selectDataRow?.type === 'file' : true, - files: [], - uri: selectDataRow?.uri?.split(',') ?? [], - is_active: selectDataRow?.is_active ?? true, - }) - }, [selectDataRow]) - - const handleCreate = async (data: any) => { - try { - setLoadingSubmit(true) - - let formdata = {} - const header = await getHeaderInfo() - - if (data.type) { - if (data.files.length < 1) { - notifications.show({ - title: 'Error', - message: 'Upload at least 1 file', - color: 'red', - }) - - return - } - - header.headers['Content-Type'] = 'multipart/form-data' - const tmpFormData = new FormData() - - tmpFormData.append('title', data.title) - tmpFormData.append('type', data.type ? 'file' : 'link') - tmpFormData.append('is_active', data.is_active ? '1' : '0') - for (let i = 0; i < data.files.length; i++) { - tmpFormData.append('files[]', data.files[i]) - } - - formdata = tmpFormData - } else { - const { files, ...rest } = data - formdata = { - ...rest, - type: rest.type ? 'file' : 'link', - } - } - - const res = await create(createDocument, formdata, getAllData, header) - if (res === true) { - setAction('') - close() - form.reset() - setSelectDataRow({}) - } - } catch (error: any) { - console.log(error) - } finally { - setLoadingSubmit(false) - } - } - - const handleUpdate = async (data: any) => { - try { - setLoadingSubmit(true) - - let formdata = {} - const header = await getHeaderInfo() - - if (data.type) { - if (data.files.length < 1 && data.uri.length < 1) { - notifications.show({ - title: 'Error', - message: 'Upload at least 1 file', - color: 'red', - }) - - return - } - - header.headers['Content-Type'] = 'multipart/form-data' - const tmpFormData = new FormData() - - tmpFormData.append('title', data.title) - tmpFormData.append('type', data.type ? 'file' : 'link') - tmpFormData.append('is_active', data.is_active ? '1' : '0') - for (let i = 0; i < data.files.length; i++) { - tmpFormData.append('files[]', data.files[i]) - } - - data.uri.forEach((fileUri: string) => { - tmpFormData.append('existing_files[]', fileUri) - }) - - formdata = tmpFormData - } else { - const { files, ...rest } = data - formdata = { - ...rest, - type: rest.type ? 'file' : 'link', - } - } - - const res = await update( - updateDocument + `?id=${selectDataRow?.id}`, - formdata, - getAllData, - header, - ) - if (res === true) { - setAction('') - close() - form.reset() - setSelectDataRow({}) - } - } catch (error: any) { - throw new Error(error) - } finally { - setLoadingSubmit(false) - } - } - - return ( - { - setAction('') - close() - form.reset() - setSelectDataRow({}) - }} - size="lg" - title={ - - {action === 'add' ? 'Add Document' : 'Update Document'} - - } - > -
{ - if (action === 'add') { - handleCreate(values) - } else { - handleUpdate(values) - } - })} - > - - - {selectDataRow?.id ? ( - '' - ) : ( - - form.setFieldValue('type', event.currentTarget.checked) - } - mb={'md'} - disabled={loadingSubmit} - /> - )} - - - {form.values.type ? ( - - { - form.setFieldValue('files', files || []) - }} - disabled={loadingSubmit} - required - /> - - {selectDataRow?.uri && form.values.uri.length > 0 && ( - - Existing Files: - {form.values.uri.map((fileUri: string, index) => ( - - - {fileUri.replace('storage/uploads/', '')} - - - - ))} - - )} - - ) : ( - - - - )} - - - - form.setFieldValue('is_active', event.currentTarget.checked) - } - disabled={loadingSubmit} - /> - - - - - -
- ) -} - -export default ModalAddEditDocument diff --git a/FRONTEND/src/pages/Document/ModalEditDocument.tsx b/FRONTEND/src/pages/Document/ModalEditDocument.tsx new file mode 100644 index 0000000..6f90b16 --- /dev/null +++ b/FRONTEND/src/pages/Document/ModalEditDocument.tsx @@ -0,0 +1,250 @@ +import { useEffect, useState } from 'react' +import { useForm } from '@mantine/form' +import { + Modal, + Button, + Text, + Box, + Checkbox, + FileInput, + Badge, + TextInput, +} from '@mantine/core' + +import { update } from '@/rtk/helpers/CRUD' +import { updateDocument } from '@/api/Admin' +import { getHeaderInfo } from '@/rtk/helpers/tokenCreator' +import { + IconFileTypeDoc, + IconFileTypePdf, + IconFileTypeXls, +} from '@tabler/icons-react' +import ModalFileDocument from './ModalFileDocument' +import { useDisclosure } from '@mantine/hooks' + +type MProps = { + opened: boolean + close: () => void + selectDataRow: any + setSelectDataRow: any + getAllData: () => void +} + +const ModalEditDocument = ({ + opened, + close, + selectDataRow, + setSelectDataRow, + getAllData, +}: MProps) => { + const [loadingSubmit, setLoadingSubmit] = useState(false) + const [selectDataFileRow, setSelectDataFileRow] = useState({}) + + const [openedModalFile, { open: openModalFile, close: closeModalFile }] = + useDisclosure(false) + + const form = useForm({ + initialValues: { + title: '', + type: true, + file: null as File | null, + uri: '', + is_active: true, + }, + }) + + useEffect(() => { + form.setValues({ + title: selectDataRow?.title || '', + type: selectDataRow?.type === 'file', + uri: selectDataRow?.uri || '', + is_active: selectDataRow?.is_active || true, + }) + }, [selectDataRow]) + + const handleUpdate = async (data: any) => { + try { + setLoadingSubmit(true) + + let formData: any = {} + const header = await getHeaderInfo() + + if (data.type) { + header.headers['Content-Type'] = 'multipart/form-data' + const tmpFormData = new FormData() + + tmpFormData.append('title', data.title) + tmpFormData.append('type', 'file') + tmpFormData.append('is_active', data.is_active ? '1' : '0') + tmpFormData.append('existing_file', data.uri) + + if (data.file) { + tmpFormData.append('file', data.file) + } + + formData = tmpFormData + } else { + formData = { + title: data.title, + type: 'link', + uri: data.uri, + is_active: data.is_active ? '1' : '0', + } + } + + const res = await update( + updateDocument + `?id=${selectDataRow?.id}`, + formData, + getAllData, + header, + ) + if (res === true) { + resetForm() + } + } catch (error: any) { + console.error(error) + } finally { + setLoadingSubmit(false) + } + } + + const getFileTypeIcon = (uri: string) => { + if (!uri) return null + + const extension = uri.split('.').pop()?.toLowerCase() + + if (['doc', 'docx'].includes(extension!)) { + return + } + + if (['xls', 'xlsx'].includes(extension!)) { + return + } + + return + } + + const resetForm = () => { + close() + form.reset() + setSelectDataRow({}) + } + + return ( + + Update Document + + } + > +
{ + handleUpdate(values) + })} + > + {form.values.type ? ( + + Files + + ) : ( + + Links + + )} + + {form.values.type ? ( + + + form.setFieldValue('file', file || null)} + disabled={loadingSubmit} + mb={'md'} + /> + + {form.values.file ? ( + '' + ) : ( + { + openModalFile() + setSelectDataFileRow(selectDataRow) + }} + > + {getFileTypeIcon(form.values.uri)} + + )} + + ) : ( + + + + + )} + + + form.setFieldValue('is_active', event.currentTarget.checked) + } + disabled={loadingSubmit} + /> + + + + + + + {openedModalFile && ( + + )} +
+ ) +} + +export default ModalEditDocument