BE, FE Document #113
			
				
			
		
		
		
	| 
						 | 
				
			
			@ -94,6 +94,7 @@ class DocumentController extends Controller
 | 
			
		|||
            'uri.*' => 'nullable|url',
 | 
			
		||||
            'files' => 'nullable|array',
 | 
			
		||||
            'files.*' => 'file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
 | 
			
		||||
            'existing_files' => 'nullable|array',
 | 
			
		||||
            'is_active' => 'required|boolean',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -104,14 +105,17 @@ class DocumentController extends Controller
 | 
			
		|||
 | 
			
		||||
        if ($request->type === "file") {
 | 
			
		||||
            $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);
 | 
			
		||||
                if (Storage::exists($filePath)) {
 | 
			
		||||
                    Storage::delete($filePath);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $uploadedFiles = [];
 | 
			
		||||
            $uploadedFiles = $selectedExistingFiles;
 | 
			
		||||
            if ($request->hasFile('files')) {
 | 
			
		||||
                foreach ($request->file('files') as $file) {
 | 
			
		||||
                    $path = $file->store('uploads', 'public');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
 | 
			
		||||
use App\Http\Middleware\CheckPermission;
 | 
			
		||||
use Illuminate\Support\Facades\Route;
 | 
			
		||||
use Illuminate\Support\Facades\Storage;
 | 
			
		||||
use Modules\Admin\app\Http\Controllers\AdminController;
 | 
			
		||||
use Modules\Admin\app\Http\Controllers\BannerController;
 | 
			
		||||
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::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
											
										
									
								
							| 
						 | 
				
			
			@ -34,6 +34,7 @@
 | 
			
		|||
    "react": "^18.2.0",
 | 
			
		||||
    "react-doc-viewer": "^0.1.14",
 | 
			
		||||
    "react-dom": "^18.2.0",
 | 
			
		||||
    "react-file-viewer": "^1.2.1",
 | 
			
		||||
    "react-redux": "^8.1.3",
 | 
			
		||||
    "react-router-dom": "^6.19.0",
 | 
			
		||||
    "reactstrap": "^9.2.2",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -116,3 +116,6 @@ export const listDocument = API_URL + 'v1/admin/document/all'
 | 
			
		|||
export const createDocument = API_URL + 'v1/admin/document/create'
 | 
			
		||||
export const updateDocument = API_URL + 'v1/admin/document/update'
 | 
			
		||||
export const deleteDocument = API_URL + 'v1/admin/document/delete'
 | 
			
		||||
 | 
			
		||||
// Download File
 | 
			
		||||
export const downloadFile = API_URL + 'v1/admin/download-file'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,18 +3,19 @@ import { get } from '@/rtk/helpers/apiService'
 | 
			
		|||
import { deleteDocument, listDocument } from '@/api/Admin'
 | 
			
		||||
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 { notifications } from '@mantine/notifications'
 | 
			
		||||
import {
 | 
			
		||||
  IconEdit,
 | 
			
		||||
  IconFileInfo,
 | 
			
		||||
  IconFileTypeDoc,
 | 
			
		||||
  IconFileTypePdf,
 | 
			
		||||
  IconFileTypeXls,
 | 
			
		||||
  IconFileUnknown,
 | 
			
		||||
  IconX,
 | 
			
		||||
} from '@tabler/icons-react'
 | 
			
		||||
import { IconEdit, IconX } from '@tabler/icons-react'
 | 
			
		||||
import DataTableAll from '@/components/DataTable/DataTable'
 | 
			
		||||
import ModalAddEditDocument from './ModalAddEditDocument'
 | 
			
		||||
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 = [
 | 
			
		||||
    {
 | 
			
		||||
      name: 'id',
 | 
			
		||||
| 
						 | 
				
			
			@ -131,7 +103,7 @@ const Document = () => {
 | 
			
		|||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'title',
 | 
			
		||||
      size: '40%',
 | 
			
		||||
      size: '30%',
 | 
			
		||||
      header: 'Title',
 | 
			
		||||
      render: (row: TDocument) => {
 | 
			
		||||
        return <Text ta="start">{row?.title}</Text>
 | 
			
		||||
| 
						 | 
				
			
			@ -139,24 +111,30 @@ const Document = () => {
 | 
			
		|||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'uri',
 | 
			
		||||
      size: '50%',
 | 
			
		||||
      size: '60%',
 | 
			
		||||
      header: 'URI',
 | 
			
		||||
      render: (row: TDocument) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <Box>
 | 
			
		||||
            {row.type === 'file' ? (
 | 
			
		||||
              <Text
 | 
			
		||||
                style={{ cursor: 'pointer' }}
 | 
			
		||||
                ta="start"
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setSelectDataRow(row)
 | 
			
		||||
                  openModalFile()
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <IconFileInfo style={{ color: '#ffa500' }} />
 | 
			
		||||
              </Text>
 | 
			
		||||
              <Group gap={5}>
 | 
			
		||||
                {row?.uri &&
 | 
			
		||||
                  row?.uri.split(',')?.map((uriItem) => (
 | 
			
		||||
                    <Badge
 | 
			
		||||
                      style={{ cursor: 'pointer' }}
 | 
			
		||||
                      tt="initial"
 | 
			
		||||
                      onClick={() => {
 | 
			
		||||
                        setSelectDataRow({ ...row, uri: uriItem })
 | 
			
		||||
                        openModalFile()
 | 
			
		||||
                      }}
 | 
			
		||||
                      color="orange"
 | 
			
		||||
                    >
 | 
			
		||||
                      {uriItem.replace('storage/uploads/', '')}
 | 
			
		||||
                    </Badge>
 | 
			
		||||
                  ))}
 | 
			
		||||
              </Group>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <Group>
 | 
			
		||||
              <Group gap={5}>
 | 
			
		||||
                {row?.uri &&
 | 
			
		||||
                  row?.uri.split(',')?.map((uriItem) => (
 | 
			
		||||
                    <Anchor
 | 
			
		||||
| 
						 | 
				
			
			@ -165,7 +143,9 @@ const Document = () => {
 | 
			
		|||
                      target="_blank"
 | 
			
		||||
                      title={uriItem}
 | 
			
		||||
                    >
 | 
			
		||||
                      {getFileTypeIcon(uriItem)}
 | 
			
		||||
                      <Badge style={{ cursor: 'pointer' }} tt="initial">
 | 
			
		||||
                        {uriItem}
 | 
			
		||||
                      </Badge>
 | 
			
		||||
                    </Anchor>
 | 
			
		||||
                  ))}
 | 
			
		||||
              </Group>
 | 
			
		||||
| 
						 | 
				
			
			@ -225,6 +205,18 @@ const Document = () => {
 | 
			
		|||
      </div>
 | 
			
		||||
 | 
			
		||||
      <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 ? (
 | 
			
		||||
          <Box ta={'center'}>
 | 
			
		||||
            <Loader size={40} mt={'15%'} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,7 @@ import {
 | 
			
		|||
  Checkbox,
 | 
			
		||||
  FileInput,
 | 
			
		||||
  TagsInput,
 | 
			
		||||
  Group,
 | 
			
		||||
} from '@mantine/core'
 | 
			
		||||
 | 
			
		||||
import { create, update } from '@/rtk/helpers/CRUD'
 | 
			
		||||
| 
						 | 
				
			
			@ -117,7 +118,7 @@ const ModalAddEditDocument = ({
 | 
			
		|||
      const header = await getHeaderInfo()
 | 
			
		||||
 | 
			
		||||
      if (data.type) {
 | 
			
		||||
        if (data.files.length < 1) {
 | 
			
		||||
        if (data.files.length < 1 && data.uri.length < 1) {
 | 
			
		||||
          notifications.show({
 | 
			
		||||
            title: 'Error',
 | 
			
		||||
            message: 'Upload at least 1 file',
 | 
			
		||||
| 
						 | 
				
			
			@ -132,11 +133,15 @@ const ModalAddEditDocument = ({
 | 
			
		|||
 | 
			
		||||
        tmpFormData.append('title', data.title)
 | 
			
		||||
        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++) {
 | 
			
		||||
          tmpFormData.append('files[]', data.files[i])
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        data.uri.forEach((fileUri: string) => {
 | 
			
		||||
          tmpFormData.append('existing_files[]', fileUri)
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        formdata = tmpFormData
 | 
			
		||||
      } else {
 | 
			
		||||
        const { files, ...rest } = data
 | 
			
		||||
| 
						 | 
				
			
			@ -230,6 +235,32 @@ const ModalAddEditDocument = ({
 | 
			
		|||
                disabled={loadingSubmit}
 | 
			
		||||
                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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = {
 | 
			
		||||
  opened: boolean
 | 
			
		||||
| 
						 | 
				
			
			@ -7,17 +12,48 @@ type MProps = {
 | 
			
		|||
  setSelectDataRow: any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface TDocumentFile {
 | 
			
		||||
  uri: string
 | 
			
		||||
  fileType: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ModalFileDocument = ({
 | 
			
		||||
  opened,
 | 
			
		||||
  close,
 | 
			
		||||
  selectDataRow,
 | 
			
		||||
  setSelectDataRow,
 | 
			
		||||
}: MProps) => {
 | 
			
		||||
  // const supportedFileTypes = ['pdf', 'xls', 'xlsx', 'docx', 'doc']
 | 
			
		||||
  // const getFileType = (fileName: string) => {
 | 
			
		||||
  //   const extension = fileName.split('.').pop()?.toLowerCase()
 | 
			
		||||
  //   return supportedFileTypes.includes(extension!) ? extension : 'default'
 | 
			
		||||
  // }
 | 
			
		||||
  const [fileDoc, setFileDoc] = useState<TDocumentFile>()
 | 
			
		||||
  const [loader, setLoader] = useState<boolean>(false)
 | 
			
		||||
 | 
			
		||||
  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 (
 | 
			
		||||
    <Modal
 | 
			
		||||
| 
						 | 
				
			
			@ -26,34 +62,48 @@ const ModalFileDocument = ({
 | 
			
		|||
        close()
 | 
			
		||||
        setSelectDataRow({})
 | 
			
		||||
      }}
 | 
			
		||||
      size="xl"
 | 
			
		||||
      size="65%"
 | 
			
		||||
      title={
 | 
			
		||||
        <Text fw={700} fz={'lg'}>
 | 
			
		||||
          {selectDataRow?.title}
 | 
			
		||||
          File Detail: {selectDataRow?.title}
 | 
			
		||||
        </Text>
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      <Box>
 | 
			
		||||
        {selectDataRow?.uri?.split(',').map((uriItem: string) => {
 | 
			
		||||
          let url = import.meta.env.VITE_BACKEND_URL + uriItem
 | 
			
		||||
          const extension = url.split('.').pop()?.toLowerCase()
 | 
			
		||||
 | 
			
		||||
          if (extension === 'doc' || extension === 'docx') {
 | 
			
		||||
            const docUrl = encodeURIComponent(uriItem)
 | 
			
		||||
            url = `https://view.officeapps.live.com/op/embed.aspx?src=${docUrl}`
 | 
			
		||||
      <Group justify="flex-end" mb={'md'}>
 | 
			
		||||
        <a
 | 
			
		||||
          href={`${import.meta.env.VITE_BACKEND_URL}${selectDataRow.uri}`}
 | 
			
		||||
          download="Document Download"
 | 
			
		||||
          target={
 | 
			
		||||
            getFileType(selectDataRow?.uri) === 'pdf' ? '_blank' : '_self'
 | 
			
		||||
          }
 | 
			
		||||
        >
 | 
			
		||||
          <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 (
 | 
			
		||||
            <Box>
 | 
			
		||||
              <iframe
 | 
			
		||||
                key={uriItem}
 | 
			
		||||
                src={url}
 | 
			
		||||
                style={{ width: '100%', height: '800px', border: 'none' }}
 | 
			
		||||
              />
 | 
			
		||||
            </Box>
 | 
			
		||||
          )
 | 
			
		||||
        })}
 | 
			
		||||
      </Box>
 | 
			
		||||
      <Paper withBorder>
 | 
			
		||||
        {loader ? (
 | 
			
		||||
          <Box ta={'center'} my={20}>
 | 
			
		||||
            <Loader size={40} />
 | 
			
		||||
          </Box>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Box w="100%">
 | 
			
		||||
            <FileViewer fileType={fileDoc?.fileType} filePath={fileDoc?.uri} />
 | 
			
		||||
          </Box>
 | 
			
		||||
        )}
 | 
			
		||||
      </Paper>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue