diff --git a/backend/src/controllers/taskController.js b/backend/src/controllers/taskController.js index b72de45b..f3f4f6b1 100644 --- a/backend/src/controllers/taskController.js +++ b/backend/src/controllers/taskController.js @@ -8,8 +8,7 @@ export default class TaskController { return new Promise((resolve, reject) => { if ( !req.body.task_title || - !req.body.task_state || - !req.body.task_description + !req.body.task_state ) { return reject({ error: "Error with request body." }); } @@ -90,6 +89,23 @@ export default class TaskController { }); } + updateTaskProject(req) { + return new Promise((resolve, reject) => { + const TaskModel = new Task(); + TaskModel.updateTaskProject( + req.params.taskId, + req.body.project_id, + (err, result) => { + if (err) { + reject({ error: err }); + } + this.isTaskLoaded = false; + resolve(result); + } + ); + }); + } + //add subtask given taskID saveSubtaskByTask(req) { return new Promise((resolve, reject) => { diff --git a/backend/src/models/task.js b/backend/src/models/task.js index 412c1344..f863caa5 100644 --- a/backend/src/models/task.js +++ b/backend/src/models/task.js @@ -90,7 +90,24 @@ export class Task { result(error, null); } else { result(null, { - result: `Task of ${taskId} Saved Successfully for Task ID: ${taskDescription}`, + result: `Task of ${taskId} Saved Successfully for Task ID: ${taskId}`, + }); + } + } + ); + } + + updateTaskProject(taskId, taskProject, result) { + con.query( + "CALL update_task_project(?, ?)", + [taskId, taskProject], + function (error, _) { + if (error) { + console.log("error: ", error); + result(error, null); + } else { + result(null, { + result: `Task of ${taskId} Saved Successfully for Task ID: ${taskId}`, }); } } diff --git a/backend/src/routes/taskRoute.js b/backend/src/routes/taskRoute.js index 31089518..7c1eb3db 100644 --- a/backend/src/routes/taskRoute.js +++ b/backend/src/routes/taskRoute.js @@ -55,6 +55,17 @@ router.post("/description/:taskId", authorize(), (req, res) => { }); }); +router.post("/project/:taskId", authorize(), (req, res) => { + taskController + .updateTaskProject(req) + .then((response) => { + res.status(200).json(response); + }) + .catch((err) => { + res.status(404).json(err); + }); +}); + //add subtask given task_id router.post("/addsubtask/:taskId", authorize(), (req, res) => { if (!req.body) { diff --git a/backend/src/routes/userRoute.js b/backend/src/routes/userRoute.js index 2fc61fcb..981e3ecb 100644 --- a/backend/src/routes/userRoute.js +++ b/backend/src/routes/userRoute.js @@ -159,6 +159,18 @@ router.post("/resetpassword", (req, res) => { } }); +router.post("/adminReset", authorize(true), (req, res) => { + // Reset Password + userController + .updateUserPassword(req, req.body.email) + .then((response) => { + res.status(200).json(response); + }) + .catch((err) => { + res.status(404).json(err); + }); +}); + router.delete("/:userId", authorize(), (req, res) => { userController .deleteUser(req.params.userId) diff --git a/database/procedures/billableProc.sql b/database/procedures/billableProc.sql index e68799ed..060bddde 100644 --- a/database/procedures/billableProc.sql +++ b/database/procedures/billableProc.sql @@ -76,9 +76,11 @@ END $$ CREATE PROCEDURE `load_billable` () BEGIN - SELECT billable.*, t.task_id, st.fk_task_id, st.subtask_id, t.task_state, st.subtask_state FROM billable + SELECT billable.*, u.username, o.organization_name, t.task_id, st.fk_task_id, st.subtask_id, t.task_state, st.subtask_state FROM billable LEFT JOIN tasks t on t.task_uuid = billable.task_uuid LEFT JOIN subtasks st on st.subtask_uuid = billable.task_uuid + LEFT JOIN users u on u.user_id = billable.created_by + LEFT JOIN organizations o on o.organization_id = u.fk_organization_id WHERE billable.billed = 0 AND (t.task_state != "archived" OR t.task_state IS NULL) AND (st.subtask_state != "archived" OR st.subtask_state IS NULL); @@ -98,11 +100,13 @@ CREATE PROCEDURE `load_billable_with_filter` ( ) BEGIN - SELECT billable.*, t.task_id, st.fk_task_id, st.subtask_id, t.task_state, st.subtask_state FROM billable + SELECT billable.*, u.username, o.organization_name, t.task_id, st.fk_task_id, st.subtask_id, t.task_state, st.subtask_state FROM billable LEFT JOIN tasks t on t.task_uuid = billable.task_uuid LEFT JOIN subtasks st on st.subtask_uuid = billable.task_uuid LEFT JOIN costcenter_assignments ca on ca.fk_project_id = billable.fk_project_id LEFT JOIN project_assignments pa on pa.fk_project_id = billable.fk_project_id + LEFT JOIN users u on u.user_id = billable.created_by + LEFT JOIN organizations o on o.organization_id = u.fk_organization_id WHERE (name = _service_name OR _service_name IS NULL OR _service_name = '') AND (ca.fk_cost_center_id = _costcenter_id OR _costcenter_id IS NULL OR _costcenter_id = '') AND (billable.fk_project_id = _project_id OR _project_id IS NULL OR _project_id = '') diff --git a/database/procedures/ticketManagement/saveProcedures.sql b/database/procedures/ticketManagement/saveProcedures.sql index b5a65f48..d75703b6 100644 --- a/database/procedures/ticketManagement/saveProcedures.sql +++ b/database/procedures/ticketManagement/saveProcedures.sql @@ -5,6 +5,7 @@ DROP procedure IF EXISTS `save_subtask`; DROP procedure IF EXISTS `update_task_status`; DROP procedure IF EXISTS `update_task_title`; DROP procedure IF EXISTS `update_task_description`; +DROP procedure IF EXISTS `update_task_project`; DROP procedure IF EXISTS `update_subtask_status`; DELIMITER $$ @@ -74,6 +75,16 @@ UPDATE `subtasks` SET `subtask_description`=`_task_description`, `subtask_update END $$ +CREATE PROCEDURE `update_task_project` ( + IN `_task_uuid` VARCHAR(50), + IN `_task_project` VARCHAR(250) + +) BEGIN +UPDATE `tasks` SET `fk_project_id`=`_task_project`, `task_updated` = NOW() WHERE `task_uuid`=`_task_uuid`; +UPDATE `billable` SET `fk_project_id`=`_task_project` WHERE `task_uuid`=`_task_uuid`; + +END $$ + CREATE PROCEDURE `save_subtask` ( IN `_subtask_uuid` VARCHAR(50), IN `_subtask_title` VARCHAR(100), diff --git a/frontend/.example.env b/frontend/.example.env index 79fc323b..5703a83a 100644 --- a/frontend/.example.env +++ b/frontend/.example.env @@ -1,4 +1,6 @@ REACT_APP_S3_ACCESS_KEY_ID = REPLACE_VALUE_WITH_NO_QUOTATIONS REACT_APP_S3_SECRET_ACCESS_KEY = REPLACE_VALUE_WITH_NO_QUOTATIONS REACT_APP_S3_BUCKET = REPLACE_VALUE_WITH_NO_QUOTATIONS -REACT_APP_BACKEND = REPLACE_VALUE_WITH_NO_QUOTATIONS \ No newline at end of file +REACT_APP_BACKEND = REPLACE_VALUE_WITH_NO_QUOTATIONS +REACT_APP_FRONTEND = REPLACE_VALUE_WITH_NO_QUOTATIONS +REACT_APP_SUMMARY_FORMAT = png \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index a2301a56..0e51fa5d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "date-fns": "^2.29.3", "html2canvas": "^1.4.1", "html2pdf.js": "^0.10.1", + "jspdf": "^2.5.1", "react": "^18.2.0", "react-apexcharts": "^1.4.0", "react-beautiful-dnd": "^13.1.1", diff --git a/frontend/src/components/CostEstimate/index.js b/frontend/src/components/CostEstimate/index.js index 9ffc0f4d..9be59658 100644 --- a/frontend/src/components/CostEstimate/index.js +++ b/frontend/src/components/CostEstimate/index.js @@ -46,7 +46,7 @@ export const CostEstimateFull = () => {
{formResponses.map((response) => { - if (!costEstimateMap) { + if (!costEstimateMap || costEstimateMap.length === 0) { return null; } const cost = costEstimateMap.get(response.response) ?? 0; diff --git a/frontend/src/components/GenerateInvoice/InvoiceTemplate/index.css b/frontend/src/components/GenerateInvoice/InvoiceTemplate/index.css index 9434048f..ce487bcb 100644 --- a/frontend/src/components/GenerateInvoice/InvoiceTemplate/index.css +++ b/frontend/src/components/GenerateInvoice/InvoiceTemplate/index.css @@ -1,7 +1,7 @@ #invoice-template { color: #000; margin: 10px auto; - border: 1px solid #ddd; + padding: 10px; } #invoice-header { font-size: 12px; diff --git a/frontend/src/components/GenerateInvoice/InvoiceTemplate/index.js b/frontend/src/components/GenerateInvoice/InvoiceTemplate/index.js index 57b8cef1..e07b7dc3 100644 --- a/frontend/src/components/GenerateInvoice/InvoiceTemplate/index.js +++ b/frontend/src/components/GenerateInvoice/InvoiceTemplate/index.js @@ -102,7 +102,7 @@ const InvoiceTemplate = ({ customer, costcenterMap, invoicNum }) => { />
-
+ {/*
{ onChange={handleChange} />
-
+
*/} @@ -147,17 +147,22 @@ function InvoiceDetails({ billingData }) { Description + SOW + User Quantity Unit Price Total(CAD) {billingData.map((invoiceItem, index) => { + console.log(invoiceItem); return ( {index + 1}. {invoiceItem.name} + {`SOW-${invoiceItem.task_id ?? (invoiceItem.subtask_id ?? "?")}`} + {invoiceItem.username} {invoiceItem.quantity} ${(invoiceItem.cost / invoiceItem.quantity).toFixed(2)} ${invoiceItem.cost} @@ -175,10 +180,6 @@ function InvoiceDetails({ billingData }) { Total Due: ${subtotal} - - Amount Paid: - $0 - diff --git a/frontend/src/components/InvoiceTable/index.js b/frontend/src/components/InvoiceTable/index.js index 7c7b6edc..f8a2b14c 100644 --- a/frontend/src/components/InvoiceTable/index.js +++ b/frontend/src/components/InvoiceTable/index.js @@ -38,6 +38,18 @@ const InvoiceTable = () => { key: "fk_project_id", editable: false, }, + { + title: "Organization", + dataIndex: "organization_name", + key: "organization_name", + editable: false, + }, + { + title: "User", + dataIndex: "username", + key: "username", + editable: false, + }, { title: "Created Date", dataIndex: "createdDate", diff --git a/frontend/src/components/ProjectSelector/ProjectSelectorEditor/index.jsx b/frontend/src/components/ProjectSelector/ProjectSelectorEditor/index.jsx index b08c485c..50aeb30e 100644 --- a/frontend/src/components/ProjectSelector/ProjectSelectorEditor/index.jsx +++ b/frontend/src/components/ProjectSelector/ProjectSelectorEditor/index.jsx @@ -26,7 +26,7 @@ function ProjectSelectorEditor({ question }) { useEffect(() => { console.log(question); setQuestionNum(`Q${question.position_index}`); - setTitle("User will select a project here..."); + setTitle(question.question ?? ""); setNote(question.question_note); }, [question]); diff --git a/frontend/src/components/ProjectSelector/index.jsx b/frontend/src/components/ProjectSelector/index.jsx index 78238376..739b4b25 100644 --- a/frontend/src/components/ProjectSelector/index.jsx +++ b/frontend/src/components/ProjectSelector/index.jsx @@ -9,13 +9,16 @@ import { REMOVE_PROJECT_RESPONSE, } from "../../redux/actions/formActions"; import { GET_PROJECT } from "../../redux/actions/billingActions"; +import { LOAD_QUESTION } from "../../redux/actions/questionActions"; function ProjectSelector({ question }) { const dispatch = useDispatch(); const projectList = useSelector((state) => state.projectReducer.projectList); const currentUser = useSelector((state) => state.userReducer.currentUser); const draftList = useSelector((state) => state.formReducer.draftList); + const formResponses = useSelector((state) => state.formReducer.formResponses); const [selectedValue, setSelectedValue] = useState(null); + const [draftDone, setDraftDone] = useState(false); const handleChange = (event) => { const selected = JSON.parse(event.target.value); @@ -34,7 +37,6 @@ function ProjectSelector({ question }) { question_info: question, }, }); - setSelectedValue(selected); const draftObj = { draft_id: question.question_id + "_" + currentUser.user_id, @@ -47,6 +49,9 @@ function ProjectSelector({ question }) { type: ADD_DRAFT, payload: draftObj, }); + setDraftDone(true); + dispatch({ type: LOAD_QUESTION, payload: question.fk_form_id }); + setSelectedValue(selected); }; useEffect(() => { @@ -63,7 +68,6 @@ function ProjectSelector({ question }) { (option) => option.project_id === draft.answer ); if (value) { - setSelectedValue(value); dispatch({ type: ADD_RESPONSE, payload: { @@ -73,10 +77,20 @@ function ProjectSelector({ question }) { question_info: question, }, }); + setSelectedValue(value); + setDraftDone(true); } } }, [dispatch, draftList, projectList, question]); + useEffect(() => { + dispatch({ type: LOAD_QUESTION, payload: question.fk_form_id }); + }, [dispatch, question.fk_form_id, draftDone]); + + useEffect(() => { + const project = formResponses.find((response) => response?.question_info?.question_type === "project"); + setSelectedValue(project?.question); + }, [formResponses, question]); return (
@@ -86,7 +100,7 @@ function ProjectSelector({ question }) {
{question.question_note}
+
{ + const password = document.getElementsByClassName("LoginInput")[0].value; + if (password && password !== "") { + dispatch({ + type: ADMIN_RESET_PASSWORD, + payload: { + email: record.email, + password: password, + }, + }); + document.getElementsByClassName("LoginInput")[0].value = ""; + setResetPasswordRecord(null); + } + }}>Save
+ + } + title="Change User's Password" + trigger="click" + open={resetPasswordRecord === record.user_id} + onOpenChange={(open) => { + setResetPasswordRecord(open ? record.user_id : null); + }} + > +
Reset Password
+ + ) : null, + width: "5%", + }, { title: "", dataIndex: "operation", @@ -111,6 +150,7 @@ const UserManagementTable = () => { ]; const dispatch = useDispatch(); + const [resetPasswordRecord, setResetPasswordRecord] = useState(); const dataSource = useSelector((state) => state.userReducer.userList); const orgList = useSelector((state) => state.userReducer.organizationList); @@ -125,6 +165,22 @@ const UserManagementTable = () => { const handleSave = (row) => { dispatch({ type: UPDATE_USER, payload: row }); }; + const handleAdd = () => { + const newID = uuid(); + const newData = { + user_id: newID, + fk_organization_id: null, + username: "New User", + email: "New User Email", + employee: false, + password: "password", + adminCreated: true, + }; + dispatch({ + type: POST_USER, + payload: newData + }); + }; const EditableContext = React.createContext(null); @@ -234,6 +290,14 @@ const UserManagementTable = () => { rowClassName={(_, index) => index % 2 === 0 ? "editable-row" : "editable-row-dark"} dataSource={dataSource} columns={renderedColumns} + footer={() => { + return ( +
+ Add +
Add
+
+ ) + }} /> ); }; diff --git a/frontend/src/constants.js b/frontend/src/constants.js index 6229c2d4..06b10591 100644 --- a/frontend/src/constants.js +++ b/frontend/src/constants.js @@ -20,6 +20,10 @@ export const backend = process.env.REACT_APP_BACKEND; export const frontend = process.env.REACT_APP_FRONTEND; // export const frontend = "http://localhost:3000/"; +export const summaryFormat = process.env.REACT_APP_SUMMARY_FORMAT; +// export const summaryFormat = "pdf"; +// export const summaryFormat = "png"; + export const awsConfig = new AWS.Config({ accessKeyId: process.env.REACT_APP_S3_ACCESS_KEY_ID, secretAccessKey: process.env.REACT_APP_S3_SECRET_ACCESS_KEY, diff --git a/frontend/src/redux/actions/ticketActions.js b/frontend/src/redux/actions/ticketActions.js index 3bad45bd..69dac4b5 100644 --- a/frontend/src/redux/actions/ticketActions.js +++ b/frontend/src/redux/actions/ticketActions.js @@ -4,6 +4,7 @@ export const SET_TICKETS = "SET_TICKETS"; export const UPDATE_TICKET_STATUS = "UPDATE_TICKET_STATUS"; export const UPDATE_TICKET_TITLE = "UPDATE_TICKET_TITLE"; export const UPDATE_TICKET_DESCRIPTION = "UPDATE_TICKET_DESCRIPTION"; +export const UPDATE_TICKET_PROJECT = "UPDATE_TICKET_PROJECT"; export const SET_ACTIVE_TICKET = "SET_ACTIVE_TICKET"; diff --git a/frontend/src/redux/actions/userActions.js b/frontend/src/redux/actions/userActions.js index 6a4e5e9e..afb16805 100644 --- a/frontend/src/redux/actions/userActions.js +++ b/frontend/src/redux/actions/userActions.js @@ -5,6 +5,7 @@ export const POST_USER = "POST_USER"; export const UPDATE_USER = "UPDATE_USER"; export const REQUEST_RESET = "REQUEST_RESET"; export const RESET_PASSWORD = "RESET_PASSWORD"; +export const ADMIN_RESET_PASSWORD = "ADMIN_RESET_PASSWORD"; export const SIGNUP_USER = "SIGNUP_USER"; export const AUTHENTICATE_USER = "AUTHENTICATE_USER"; export const SET_CURRENT_USER = "SET_CURRENT_USER"; diff --git a/frontend/src/redux/api/ticketApi.js b/frontend/src/redux/api/ticketApi.js index a0eb121a..6f5c05d2 100644 --- a/frontend/src/redux/api/ticketApi.js +++ b/frontend/src/redux/api/ticketApi.js @@ -185,6 +185,27 @@ export const updateTicketDescriptionApi = async (payload) => { } }; +export const updateTicketProjectApi = async (payload) => { + try { + const token = JSON.parse(localStorage.getItem("currentUser")).token; + var headers = { + Authorization: `Bearer ${token}`, + }; + var data = JSON.stringify({ + project_id: payload.project_id, + }); + + const tickets = await axios.post( + `task/project/${payload.ticketId}`, + data, + { headers: headers } + ); + return tickets; + } catch (err) { + return console.error(err); + } +}; + export const getServiceCostApi = async (payload) => { try { const token = JSON.parse(localStorage.getItem("currentUser")).token; diff --git a/frontend/src/redux/api/userApi.js b/frontend/src/redux/api/userApi.js index 8149078b..26ff10e8 100644 --- a/frontend/src/redux/api/userApi.js +++ b/frontend/src/redux/api/userApi.js @@ -120,6 +120,27 @@ export const resetPasswordApi = async (payload) => { } }; +export const adminResetPasswordApi = async (payload) => { + try { + const token = JSON.parse(localStorage.getItem("currentUser")).token; + var headers = { + Authorization: `Bearer ${token}`, + }; + var data = JSON.stringify({ + email: payload.email, + password: payload.password, + }); + + const user = await axios.post("user/adminReset", data, { + headers: headers, + }); + + return user; + } catch (err) { + return console.error(err); + } +}; + export const updateUserApi = async (payload) => { try { const token = JSON.parse(localStorage.getItem("currentUser")).token; diff --git a/frontend/src/redux/reducers/costEstimateReducer.js b/frontend/src/redux/reducers/costEstimateReducer.js index a1504735..55761002 100644 --- a/frontend/src/redux/reducers/costEstimateReducer.js +++ b/frontend/src/redux/reducers/costEstimateReducer.js @@ -16,15 +16,15 @@ const hideCost = (state = defaultCostEstimate, action) => { }; const defaultCostEstimateList = []; -const costEstimateMap = new Map(); const costEstimateList = (state = defaultCostEstimateList, action) => { switch (action.type) { case SET_COST: { - const org = action.payload[0]; + const costEstimateMap = new Map(); + const category = action.payload[0]; action.payload[1].map((cost) => { - if (cost.organization_name === org) { + if (cost.price_category === category) { costEstimateMap.set(cost.fk_answer_id, cost.cost); } return costEstimateMap; diff --git a/frontend/src/redux/reducers/questionReducer.js b/frontend/src/redux/reducers/questionReducer.js index 52ec03bd..c844b627 100644 --- a/frontend/src/redux/reducers/questionReducer.js +++ b/frontend/src/redux/reducers/questionReducer.js @@ -38,8 +38,8 @@ const answerList = (state = defaultAnswerList, action) => { switch (action.type) { case SET_ANSWER: { var finalAnswers = {}; - action.payload.forEach((answer) => { - if (answer.answer_id !== null) { + action.payload.answers.forEach((answer) => { + if (answer.answer_id !== null && (answer.price_category === action.payload.price_category || answer.price_cateogry === undefined)) { var answerForQuestion = finalAnswers[answer.question_id] ?? []; if ( !answerForQuestion.find( diff --git a/frontend/src/redux/saga/costSaga.js b/frontend/src/redux/saga/costSaga.js index d3a6f421..7793878e 100644 --- a/frontend/src/redux/saga/costSaga.js +++ b/frontend/src/redux/saga/costSaga.js @@ -4,18 +4,18 @@ import { deleteCosts, getCosts, postCosts, updateQuantifiableApi } from "../api/ export function* fetchCost({ payload }) { const costs = []; - let organization = "Hungrii Inc."; + let category = "Industry"; if (payload.formResponses.length !== 0) { for (var i = 0; i < payload.formResponses.length; i++) { - const org = payload.formResponses[i].question.price_category; - if (org != null) { - organization = org; + const price_category = payload.formResponses[i].question.price_category; + if (price_category != null) { + category = price_category; break; } } } - costs.push(organization); + costs.push(category); const costEstimates = yield call(getCosts); costs.push(costEstimates.data); diff --git a/frontend/src/redux/saga/questionSaga.js b/frontend/src/redux/saga/questionSaga.js index 3435c993..feb2e5c5 100644 --- a/frontend/src/redux/saga/questionSaga.js +++ b/frontend/src/redux/saga/questionSaga.js @@ -1,4 +1,4 @@ -import { call, put, takeEvery, takeLatest } from "redux-saga/effects"; +import { call, put, select, takeEvery, takeLatest } from "redux-saga/effects"; import { SAVE_LOGIC, SET_LOGIC } from "../actions/logicActions"; import { DELETE_ANSWER, @@ -30,9 +30,16 @@ export function* fetchQuestion({ payload }) { yield put({ type: SET_LOGIC, payload: logic.data }); const questions = yield call(getQuestions, payload); + const form_response = yield select((state) => state.formReducer.formResponses); + const projects = yield select((state) => state.projectReducer.projectList); + + const project = form_response.find((response) => response.question_info?.question_type === "project"); + const project_id = project?.response + const selectedProject = projects.find((project) => project.project_id === project_id); + const price_category = selectedProject?.costcenter[0].cost_center_type ?? "Industry"; yield put({ type: SET_QUESTION, payload: questions?.data ?? [] }); - yield put({ type: SET_ANSWER, payload: questions?.data ?? [] }); + yield put({ type: SET_ANSWER, payload: { answers: questions?.data ?? [], price_category }}); yield put({ type: SET_LOADING, payload: false }); } diff --git a/frontend/src/redux/saga/ticketSaga.js b/frontend/src/redux/saga/ticketSaga.js index 1383ca78..5b68d047 100644 --- a/frontend/src/redux/saga/ticketSaga.js +++ b/frontend/src/redux/saga/ticketSaga.js @@ -21,6 +21,7 @@ import { UPDATE_TICKET_TITLE, CLEAR_ATTACHMENTS, DELETE_VIEW_SUMMARY, + UPDATE_TICKET_PROJECT, } from "../actions/ticketActions"; import { assignUserApi, @@ -36,6 +37,7 @@ import { getSubTicketsById, createSubtask, updateTicketTitleApi, + updateTicketProjectApi, } from "../api/ticketApi"; import { getAnswersBySurvey } from "../api/questionApi"; @@ -62,7 +64,9 @@ export function* fetchTickets() { (state) => state.ticketReducer.ticketBoardDndData ); let newTicket = allTickets.tasks[currentTicket.task_uuid]; - yield put({ type: SET_ACTIVE_TICKET, payload: newTicket }); + if (currentTicket.ticket_id !== newTicket.ticket_id) { + yield put({ type: SET_ACTIVE_TICKET, payload: newTicket }); + } } } @@ -84,6 +88,13 @@ export function* updateTicketDescription(action) { yield call(fetchTickets); } +export function* updateTicketProject(action) { + const { ticketId, project_id } = action.payload; + yield call(updateTicketProjectApi, { ticketId, project_id }); + yield call(fetchTickets); +} + + export function* assignUser(action) { yield call(assignUserApi, action.payload); yield call(fetchTickets); @@ -212,6 +223,7 @@ export default function* ticketSaga() { yield takeLatest(UPDATE_TICKET_STATUS, updateTicketStatus); yield takeLatest(UPDATE_TICKET_TITLE, updateTicketTitle); yield takeLatest(UPDATE_TICKET_DESCRIPTION, updateTicketDescription); + yield takeLatest(UPDATE_TICKET_PROJECT, updateTicketProject); yield takeLatest(ASSIGN_USER, assignUser); yield takeLatest(UNASSIGN_USER, unassignUser); yield takeLatest(POST_SERVICE_COST, postServiceCost); diff --git a/frontend/src/redux/saga/userSaga.js b/frontend/src/redux/saga/userSaga.js index f03ab3de..c292a514 100644 --- a/frontend/src/redux/saga/userSaga.js +++ b/frontend/src/redux/saga/userSaga.js @@ -23,6 +23,7 @@ import { UPDATE_USER, REQUEST_RESET, RESET_PASSWORD, + ADMIN_RESET_PASSWORD, } from "../actions/userActions"; import { getProjectAssignmentApi } from "../api/projectApi"; import { @@ -41,6 +42,7 @@ import { updateUserApi, requestResetApi, resetPasswordApi, + adminResetPasswordApi, } from "../api/userApi"; import { ErrorToast, SuccessToast } from "../../components/Toasts"; @@ -110,9 +112,24 @@ export function* resetPasswordSaga({ payload }) { } } +export function* adminResetPasswordSaga({ payload }) { + const resetRes = yield call(adminResetPasswordApi, payload); + if (resetRes) { + SuccessToast("Password reset successfully!"); + } else { + ErrorToast("Password reset failed. Please try again."); + } +} + export function* postUserSaga({ payload }) { const newUser = yield call(saveUserApi, payload); - if (newUser) { + if (payload.adminCreated) { + SuccessToast("Account created successfully!"); + yield call(approveUserList, { users: [payload.user_id]}); + yield delay(1000); + yield loadUserlistSaga(); + yield loadPendingUserListSaga(); + } else if (newUser) { SuccessToast("Account created successfully! Please login to continue."); payload.navTo(); } else { @@ -182,4 +199,5 @@ export default function* userSaga() { yield takeLatest(GET_USER, getUserSaga); yield takeLatest(REQUEST_RESET, requestResetSaga); yield takeLatest(RESET_PASSWORD, resetPasswordSaga); + yield takeLatest(ADMIN_RESET_PASSWORD, adminResetPasswordSaga); } diff --git a/frontend/src/screens/request-form/request-form.js b/frontend/src/screens/request-form/request-form.js index b457414f..3d1dc3f5 100644 --- a/frontend/src/screens/request-form/request-form.js +++ b/frontend/src/screens/request-form/request-form.js @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from "react-redux"; import { NavLink, Navigate } from "react-router-dom"; import uuid from "react-uuid"; import "./request-form.css"; -import { appColor, awsConfig } from "../../constants"; +import { appColor, awsConfig, summaryFormat } from "../../constants"; import { LOAD_QUESTION } from "../../redux/actions/questionActions"; import { LOAD_COST } from "../../redux/actions/costActions"; import MultiSelect from "../../components/MultiSelect"; @@ -30,6 +30,7 @@ import FormInfo from "../../components/FormInfo"; import { GET_USER, SET_CURRENT_USER } from "../../redux/actions/userActions"; import { useDocumentTitle } from "@uidotdev/usehooks"; import ToastContainer from "../../components/Toasts/ToastContainer"; +import jsPDF from 'jspdf'; function RequestForm({ origin }) { const dispatch = useDispatch(); @@ -45,7 +46,7 @@ function RequestForm({ origin }) { const clinicalList = useSelector( (state) => state.formReducer.clinicalResponses ); - let noShowList = []; + const [noShowList, setNoShowList] = useState([]); const costEstimateMap = useSelector( (state) => state.costEstimateReducer.costEstimateList ); @@ -68,6 +69,7 @@ function RequestForm({ origin }) { dispatch({ type: GET_USER, payload: { user_id: vaUser } }); } } + setNoShowList([]); }, [dispatch, formId, currentUser]); // Load Cost Estimate @@ -95,22 +97,60 @@ function RequestForm({ origin }) { height: document.getElementById("requestFormContainer").scrollHeight, windowHeight: document.getElementById("requestFormContainer").scrollHeight, }).then(function (canvas) { - canvas.toBlob((blob) => { - if (blob === null) return; + if (summaryFormat === "pdf") { + /* SAVE AS PDF TO S3 */ + const imgData = canvas.toDataURL('image/png'); + const imgWidth = 210; + const pageHeight = 295; + const imgHeight = canvas.height * imgWidth / canvas.width; + var heightLeft = imgHeight; + + const doc = new jsPDF('p', 'mm'); + var position = 0; + + doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); + heightLeft -= pageHeight; + + while (heightLeft >= 0) { + position = heightLeft - imgHeight; + doc.addPage(); + doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); + heightLeft -= pageHeight; + } + AWS.config.update(awsConfig); const S3 = new AWS.S3({}); const objParams = { Bucket: process.env.REACT_APP_S3_BUCKET, Key: `requestSummary/${survey_id}`, - Body: blob, - ContentType: "image/png", + Body: doc.output('blob'), + ContentType: "application/pdf", }; S3.putObject(objParams) .send(function (err, data) { console.log(err ? `Issue in S3.putObject.send() => ${err}` : "Send completed in S3.putObject.send()"); }); - }, "image/png"); + // doc.save('download.pdf'); + } else if (summaryFormat === "png") { + /* SAVE AS IMAGE TO S3 */ + canvas.toBlob((blob) => { + if (blob === null) return; + AWS.config.update(awsConfig); + const S3 = new AWS.S3({}); + const objParams = { + Bucket: process.env.REACT_APP_S3_BUCKET, + Key: `requestSummary/${survey_id}`, + Body: blob, + ContentType: "image/png", + }; + + S3.putObject(objParams) + .send(function (err, data) { + console.log(err ? `Issue in S3.putObject.send() => ${err}` : "Send completed in S3.putObject.send()"); + }); + }, "image/png"); + } }); } @@ -121,8 +161,10 @@ function RequestForm({ origin }) { questions.forEach((question) => { if ( question.mandatory && - formResponses.filter( - (response) => response.question.question_id === question.question_id + formResponses.filter((response) => + response.question?.question_id === question.question_id + || response.question[0]?.question_id === question.question_id + || response.question_info?.question_id === question.question_id ).length === 0 && !noShowList.includes(question.question_id) ) { @@ -221,7 +263,6 @@ function RequestForm({ origin }) { }); if (questions.length !== 0 && logicList.length !== 0) { - noShowList = []; return (
@@ -274,7 +315,9 @@ function RequestForm({ origin }) { } if (!show || !orShow) { - noShowList.push(question.question_id); + if (!noShowList.includes(question.question_id)) { + setNoShowList((prev) => [...prev, question.question_id]); + } return null; } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 97d10fb0..a691df5d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -8169,6 +8169,21 @@ jspdf@^2.3.1: dompurify "^2.2.0" html2canvas "^1.0.0-rc.5" +jspdf@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-2.5.1.tgz#00c85250abf5447a05f3b32ab9935ab4a56592cc" + integrity sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA== + dependencies: + "@babel/runtime" "^7.14.0" + atob "^2.1.2" + btoa "^1.2.1" + fflate "^0.4.8" + optionalDependencies: + canvg "^3.0.6" + core-js "^3.6.0" + dompurify "^2.2.0" + html2canvas "^1.0.0-rc.5" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: version "3.3.3" resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz"