note history, work order images

This commit is contained in:
npalOmega 2025-04-01 14:07:58 -05:00
parent 3f3dea2a5f
commit 24896b8acb
12 changed files with 430 additions and 19 deletions

View file

@ -9,6 +9,13 @@ const STANDARD_HEADERS = {
Authorization: getAccessToken()
};
const STANDARD_HEADERS_FORM = {
accept: "application/json",
pragma: "no-cache",
"cache-control": "no-cache",
Authorization: getAccessToken(),
};
export const get = (url) =>
fetch(url, {
headers: { ...STANDARD_HEADERS, Authorization: getAccessToken() },
@ -30,6 +37,26 @@ export const post = (url, payload) =>
throw new Error(ex.message || API_ERROR_MESSAGE);
});
export const postFormBody = (url, payload) => {
const formData = new FormData();
for (const [key, value] of Object.entries(payload)) {
formData.append(key, value);
}
return fetch(url, {
method: "POST",
headers: {
...STANDARD_HEADERS_FORM,
},
body: formData,
})
.then(handleHttpResponse)
.catch((ex) => {
throw new Error(ex.message || API_ERROR_MESSAGE);
});
};
export const put = (url, payload) =>
fetch(url, {
headers: STANDARD_HEADERS,
@ -41,6 +68,25 @@ export const put = (url, payload) =>
throw new Error(ex.message || API_ERROR_MESSAGE);
});
export const putFormBody = (url, payload) => {
const formData = new FormData();
for (const [key, value] of Object.entries(payload)) {
formData.append(key, value);
}
fetch(url, {
method: "PUT",
headers: {
...STANDARD_HEADERS_FORM,
},
body: formData,
})
.then(handleHttpResponse)
.catch((ex) => {
throw new Error(ex.message || API_ERROR_MESSAGE);
});
}
export const deleteItem = (url, payload) =>
fetch(url, {
headers: STANDARD_HEADERS,

View file

@ -1,4 +1,4 @@
import * as React from 'react';
import React, { useEffect } from 'react';
import { styled, useTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Drawer from '@mui/material/Drawer';
@ -70,6 +70,15 @@ const DrawerHeader = styled('div')(({ theme }) => ({
export default function PersistentDrawerLeft({ handleDrawerClose, open}) {
const location = useLocation();
const theme = useTheme();
useEffect(() => {
document.addEventListener('mousedown', handleDrawerClose);
return () => {
document.removeEventListener('mousedown', handleDrawerClose);
};
}, []);
return (
<Drawer
sx={{

View file

@ -6,10 +6,7 @@ export const initialValues =
"po": "",
"tt": "",
"assignTo": "",
"dueDate": null,
"priority": "",
"locationId": null,
"istemplate": null,
"attachments": [],
"beforPhotoIssue": "",
"beforPhotoAttachment": null,

View file

@ -0,0 +1,144 @@
import React, { useState, useEffect } from 'react';
import {
Button,
Drawer,
Typography,
Grid,
Paper,
IconButton,
FormControl,
Input,
FormHelperText,
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import {getAttachments, AddEditAttachments} from './api';
const ImageDrawer = ({ item, open, setOpen }) => {
const [imagePaths, setImagePaths] = useState([]);
const [selectedFiles, setSelectedFiles] = useState([]);
const [errors, setErrors] = useState({ file: null });
const toggleDrawer = (open) => () => {
setOpen(open);
};
// Handle file upload
const handleFileChange = (event) => {
const files = Array.from(event.target.files);
const fileURLs = files.map(file => URL.createObjectURL(file));
setImagePaths((prev) => [...prev, ...fileURLs]);
setSelectedFiles((prev) => [...prev, ...files]);
setErrors({ file: null }); // Clear errors
};
// Handle delete image
const handleDeleteImage = (image, index) => {
setImagePaths((prev) => prev.filter((img, idx) => img !== image));
setSelectedFiles((prev) => prev.filter((_, idx) => idx !== index)); // Ensure file array is in sync
};
const getImageName = (file) => {
if(!file) return "";
const name = file.name;
if(name.indexOf('_') > 0) {
const parts = name.split('_');
return parts[parts.length - 1].split('.')[0];
} else {
return name;
}
};
const handUpload = async () => {
};
const handleSubmit = async () => {
if (selectedFiles) {
await AddEditAttachments({id: item.id , attachments: selectedFiles[0], imagePaths:imagePaths});
if(item.id) {
const attachments = await getAttachments(item.id);
if(attachments && attachments.length>0) {
const _attachments = attachments.map(c=> c.attachments)
setImagePaths(_attachments);
}
}
}
};
useEffect(async() => {
if(item.id) {
const attachments = await getAttachments(item.id);
if(attachments && attachments.length>0) {
const _attachments = attachments.map(c=> c.attachments)
setImagePaths(_attachments);
}
}
}, [item])
return (
<div>
<Drawer anchor="right" open={open} onClose={toggleDrawer(false)}>
<div style={{ width: 400, padding: 16 }}>
<Typography variant="h6" gutterBottom>
Workorder Images
</Typography>
<FormControl fullWidth margin="normal" error={!!errors.file}>
<Typography variant="subtitle1">Upload Images</Typography>
<Input
type="file"
inputProps={{ accept: "image/*", multiple: true }} // Allow multiple files
onChange={handleFileChange}
style={{ display: 'none' }}
id="file-input"
/>
<label htmlFor="file-input">
<Button
variant="contained"
component="span"
onClick={handUpload}
>
{selectedFiles.length > 0 ? `${selectedFiles.length} File(s) Selected` : 'Choose Files'}
</Button>
</label>
<Button
variant="contained"
color="primary"
onClick={handleSubmit}
style={{ marginTop: 10 }}
>
Submit
</Button>
{errors.file && <FormHelperText>{errors.file}</FormHelperText>}
</FormControl>
<Grid container spacing={2}>
{imagePaths.map((image, index) => (
<Grid item xs={6} key={index}> {/* Set to 6 to allow 2 items per row */}
<Paper elevation={2} style={{ padding: '8px' }}>
<img
src={image}
alt={`Image ${index}`}
style={{ width: '100%', height: 'auto', borderRadius: '4px' }}
/>
<div style={{ display: 'flex', alignItems: 'center', marginTop: '8px' }}>
<Typography variant="body2" style={{ flexGrow: 1 }}>
{getImageName(selectedFiles[index])} {/* Display the extracted image name */}
</Typography>
<IconButton
size="small"
onClick={() => handleDeleteImage(image, index)}
aria-label="delete"
>
<DeleteIcon fontSize="small" />
</IconButton>
</div>
</Paper>
</Grid>
))}
</Grid>
</div>
</Drawer>
</div>
);
};
export default ImageDrawer;

View file

@ -0,0 +1,6 @@
import { postFormBody, get } from '../../../apiUtil';
import { API_URL } from '../../../constants';
export const AddEditAttachments = (item) => postFormBody(`${API_URL}/WorkOrder/AddEditAttachments`, item);
export const getAttachments = (id) => get(`${API_URL}/WorkOrder/GetAttachmentsByWorkorderId?woid=${id}`);

View file

@ -20,6 +20,7 @@ import { formValidation } from "../_utility";
import SubmitButtonWithLoader from "../../../components/SubmitButtonWithLoader";
import { form_text_builder, API_SUCCESS_MESSAGE } from "../../../constants";
import { getContacts, getCategories, getLocations, getUsers } from '../../contacts/list/api';
import moment from 'moment';
const ItemForm = ({
item,
mode,
@ -38,6 +39,19 @@ const ItemForm = ({
{ text: "Sev 4", value: "Sev 4" },
{ text: "Sev 5", value: "Sev 5" }
]);
const [workOrderTypes, setWorkOrderTypes] = useState([
{ text: "PM", value: "PM" },
{ text: "Reactive", value: "Reactive" },
{ text: "Project", value: "Project" },
]);
const workOrderTitles = [
"GENERIC PLUMBING SYSTEMS – ANNUAL",
"GENERIC TANK/TANKLESS WATER HEATER ANNUAL",
"BACKFLOW PREVENTER ANNUAL PM",
"GENERIC PUMPING SYSTEMS - ANNUAL",
"ANNUAL PRESSURE REDUCING VALVE INSPECTION",
"PLUMBING JANITORIAL SCRUBBER PIT CLEANOUT 13W",
];
const [locations, setLocations] = useState([]);
const [users, setUsers] = useState([]);
const [categories, setCategories] = useState([]);
@ -75,13 +89,7 @@ const ItemForm = ({
const handleClose = () => {
setOpen({ open: false });
};
const handlelineItemIncrements = () => {
setValues((values) => ({
...values,
...{lineitems:[...values.lineitems||[], {descriptions:'', lineitem:0}]},
}));
};
useEffect(() => {
getUsers().then((response) => { handleSuccess(response, setUsers) });
getContacts().then((response) => { handleSuccess(response, setContacts) });
@ -127,10 +135,28 @@ const ItemForm = ({
error={!!errors.workerOrderNumber}
helperText={errors.workerOrderNumber}
/>
<InputLabel id="PO-label">Work Order Title:</InputLabel>
</div>
<div>
<Select
labelId="work-order-title-select-label"
id="work-order-title-select"
fullWidth
value={values.workOrderTitle || ''}
onChange={(event) => {
const { value } = event.target;
handleChangeKV("workerOrderTitle", value);
}}
label="Work Order Title"
>
{workOrderTitles.map((title, index) => (
<MenuItem value={title} key={index}>
{title}
</MenuItem>
))}
</Select>
<InputLabel id="PO-label">Work Order Title:</InputLabel>
<TextField
id="WorkerOrderTitle"
label="Work Order Title:"
variant="outlined"
style={{ width: '100%' }}
margin="normal"
@ -213,7 +239,7 @@ const ItemForm = ({
required
name="dueDate"
type="date"
value={values.dueDate}
defaultValue={ moment(values.formattedDueDate).format("YYYY-MM-DD")}
onChange={(event) => {
const { value } = event.target;
handleChangeKV("dueDate", value);
@ -282,6 +308,26 @@ const ItemForm = ({
))}
</Select>
</div>
<div>
<InputLabel id="work-order-type-select-label">Work Order Type:*</InputLabel>
<Select
labelId="work-order-type-select-label"
id="work-order-type-select"
value={values.workOrderType}
fullWidth
onChange={(event) => {
const { value } = event.target;
handleChangeKV("workOrderType", value);
}}
label="Work Order Type"
>
{workOrderTypes.map((type, index) => (
<MenuItem value={type.value} key={index}>
{type.text}
</MenuItem>
))}
</Select>
</div>
<div>
<InputLabel id="Wo-select-label">Location:*</InputLabel>
<Select

View file

@ -3,10 +3,12 @@ import MUIDataTable from "mui-datatables";
import IconButton from "@mui/material/IconButton";
import EditOutlined from "@mui/icons-material/EditOutlined";
import DeleteIcon from "@mui/icons-material/Delete";
import SpeakerNotes from '@mui/icons-material/SpeakerNotes';
import TableStyles from './TableStyles';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import PermMediaIcon from '@mui/icons-material/PermMedia';
const TableItems = ({ items, onEditClick, onDeleteClick }) => {
const TableItems = ({ items, onEditClick, onDeleteClick, onOpenNoteClick, onOpenAttachmentsClick }) => {
const classes = TableStyles();
const getMuiTheme = () => {
@ -153,6 +155,34 @@ const TableItems = ({ items, onEditClick, onDeleteClick }) => {
>
<DeleteIcon />
</IconButton>
<IconButton
edge="end"
aria-label="history"
className={classes.IconButton}
onClick={(e) => {
e.stopPropagation();
const id = tableMeta.rowData[0];
const _selectedItem = tableMeta.tableData
.find(obj => obj.id == id);
onOpenNoteClick({ open: true, selectedItem: _selectedItem });
}}
>
<SpeakerNotes/>
</IconButton>
<IconButton
edge="end"
aria-label="attachments"
className={classes.IconButton}
onClick={(e) => {
e.stopPropagation();
const id = tableMeta.rowData[0];
const _selectedItem = tableMeta.tableData
.find(obj => obj.id == id);
onOpenAttachmentsClick({ open: true, selectedItem: _selectedItem });
}}
>
<PermMediaIcon/>
</IconButton>
</>
);
}

View file

@ -1,4 +1,4 @@
import { post } from '../../../apiUtil';
import { postFormBody } from '../../../apiUtil';
import { API_URL } from '../../../constants';
export const editItems = (item) => post(`${API_URL}/WorkOrder/EditWorkorder`, item);
export const editItems = (item) => postFormBody(`${API_URL}/WorkOrder/EditWorkorder`, item);

View file

@ -6,11 +6,13 @@ import Banner from "../../../components/Banner";
import AddModal from "../new";
import EditModal from "../edit";
import DeleteModal from "../delete";
import NoteHistoryDrawer from '../noteHistory/NoteHistoryDrawer'
import useItems from "./useItems";
import { CircularProgress, Box } from "@mui/material";
import Alert from "@mui/material/Alert";
import Stack from "@mui/material/Stack";
import { getUser } from '../../../tokenUtility'
import ImageDrawer from '../attachments/ImageDrawer';
const List = () => {
const [formModalopen, setFormModalopen] = React.useState({
@ -21,6 +23,14 @@ const List = () => {
selectedItem: {},
open: false,
});
const [noteHistoryModalopen, setNoteHistoryModalopen] = React.useState({
selectedItem: {},
open: false,
});
const [attachmentsModalopen, setAttachmentsModalopen] = React.useState({
selectedItem: {},
open: false,
});
const [apiStatus, setApiStatus] = React.useState({
show: false,
status: "",
@ -67,6 +77,8 @@ const List = () => {
items={items}
onEditClick={setFormModalopen}
onDeleteClick={setDeleteModalopen}
onOpenNoteClick = {setNoteHistoryModalopen}
onOpenAttachmentsClick = {setAttachmentsModalopen}
isEmpty={isEmptyList}
/>
<AddModal
@ -76,12 +88,24 @@ const List = () => {
setApiStatus={setApiStatus}
/>
<EditModal
userItem={{ ...formModalopen.selectedItem }}
item={{ ...formModalopen.selectedItem }}
open={formModalopen.open}
setOpen={setFormModalopen}
isEdit={isEdit}
setApiStatus={setApiStatus}
/>
<NoteHistoryDrawer
item={{ ...noteHistoryModalopen.selectedItem }}
open={noteHistoryModalopen.open}
setOpen={setNoteHistoryModalopen}
setApiStatus={setApiStatus}
/>
<ImageDrawer
item={{ ...attachmentsModalopen.selectedItem }}
open={attachmentsModalopen.open}
setOpen={setAttachmentsModalopen}
setApiStatus={setApiStatus}
/>
<DeleteModal
item={{ ...deleteModalopen.selectedItem }}
open={deleteModalopen.open}

View file

@ -1,4 +1,4 @@
import { post } from '../../../apiUtil';
import { postFormBody } from '../../../apiUtil';
import { API_URL } from '../../../constants';
export const addItems = (item) => post(`${API_URL}/WorkOrder/AddWorkorder`, item);
export const addItems = (item) => postFormBody(`${API_URL}/WorkOrder/AddWorkorder`, item);

View file

@ -0,0 +1,103 @@
import React, { useEffect, useState } from 'react';
import {
Drawer,
Button,
TextField,
List,
ListItem,
ListItemText,
Typography,
IconButton,
Paper,
} from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material';
import {getComments, addComments} from './api';
const NoteHistoryDrawer = ({ item, open, setOpen, setApiStatus }) => {
const [note, setNote] = useState('');
const [noteHistory, setNoteHistory] = useState([]);
const [comments, setComments] = useState([]);
const handleSubmit = async () => {
if (note.trim()) {
await addComments({id: item.id , Comments: [...noteHistory, note]});
setNoteHistory([]);
setNote('');
setOpen(false);
}
};
const toggleDrawer = (open) => (event) => {
if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) {
return;
}
setOpen(open);
};
useEffect(async() => {
if(item.id) {
const comments = await getComments(item.id);
if(comments && comments.length>0) {
const _comments = comments.map(c=> c.commenttext)
setComments(_comments);
}
}
}, [item])
return (
<div>
<Button variant="outlined" onClick={toggleDrawer(true)}>
Open Note History
</Button>
<Drawer anchor="right" open={open} onClose={toggleDrawer(false)}>
<div style={{ padding: 20, width: 300 }}>
<IconButton onClick={toggleDrawer(false)}>
<CloseIcon />
</IconButton>
<Typography variant="h6">Note History</Typography>
<TextField
label="Add a note"
variant="outlined"
fullWidth
margin="normal"
value={note}
onChange={(e) => setNote(e.target.value)}
/>
<Button
variant="contained"
color="primary"
onClick={handleSubmit}
style={{ marginTop: 10 }}
>
Submit
</Button>
<Typography variant="subtitle1" style={{ marginTop: 20 }}>
Submitted Notes:
</Typography>
<div>
<List>
{[...comments, ...noteHistory].map((n, index) => (
<ListItem
key={index}
style={{
backgroundColor: '#f0f0f0',
marginBottom: 5,
borderRadius: 4,
transition: 'background-color 0.3s',
'&:hover': {
backgroundColor: '#e0e0e0',
},
}}
>
<ListItemText primary={n} />
</ListItem>
))}
</List>
</div>
</div>
</Drawer>
</div>
);
};
export default NoteHistoryDrawer;

View file

@ -0,0 +1,6 @@
import { post, get } from '../../../apiUtil';
import { API_URL } from '../../../constants';
export const addComments = (item) => post(`${API_URL}/WorkOrder/AddComments`, item);
export const getComments = (id) => get(`${API_URL}/WorkOrder/GetCommentsByWorkorderId?woid=${id}`);