ソースを参照

no message

master
コミット
1d0526cdda
5個のファイルの変更385行の追加140行の削除
  1. +16
    -0
      src/pages/pdf/PdfFormUpAndDown/index.js
  2. +292
    -112
      src/pages/pdf/PdfSearchPage/PdfTable.js
  3. +71
    -28
      src/pages/pdf/PdfSearchPage/SelectTemplateWindow.js
  4. +4
    -0
      src/themes/colorConst.js
  5. +2
    -0
      src/utils/ApiPathConst.js

+ 16
- 0
src/pages/pdf/PdfFormUpAndDown/index.js ファイルの表示

@@ -3,6 +3,7 @@ import { Button, Grid } from '@mui/material';
import axios from 'axios';
import { apiPath, appURL } from "../../../auth/utils";
import {
POST_MERGE_PDF,
GET_PDF_TEMPLATE_PATH,
GET_PDF_PATH, // Still potentially used for fetching record data if needed, but not PDF content for editor
POST_UPLOAD_PDF_PATH
@@ -254,6 +255,12 @@ function PDF() {
() => setModalOpen(false) // Cancel action
);
};
const doMerge = async () => {
const response = await axios.get(`${apiPath}${POST_MERGE_PDF}`, {
responseType: 'blob', // Crucial: get the response as a binary blob
});
};

const handleBack = async () => {
// No PDF editor to clear now
@@ -310,6 +317,15 @@ function PDF() {
Cancel
</Button>
</Grid>
<Grid item sx={{ ml: { xs: 1.5, md: 1.5, lg: 1.5 }, mr: 1.5, mb: 2 }}>
<Button
variant="contained"
color="primary"
onClick={doMerge}
>
Merge PDF A B as Output
</Button>
</Grid>
</Grid>
</Grid>
</header>


+ 292
- 112
src/pages/pdf/PdfSearchPage/PdfTable.js ファイルの表示

@@ -1,14 +1,30 @@
// material-ui
import * as React from 'react';
import {apiPath} from "../../../auth/utils";
import { POST_SIG_UPLOAD1 } from "../../../utils/ApiPathConst";
import axios from 'axios';
import {
DataGrid,
GridActionsCellItem,
} from "@mui/x-data-grid";
import EditIcon from '@mui/icons-material/Edit';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import UploadFileIcon from '@mui/icons-material/UploadFile';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import FileDownloadIcon from '@mui/icons-material/FileDownload';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
Box, // for centering and layout
CircularProgress, // Import for loading indicator
} from '@mui/material';
import {useContext, useEffect} from "react";
import {useNavigate} from "react-router-dom";
import {CustomNoRowsOverlay, dateComparator, getDateString} from "../../../utils/CommonFunction";
// Note: Assuming these utility functions/components are defined elsewhere
import {CustomNoRowsOverlay, dateComparator, getDateString} from "../../../utils/CommonFunction";
import AbilityContext from "../../../components/AbilityProvider";
import {LIONER_BUTTON_THEME} from "../../../themes/colorConst";
import {ThemeProvider} from "@emotion/react";
@@ -19,6 +35,11 @@ export default function PdfTable({recordList}) {
const [rows, setRows] = React.useState(recordList);
const [rowModesModel] = React.useState({});

// State for Dialog visibility, row ID, and Loading state
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
const [currentRowId, setCurrentRowId] = React.useState(null);
const [isUploading, setIsUploading] = React.useState(false);

const navigate = useNavigate()
const ability = useContext(AbilityContext);

@@ -26,6 +47,9 @@ export default function PdfTable({recordList}) {
page: 0,
pageSize:10
});
// Ref for the hidden file input
const fileInputRef = React.useRef(null);

useEffect(() => {
setPaginationModel({page:0,pageSize:10});
@@ -36,16 +60,141 @@ export default function PdfTable({recordList}) {
navigate(`/pdf/maintain/${id}`);
};

const handleFormUpDownClick = (id) => () => {
navigate(`/pdf/form-up-down/${id}`);
// Opens the upload dialog and sets the current row ID
const handleUploadClick = (id) => () => {
setCurrentRowId(id);
setIsDialogOpen(true);
};

const handleUpload2Click = (id, templateName) => () => {
// Placeholder for Upload 2
console.log(`Uploading for row ID ${id} (Upload 2)`);
setCurrentRowId(id);
setIsDialogOpen(true);
};

const handleDownloadClick = (id) => () => {
// 1. Construct the download URL with the ID query parameter
const downloadUrl = `${apiPath}/pdf/download-ff/${id}`;

// Use axios to fetch the PDF as a Blob
axios.get(downloadUrl, {
responseType: 'blob', // IMPORTANT: Tells axios to handle the response as binary data
})
.then((response) => {
// 2. Extract Filename from Content-Disposition Header
const contentDisposition = response.headers['content-disposition'];
let filename = `document-${id}.pdf`; // Fallback filename

if (contentDisposition) {
// Regex to find filename="name.pdf" or filename*=UTF-8''name.pdf
// The server should be setting the filename header correctly.
const filenameMatch = contentDisposition.match(/filename\*?=['"]?([^'"]+)/);
if (filenameMatch && filenameMatch[1]) {
// Decode URI component and remove extra quotes
filename = decodeURIComponent(filenameMatch[1].replace(/\\"/g, ''));
}
}

// 3. Create a temporary anchor tag (<a>) to trigger the download
const blob = new Blob([response.data], { type: 'application/pdf' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
// 4. Clean up
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
})
.catch((error) => {
console.error(`Download failed for ID ${id}:`, error);
// Handle error response (e.g., if the backend returns a 404 or a JSON error)
alert('Failed to download the PDF file. Check server logs.');
});
};

const handleCloseDialog = () => {
setIsDialogOpen(false);
setCurrentRowId(null);
if (fileInputRef.current) {
fileInputRef.current.value = ""; // Clear the file input
}
};
// Function to handle file selection and API submission
const handleFileChange = async (event) => {
const file = event.target.files[0];
if (!file) return;

if (file.type !== "application/pdf") {
alert("Please select a PDF file.");
event.target.value = "";
return;
}

const uploadUrl = apiPath + '/pdf/upload1';

// 1. Create FormData
const formData = new FormData();
formData.append('file', file);
formData.append('refId', currentRowId);
formData.append('refType', "upload1");
setIsUploading(true);

try {
axios.post(`${apiPath}${POST_SIG_UPLOAD1}`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
}
}
)
.then((response) => {
if (response.status === 200) {
// Optional: Read response if the server returns data
alert('Upload success');
console.log(`PDF file ${file.name} successfully uploaded for record ID: ${currentRowId}!`);

}
setIsUploading(false);
})
.catch((error) => {
setIsUploading(false);
console.log(error);
return false;
});

} catch (error) {
console.error('Upload error:', error);
alert(`Error uploading file: ${error.message}`);
} finally {
// 3. Cleanup and close
setIsUploading(false);
handleCloseDialog();
}
};

const handleChooseFile = () => {
// Trigger the hidden file input click
if (fileInputRef.current) {
fileInputRef.current.click();
}
};
const columns = [
{
field: 'actions',
type: 'actions',
headerName: 'Actions',
// flex: 0.5,
headerName: 'Edit',
width: 100,
cellClassName: 'actions',
getActions: ({id}) => {
@@ -57,28 +206,12 @@ export default function PdfTable({recordList}) {
className="textPrimary"
onClick={handleEditClick(id)}
color="edit"
// disabled={'true'}
// disabled={!ability.can('VIEW','DASHBOARD')}
/>
</ThemeProvider>
]
},
},
// {
// id: 'title',
// field: 'title',
// headerName: 'Title',
// // sortComparator: dateComparator,
// flex: 0.75,
// renderCell: (params) => (
// <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', whiteSpace: 'normal', wordBreak: 'break-word'}}>
// {params.value}
// </div>
// // <div>
// // {getDateString(params.row.pdfFrom,false)}
// // </div>
// ),
// },
{
id: 'templateName',
field: 'templateName',
@@ -86,11 +219,85 @@ export default function PdfTable({recordList}) {
flex: 2,
renderCell: (params) => {
return (
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', whiteSpace: 'normal', wordBreak: 'break-word'}}>
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', whiteSpace: 'normal', wordBreak: 'break-word'}}>
{params.value} {params.row.vNum}
</div>
</div>
);
}
}
},
{
field: 'upload1',
type: 'actions',
// Multi-line header
headerName: (
<div>
Upload Sig.
</div>
),
width: 100,
cellClassName: 'actions',
getActions: ({ id, row }) => {
// Check if a file ID exists to determine if a file is present
const isUploaded = !!row.upload1FileId;
// Determine the icon and label based on upload status
const upload1Icon = isUploaded
? <CheckCircleIcon sx={{fontSize: 25, color: 'success.main'}} /> // Green tick if uploaded
: <UploadFileIcon sx={{fontSize: 25}}/>; // Upload icon if not uploaded
const upload1Label = isUploaded ? "Update Signature" : "Upload Signature";

// Define the actions
const actions = [
<ThemeProvider key="UploadSign1" theme={LIONER_BUTTON_THEME}>
<GridActionsCellItem
icon={upload1Icon} // Use the dynamic icon
label={upload1Label} // Use the dynamic label
className="textPrimary"
onClick={handleUploadClick(id)}
color="upload" // Use 'upload' color which will apply to the button
/>
</ThemeProvider>
];

// Conditional rendering logic for Upload 2
if (row.templateName === "MLB03S" || row.templateName === "SLGII" || row.templateName === "SLAPP") {
actions.push(
<ThemeProvider key="UploadSign2" theme={LIONER_BUTTON_THEME}>
<GridActionsCellItem
icon={<UploadFileIcon sx={{fontSize: 25}}/>}
label="Upload2"
className="textPrimary"
onClick={handleUpload2Click(id, row.templateName)}
color="upload"
/>
</ThemeProvider>
);
}

return actions;
},
},
{
field: 'actions2',
type: 'actions',
headerName: 'Download',
width: 100,
cellClassName: 'actions',
getActions: ({id}) => {
return [
<ThemeProvider key="DownloadFile" theme={LIONER_BUTTON_THEME}>
<GridActionsCellItem
icon={<FileDownloadIcon sx={{fontSize: 25}}/>}
label="Download"
className="textPrimary"
onClick={handleDownloadClick(id, "{params.row.templateName}")}
color="download"
/>
</ThemeProvider>
]
},
},
{
id: 'createDate',
@@ -129,89 +336,6 @@ export default function PdfTable({recordList}) {
</div>
),
},
// {
// id: 'lastname',
// field: 'lastname',
// headerName: 'Last Name',
// flex: 1.5,
// sortComparator: dateComparator,
// renderCell: (params) => (
// <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', whiteSpace: 'normal', wordBreak: 'break-word'}}>
// {params.value}
// </div>
// // <div>
// // {getDateString(params.row.startDate,false)}
// // </div>
// ),
// },
// {
// id: 'firstname',
// field: 'firstname',
// headerName: 'First Name',
// // sortComparator: dateComparator,
// flex: 2,
// renderCell: (params) => (
// <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', whiteSpace: 'normal', wordBreak: 'break-word'}}>
// {params.value}
// </div>
// // <div>
// // {getDateString(params.row.applicationDeadline,false)}
// // </div>
// ),
// },
// {
// id: 'email',
// field: 'email',
// headerName: 'Email',
// flex: 1.5,
// renderCell: (params) => {
// return (
// <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', whiteSpace: 'normal', wordBreak: 'break-word'}}>
// {params.value}
// </div>
// );
// }
// },
// {
// id: 'phone1',
// field: 'phone1',
// headerName: 'Phone No.',
// flex: 1,
// renderCell: (params) => {
// return (
// <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', whiteSpace: 'normal', wordBreak: 'break-word'}}>
// {params.value}
// </div>
// );
// }
// },
// {
// id: 'phone2',
// field: 'phone2',
// headerName: '2nd Phone No.',
// flex: 1,
// renderCell: (params) => {
// return (
// <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', whiteSpace: 'normal', wordBreak: 'break-word'}}>
// {params.value}
// </div>
// );
// }
// },
// {
// id: 'remarks',
// field: 'remarks',
// headerName: 'Remarks',
// flex: 2,
// renderCell: (params) => {
// return (
// <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', whiteSpace: 'normal', wordBreak: 'break-word'}}>
// {params.value}
// </div>
// );
// }
// },

];

return (
@@ -219,9 +343,9 @@ export default function PdfTable({recordList}) {
<DataGrid
rows={rows}
columns={columns}
columnHeaderHeight={45}
// Increased height to accommodate the multi-line header
columnHeaderHeight={70}
editMode="row"
//autoPageSize
rowModesModel={rowModesModel}
getRowHeight={() => 'auto'}
paginationModel={paginationModel}
@@ -234,6 +358,62 @@ export default function PdfTable({recordList}) {
pageSizeOptions={[10]}
autoHeight
/>
{/* The Upload Dialog Box */}
<Dialog
open={isDialogOpen}
onClose={handleCloseDialog}
fullWidth
maxWidth="sm"
// Prevent closing when upload is active
disableEscapeKeyDown={isUploading}
TransitionProps={{ onExited: handleCloseDialog }}
>
<DialogTitle>
Upload Signature Page (1-Page PDF)
</DialogTitle>
<DialogContent dividers>
<Typography gutterBottom>
Please select the single-page PDF file containing the signature for record ID: **{currentRowId}**.
</Typography>
<Box sx={{ mt: 2, textAlign: 'center' }}>
{/* Button to trigger file selection */}
<Button
variant="contained"
color="primary"
startIcon={isUploading ? <CircularProgress size={20} color="inherit" /> : <UploadFileIcon />}
onClick={handleChooseFile}
disabled={isUploading}
>
{isUploading ? 'Uploading...' : 'Choose PDF File'}
</Button>

{/* Hidden File Input */}
<input
ref={fileInputRef}
type="file"
accept="application/pdf"
onChange={handleFileChange}
style={{ display: 'none' }}
disabled={isUploading}
/>
{/* Display the selected file name if a file is chosen */}
{fileInputRef.current?.files[0] && (
<Typography variant="body2" sx={{ mt: 1 }}>
Selected: **{fileInputRef.current.files[0].name}**
</Typography>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog} color="inherit" disabled={isUploading}>
Cancel
</Button>
</DialogActions>
</Dialog>
</div>
);
}
}

+ 71
- 28
src/pages/pdf/PdfSearchPage/SelectTemplateWindow.js ファイルの表示

@@ -21,6 +21,8 @@ export function SelectTemplateWindow({ isWindowOpen, onNormalClose, onConfirmClo
const [templateId, setTemplateId] = React.useState(0)
const [templateList, setTemplateList] = React.useState([])
const fileInputRef = useRef(null);
const [currentView, setCurrentView] = useState('prefixSelection');
const [selectedPrefix, setSelectedPrefix] = useState(null);
const navigate = useNavigate()
@@ -30,17 +32,27 @@ export function SelectTemplateWindow({ isWindowOpen, onNormalClose, onConfirmClo

useEffect(() => {
axios.get(`${apiPath}${GET_TEMPLATE_PATH}`)
.then((response) => {
if (response.status === 200) {
const record = response.data.records;
console.log("record", record);
setTemplateList(record);
}
})
.catch(error => {
console.log(error);
return false;
});
.then((response) => {
if (response.status === 200) {
const record = response.data.records;
// Group templates by their prefix
const grouped = record.reduce((acc, template) => {
const prefix = template.name.split(' ')[0];
if (!acc[prefix]) {
acc[prefix] = [];
}
//let's test ML first, so hide all SL forms
//if(prefix != "SL")
acc[prefix].push(template);
return acc;
}, {});
setTemplateList(grouped); // Store the grouped data
}
})
.catch(error => {
console.log(error);
return false;
});
}, []);
const createNewForm = (templateId) => {
@@ -80,23 +92,54 @@ export function SelectTemplateWindow({ isWindowOpen, onNormalClose, onConfirmClo
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{templateList.length === 0 ? (
<Typography variant="lionerSize" component="span">
No available template, please add!
</Typography>
) : ( templateList.map(record => (
<div key={record.id} style={{ margin: '10px' }}>
<Button
variant="contained"
//color="create"
sx={{backgroundColor: generateColorFromKey(record.name.split(' ')[0]), color: 'white', mr: 1}}
onClick={() => createNewForm(record.id)}
>
{record.name}
</Button>
</div>
)
))}
{Object.keys(templateList).length === 0 ? (
<Typography variant="lionerSize" component="span">
No available template, please add!
</Typography>
) : (
<>
{currentView === 'prefixSelection' ? (
// Step 1: Show the prefix selection buttons
Object.keys(templateList).map(prefix => (
<div key={prefix} style={{ margin: '10px' }}>
<Button
variant="contained"
sx={{ backgroundColor: generateColorFromKey(prefix), color: 'white', mr: 1 }}
onClick={() => {
setSelectedPrefix(prefix);
setCurrentView('templateList');
}}
>
{prefix}
</Button>
</div>
))
) : (
// Step 2: Show the templates for the selected prefix
<>
<Button onClick={() => setCurrentView('prefixSelection')}>
<Typography variant="lionerSize" component="span">
&lt; Back to Prefixes
</Typography>
</Button>
<Typography variant="lionerBold" sx={{ml: 2}}>
Select a {selectedPrefix} Template
</Typography>
{templateList[selectedPrefix].map(record => (
<div key={record.id} style={{ margin: '10px' }}>
<Button
variant="contained"
sx={{ backgroundColor: generateColorFromKey(record.name.split(' ')[0]), color: 'white', mr: 1 }}
onClick={() => createNewForm(record.id)}
>
{record.name}
</Button>
</div>
))}
</>
)}
</>
)}
</DialogContentText>
</DialogContent>
<DialogActions>


+ 4
- 0
src/themes/colorConst.js ファイルの表示

@@ -86,6 +86,10 @@ export const LIONER_BUTTON_THEME = createTheme({
main: '#F3AF2B',
contrastText: '#FFFFFF',
},
upload:{
main: '#6A8B9E',
contrastText: '#FFFFFF',
},
exportExcel:{
main: '#6A8B9E',
contrastText: '#FFFFFF',


+ 2
- 0
src/utils/ApiPathConst.js ファイルの表示

@@ -68,6 +68,8 @@ export const GET_PDF_PATH = "/pdf"
export const POST_PDF_PATH = "/pdf/save"
export const POST_UPLOAD_PDF_PATH = "/pdf2/upload"
export const GET_PDF_TEMPLATE_PATH = "/pdf/template"
export const POST_MERGE_PDF = "/pdf/merge-pdf"
export const POST_SIG_UPLOAD1 = "/pdf/upload1"
export const GET_CLIENT_PATH = "/client"
export const POST_CLIENT_PATH = "/client/save"
export const GET_EVENT_PATH = "/event"


読み込み中…
キャンセル
保存