fix show file document detail, update document

This commit is contained in:
dbdbd9 2025-03-13 14:14:43 +07:00
parent 0c5f56d30c
commit e026b70e66
8 changed files with 4181 additions and 151 deletions

View File

@ -94,6 +94,7 @@ class DocumentController extends Controller
'uri.*' => 'nullable|url', 'uri.*' => 'nullable|url',
'files' => 'nullable|array', 'files' => 'nullable|array',
'files.*' => 'file|mimes:doc,docx,xls,xlsx,pdf|max:20480', 'files.*' => 'file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
'existing_files' => 'nullable|array',
'is_active' => 'required|boolean', 'is_active' => 'required|boolean',
]); ]);
@ -104,14 +105,17 @@ class DocumentController extends Controller
if ($request->type === "file") { if ($request->type === "file") {
$existingFiles = explode(',', $document->uri); $existingFiles = explode(',', $document->uri);
foreach ($existingFiles as $file) { $selectedExistingFiles = $request->existing_files ?? [];
$filesToDelete = array_diff($existingFiles, $selectedExistingFiles);
foreach ($filesToDelete as $file) {
$filePath = str_replace('storage/', 'public/', $file); $filePath = str_replace('storage/', 'public/', $file);
if (Storage::exists($filePath)) { if (Storage::exists($filePath)) {
Storage::delete($filePath); Storage::delete($filePath);
} }
} }
$uploadedFiles = []; $uploadedFiles = $selectedExistingFiles;
if ($request->hasFile('files')) { if ($request->hasFile('files')) {
foreach ($request->file('files') as $file) { foreach ($request->file('files') as $file) {
$path = $file->store('uploads', 'public'); $path = $file->store('uploads', 'public');

View File

@ -2,6 +2,7 @@
use App\Http\Middleware\CheckPermission; use App\Http\Middleware\CheckPermission;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;
use Modules\Admin\app\Http\Controllers\AdminController; use Modules\Admin\app\Http\Controllers\AdminController;
use Modules\Admin\app\Http\Controllers\BannerController; use Modules\Admin\app\Http\Controllers\BannerController;
use Modules\Admin\app\Http\Controllers\CategoryController; use Modules\Admin\app\Http\Controllers\CategoryController;
@ -224,6 +225,23 @@ Route::middleware('api')
Route::post('/update', [DocumentController::class, 'update'])->middleware('check.permission:admin'); Route::post('/update', [DocumentController::class, 'update'])->middleware('check.permission:admin');
Route::get('/delete', [DocumentController::class, 'delete'])->middleware('check.permission:admin'); Route::get('/delete', [DocumentController::class, 'delete'])->middleware('check.permission:admin');
}); });
Route::get('/download-file/{filename}', function ($filename) {
$path = "uploads/{$filename}";
if (!Storage::disk('public')->exists($path)) {
return response()->json(['error' => 'File not found'], 404);
}
$headers = [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET',
'Access-Control-Allow-Headers' => 'Content-Type',
'Content-Disposition' => 'inline; filename="' . $filename . '"',
];
return response()->file(storage_path("app/public/{$path}"), $headers);
});
}); });
}); });

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-doc-viewer": "^0.1.14", "react-doc-viewer": "^0.1.14",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-file-viewer": "^1.2.1",
"react-redux": "^8.1.3", "react-redux": "^8.1.3",
"react-router-dom": "^6.19.0", "react-router-dom": "^6.19.0",
"reactstrap": "^9.2.2", "reactstrap": "^9.2.2",

View File

@ -116,3 +116,6 @@ export const listDocument = API_URL + 'v1/admin/document/all'
export const createDocument = API_URL + 'v1/admin/document/create' export const createDocument = API_URL + 'v1/admin/document/create'
export const updateDocument = API_URL + 'v1/admin/document/update' export const updateDocument = API_URL + 'v1/admin/document/update'
export const deleteDocument = API_URL + 'v1/admin/document/delete' export const deleteDocument = API_URL + 'v1/admin/document/delete'
// Download File
export const downloadFile = API_URL + 'v1/admin/download-file'

View File

@ -3,18 +3,19 @@ import { get } from '@/rtk/helpers/apiService'
import { deleteDocument, listDocument } from '@/api/Admin' import { deleteDocument, listDocument } from '@/api/Admin'
import { Xdelete } from '@/rtk/helpers/CRUD' import { Xdelete } from '@/rtk/helpers/CRUD'
import { Anchor, Box, Button, Dialog, Group, Loader, Text } from '@mantine/core' import {
Anchor,
Badge,
Box,
Button,
Dialog,
Group,
Loader,
Text,
} from '@mantine/core'
import { useDisclosure } from '@mantine/hooks' import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications' import { notifications } from '@mantine/notifications'
import { import { IconEdit, IconX } from '@tabler/icons-react'
IconEdit,
IconFileInfo,
IconFileTypeDoc,
IconFileTypePdf,
IconFileTypeXls,
IconFileUnknown,
IconX,
} from '@tabler/icons-react'
import DataTableAll from '@/components/DataTable/DataTable' import DataTableAll from '@/components/DataTable/DataTable'
import ModalAddEditDocument from './ModalAddEditDocument' import ModalAddEditDocument from './ModalAddEditDocument'
import ModalFileDocument from './ModalFileDocument' import ModalFileDocument from './ModalFileDocument'
@ -91,35 +92,6 @@ const Document = () => {
} }
} }
const getFileTypeIcon = (url: string) => {
if (!url) return null
try {
const parsedUrl = new URL(url)
const hostname = parsedUrl.hostname
const pathname = parsedUrl.pathname.toLowerCase()
if (
hostname.includes('docs.google.com') ||
hostname.includes('drive.google.com')
) {
if (pathname.includes('/document/')) {
return <IconFileTypeDoc style={{ color: '#1e62c1' }} />
}
if (pathname.includes('/spreadsheets/')) {
return <IconFileTypeXls style={{ color: '#0e864b' }} />
}
return <IconFileTypePdf style={{ color: '#ff1b0e' }} />
}
return <IconFileUnknown style={{ color: '#000' }} />
} catch (error) {
console.error('Invalid URL:', url)
}
return null
}
const columns = [ const columns = [
{ {
name: 'id', name: 'id',
@ -131,7 +103,7 @@ const Document = () => {
}, },
{ {
name: 'title', name: 'title',
size: '40%', size: '30%',
header: 'Title', header: 'Title',
render: (row: TDocument) => { render: (row: TDocument) => {
return <Text ta="start">{row?.title}</Text> return <Text ta="start">{row?.title}</Text>
@ -139,24 +111,30 @@ const Document = () => {
}, },
{ {
name: 'uri', name: 'uri',
size: '50%', size: '60%',
header: 'URI', header: 'URI',
render: (row: TDocument) => { render: (row: TDocument) => {
return ( return (
<Box> <Box>
{row.type === 'file' ? ( {row.type === 'file' ? (
<Text <Group gap={5}>
style={{ cursor: 'pointer' }} {row?.uri &&
ta="start" row?.uri.split(',')?.map((uriItem) => (
onClick={() => { <Badge
setSelectDataRow(row) style={{ cursor: 'pointer' }}
openModalFile() tt="initial"
}} onClick={() => {
> setSelectDataRow({ ...row, uri: uriItem })
<IconFileInfo style={{ color: '#ffa500' }} /> openModalFile()
</Text> }}
color="orange"
>
{uriItem.replace('storage/uploads/', '')}
</Badge>
))}
</Group>
) : ( ) : (
<Group> <Group gap={5}>
{row?.uri && {row?.uri &&
row?.uri.split(',')?.map((uriItem) => ( row?.uri.split(',')?.map((uriItem) => (
<Anchor <Anchor
@ -165,7 +143,9 @@ const Document = () => {
target="_blank" target="_blank"
title={uriItem} title={uriItem}
> >
{getFileTypeIcon(uriItem)} <Badge style={{ cursor: 'pointer' }} tt="initial">
{uriItem}
</Badge>
</Anchor> </Anchor>
))} ))}
</Group> </Group>
@ -225,6 +205,18 @@ const Document = () => {
</div> </div>
<Box mt={'md'}> <Box mt={'md'}>
<Group wrap="wrap" mb={'md'} px={16}>
<Text fw={500}>Note: </Text>
<Badge style={{ cursor: 'pointer' }} tt="initial">
Links
</Badge>
<Badge style={{ cursor: 'pointer' }} tt="initial" color="orange">
Files
</Badge>
</Group>
{loader ? ( {loader ? (
<Box ta={'center'}> <Box ta={'center'}>
<Loader size={40} mt={'15%'} /> <Loader size={40} mt={'15%'} />

View File

@ -10,6 +10,7 @@ import {
Checkbox, Checkbox,
FileInput, FileInput,
TagsInput, TagsInput,
Group,
} from '@mantine/core' } from '@mantine/core'
import { create, update } from '@/rtk/helpers/CRUD' import { create, update } from '@/rtk/helpers/CRUD'
@ -117,7 +118,7 @@ const ModalAddEditDocument = ({
const header = await getHeaderInfo() const header = await getHeaderInfo()
if (data.type) { if (data.type) {
if (data.files.length < 1) { if (data.files.length < 1 && data.uri.length < 1) {
notifications.show({ notifications.show({
title: 'Error', title: 'Error',
message: 'Upload at least 1 file', message: 'Upload at least 1 file',
@ -132,11 +133,15 @@ const ModalAddEditDocument = ({
tmpFormData.append('title', data.title) tmpFormData.append('title', data.title)
tmpFormData.append('type', data.type ? 'file' : 'link') tmpFormData.append('type', data.type ? 'file' : 'link')
tmpFormData.append('is_active', data.is_active) tmpFormData.append('is_active', data.is_active ? '1' : '0')
for (let i = 0; i < data.files.length; i++) { for (let i = 0; i < data.files.length; i++) {
tmpFormData.append('files[]', data.files[i]) tmpFormData.append('files[]', data.files[i])
} }
data.uri.forEach((fileUri: string) => {
tmpFormData.append('existing_files[]', fileUri)
})
formdata = tmpFormData formdata = tmpFormData
} else { } else {
const { files, ...rest } = data const { files, ...rest } = data
@ -230,6 +235,32 @@ const ModalAddEditDocument = ({
disabled={loadingSubmit} disabled={loadingSubmit}
required required
/> />
{selectDataRow?.uri && form.values.uri.length > 0 && (
<Box>
<Text fw={500}>Existing Files:</Text>
{form.values.uri.map((fileUri: string, index) => (
<Group key={index} justify="space-between" mb="sm">
<Text size="sm">
{fileUri.replace('storage/uploads/', '')}
</Text>
<Button
color="red"
size="xs"
ml="md"
onClick={() =>
form.setFieldValue(
'uri',
form.values.uri.filter((_, i) => i !== index),
)
}
>
Remove
</Button>
</Group>
))}
</Box>
)}
</Box> </Box>
) : ( ) : (
<Box> <Box>

View File

@ -1,4 +1,9 @@
import { Modal, Text, Box } from '@mantine/core' import { useEffect, useState } from 'react'
import { getDownloadFile } from '@/rtk/helpers/apiService'
import { Modal, Text, Box, Loader, Paper, Button, Group } from '@mantine/core'
import FileViewer from 'react-file-viewer'
import { IconDownload } from '@tabler/icons-react'
import { downloadFile } from '@/api/Admin'
type MProps = { type MProps = {
opened: boolean opened: boolean
@ -7,17 +12,48 @@ type MProps = {
setSelectDataRow: any setSelectDataRow: any
} }
interface TDocumentFile {
uri: string
fileType: string
}
const ModalFileDocument = ({ const ModalFileDocument = ({
opened, opened,
close, close,
selectDataRow, selectDataRow,
setSelectDataRow, setSelectDataRow,
}: MProps) => { }: MProps) => {
// const supportedFileTypes = ['pdf', 'xls', 'xlsx', 'docx', 'doc'] const [fileDoc, setFileDoc] = useState<TDocumentFile>()
// const getFileType = (fileName: string) => { const [loader, setLoader] = useState<boolean>(false)
// const extension = fileName.split('.').pop()?.toLowerCase()
// return supportedFileTypes.includes(extension!) ? extension : 'default' useEffect(() => {
// } getFile()
}, [])
const getFile = async () => {
try {
setLoader(true)
const params = {}
const fileUri = selectDataRow?.uri.replace('storage/uploads/', '')
const res = await getDownloadFile(`${downloadFile}/${fileUri}`, params)
setFileDoc({
uri: URL.createObjectURL(res.data),
fileType: getFileType(selectDataRow?.uri) || 'default',
})
} catch (error: any) {
console.log(error)
} finally {
setLoader(false)
}
}
const supportedFileTypes = ['pdf', 'xlsx', 'xls', 'docx', 'doc']
const getFileType = (fileName: string) => {
const extension = fileName.split('.').pop()?.toLowerCase()
return supportedFileTypes.includes(extension!) ? extension : 'default'
}
return ( return (
<Modal <Modal
@ -26,34 +62,48 @@ const ModalFileDocument = ({
close() close()
setSelectDataRow({}) setSelectDataRow({})
}} }}
size="xl" size="65%"
title={ title={
<Text fw={700} fz={'lg'}> <Text fw={700} fz={'lg'}>
{selectDataRow?.title} File Detail: {selectDataRow?.title}
</Text> </Text>
} }
> >
<Box> <Group justify="flex-end" mb={'md'}>
{selectDataRow?.uri?.split(',').map((uriItem: string) => { <a
let url = import.meta.env.VITE_BACKEND_URL + uriItem href={`${import.meta.env.VITE_BACKEND_URL}${selectDataRow.uri}`}
const extension = url.split('.').pop()?.toLowerCase() download="Document Download"
target={
if (extension === 'doc' || extension === 'docx') { getFileType(selectDataRow?.uri) === 'pdf' ? '_blank' : '_self'
const docUrl = encodeURIComponent(uriItem)
url = `https://view.officeapps.live.com/op/embed.aspx?src=${docUrl}`
} }
>
<Button
leftSection={<IconDownload size={18} />}
color={
getFileType(selectDataRow?.uri) === 'pdf'
? 'red'
: getFileType(selectDataRow?.uri) === 'xlsx' ||
getFileType(selectDataRow?.uri) === 'xls'
? 'green'
: ''
}
>
Download .{getFileType(selectDataRow?.uri)}
</Button>
</a>
</Group>
return ( <Paper withBorder>
<Box> {loader ? (
<iframe <Box ta={'center'} my={20}>
key={uriItem} <Loader size={40} />
src={url} </Box>
style={{ width: '100%', height: '800px', border: 'none' }} ) : (
/> <Box w="100%">
</Box> <FileViewer fileType={fileDoc?.fileType} filePath={fileDoc?.uri} />
) </Box>
})} )}
</Box> </Paper>
</Modal> </Modal>
) )
} }