Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. import React, { useState, useEffect } from "react";
  2. import IconButton from "../../components/IconButton/IconButton";
  3. import deleteIcon from "../../assets/images/delete.png";
  4. import editIcon from "../../assets/images/edit.png";
  5. import { useTranslation } from "react-i18next";
  6. import PropTypes from "prop-types";
  7. import { useSelector, useDispatch } from "react-redux";
  8. import {
  9. deleteFileReq,
  10. getFileFiltersReq,
  11. getFilesReq,
  12. setContent,
  13. updateFileReq,
  14. } from "../../store/actions/files/fileActions";
  15. import { Fade, Pagination } from "@mui/material";
  16. import FilterButton from "../../components/Button/FilterButton";
  17. import searchImage from "../../assets/images/search.png";
  18. import DocsFilters from "../../components/Docs/DocsFilters";
  19. import ConfirmDialog from "../../components/MUI/ConfirmDialog";
  20. import FileViewer from "react-file-viewer";
  21. import CustomModal from "../../components/UI/CustomModal";
  22. import xIcon from "../../assets/images/x.png";
  23. import Button from "../../components/Button/Button";
  24. import { selectIsLoadingByActionType } from "../../store/selectors/loadingSelectors";
  25. import { FETCH_FILES_LOADING } from "../../store/actions/files/fileActionConstants";
  26. import { useParams } from "react-router";
  27. import { PAGE_SIZE_FILES } from "../../constants/keyCodeConstants";
  28. const FileTable = ({ trigger }) => {
  29. const [file, setFile] = useState(null);
  30. const { data } = useSelector((s) => s.files);
  31. const { filters } = useSelector((s) => s.fileFilters);
  32. const [page, setPage] = useState(1);
  33. const [toggleFiltersDrawer, setToggleFiltersDrawer] = useState(false);
  34. const [isSearchFieldVisible, setIsSearchFieldVisible] = useState(false);
  35. let { id } = useParams();
  36. const [fileForDelete, setFileForDelete] = useState({
  37. open: false,
  38. title: "",
  39. streamId: null,
  40. });
  41. const [openNoteModal, setOpenNoteModal] = useState({
  42. open: false,
  43. note: null,
  44. streamId: "",
  45. });
  46. const { t } = useTranslation();
  47. const dispatch = useDispatch();
  48. useEffect(() => {
  49. if (data !== null && data.data !== undefined) {
  50. if (data.data.length === 0) {
  51. setPage(getTotalNumberOfPages(data) - 1);
  52. }
  53. }
  54. }, [data]);
  55. const getTotalNumberOfPages = (data) => {
  56. return parseInt(data.total) <= PAGE_SIZE_FILES
  57. ? 1
  58. : Math.ceil(parseInt(data.total) / PAGE_SIZE_FILES);
  59. };
  60. useEffect(() => {
  61. setFile(null);
  62. }, [id]);
  63. useEffect(() => {
  64. if (trigger) {
  65. handleChangeVisibility();
  66. }
  67. }, [trigger]);
  68. const handleToggleFiltersDrawer = () => {
  69. setToggleFiltersDrawer((oldState) => !oldState);
  70. };
  71. useEffect(() => {
  72. handleFilters(page);
  73. }, [page]);
  74. useEffect(() => {
  75. dispatch(getFileFiltersReq());
  76. dispatch(
  77. getFilesReq({
  78. payload: {
  79. pageSize: PAGE_SIZE_FILES,
  80. currentPage: page,
  81. categoryId: id,
  82. extensions: [],
  83. tags: [],
  84. content: "",
  85. },
  86. })
  87. );
  88. }, [id, page]);
  89. const handleChange = (_, value) => {
  90. setFile(null);
  91. handleFilters(value);
  92. setPage(value);
  93. };
  94. const handleFilters = (value) => {
  95. var extFilters = [];
  96. filters.extensions
  97. ?.filter((n) => n.isChecked)
  98. .forEach((m) => extFilters.push(m.name));
  99. var tagFilters = [];
  100. filters.tags
  101. ?.filter((n) => n.isChecked)
  102. .forEach((m) => tagFilters.push(m.name));
  103. dispatch(
  104. getFilesReq({
  105. payload: {
  106. pageSize: PAGE_SIZE_FILES,
  107. currentPage: value,
  108. categoryId: id,
  109. extensions: extFilters,
  110. tags: tagFilters,
  111. content: filters.content === undefined ? "" : filters.content,
  112. },
  113. })
  114. );
  115. };
  116. const isLoading = useSelector(
  117. selectIsLoadingByActionType(FETCH_FILES_LOADING)
  118. );
  119. const handleChangeVisibility = () => {
  120. setIsSearchFieldVisible(false);
  121. };
  122. const stopPropagation = (e) => {
  123. e.stopPropagation();
  124. };
  125. const handleChangeContent = (value) => {
  126. dispatch(setContent(value));
  127. };
  128. const deleteFileHandler = (stream_id) => {
  129. dispatch(deleteFileReq({ id: stream_id }));
  130. if (file !== null && stream_id === file.streamId) {
  131. setFile(null);
  132. }
  133. };
  134. const handleKeyDown = (event) => {
  135. if (event.key === "Enter" && filters.content !== "" && filters.content !== undefined) {
  136. var extFilters = [];
  137. filters.extensions
  138. ?.filter((n) => n.isChecked)
  139. .forEach((m) => extFilters.push(m.name));
  140. var tagFilters = [];
  141. filters.tags
  142. ?.filter((n) => n.isChecked)
  143. .forEach((m) => tagFilters.push(m.name));
  144. dispatch(
  145. getFilesReq({
  146. payload: {
  147. pageSize: PAGE_SIZE_FILES,
  148. currentPage: page,
  149. categoryId: id,
  150. extensions: extFilters,
  151. tags: tagFilters,
  152. content: filters.content,
  153. },
  154. })
  155. );
  156. }
  157. };
  158. const input = (
  159. <div>
  160. <input
  161. placeholder="Pretrazi..."
  162. value={filters.content}
  163. onChange={(e) => handleChangeContent(e.target.value)}
  164. onKeyDown={handleKeyDown}
  165. className="candidate-search-field"
  166. onClick={stopPropagation}
  167. role="input"
  168. />
  169. </div>
  170. );
  171. const onError = (e) => {
  172. console.log(e, "error in file-viewer");
  173. };
  174. const displayFile = (fileStream, streamId, fileType) => {
  175. setFile({ fileStream, streamId, fileType });
  176. };
  177. const getPathForFile = (fileType, fileStream) => {
  178. if (fileType === "docx") {
  179. return `data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,${fileStream}`;
  180. } else if (fileType === "doc") {
  181. return `data:application/msword;base64,${fileStream}`;
  182. } else {
  183. return `data:application/pdf;base64,${fileStream}`;
  184. }
  185. };
  186. const updateFileNoteHandler = () => {
  187. dispatch(
  188. updateFileReq({
  189. id: openNoteModal.streamId,
  190. data: openNoteModal.note,
  191. onSuccessOpenNoteModal,
  192. })
  193. );
  194. };
  195. const onSuccessOpenNoteModal = () => {
  196. setOpenNoteModal({
  197. open: false,
  198. note: null,
  199. streamId: "",
  200. });
  201. };
  202. return isLoading ? (
  203. <div>
  204. <div className="l-t-rectangle"></div>
  205. <div className="r-b-rectangle"></div>
  206. <div className="loader-container h-withHeader">
  207. <span>Loading</span>
  208. </div>
  209. </div>
  210. ) : (
  211. <div>
  212. <ConfirmDialog
  213. open={fileForDelete.open}
  214. title="Brisanje dokumenta"
  215. subtitle={fileForDelete.title}
  216. imgSrc={deleteIcon}
  217. content="Da li ste sigurni za brisanje dokumenta?"
  218. onClose={() => {
  219. setFileForDelete({ open: false, title: "", streamId: null });
  220. }}
  221. onConfirm={() => {
  222. deleteFileHandler(fileForDelete.streamId);
  223. setFileForDelete({ open: false, title: "", streamId: null });
  224. }}
  225. />
  226. <CustomModal
  227. classes="files-custom-modal"
  228. open={openNoteModal.open}
  229. onCloseModal={() =>
  230. setOpenNoteModal({ open: false, note: null, streamId: "" })
  231. }
  232. >
  233. <div className="add-pattern-modal-header">
  234. <div className="add-pattern-modal-header-title">
  235. <div className="add-pattern-modal-header-title-image">
  236. <img src={editIcon} alt="plus" />
  237. </div>
  238. <div className="add-pattern-modal-header-title-title">
  239. <p>Edit text</p>
  240. </div>
  241. <div className="add-pattern-modal-header-title-sub">
  242. <sub> | Note</sub>
  243. </div>
  244. </div>
  245. <div
  246. className="add-pattern-modal-header-close"
  247. onClick={() =>
  248. setOpenNoteModal({ open: false, note: null, streamId: "" })
  249. }
  250. >
  251. <img src={xIcon} alt="close" />
  252. </div>
  253. </div>
  254. <div className="files-edit-note">
  255. <textarea
  256. className="files-note"
  257. value={openNoteModal.note}
  258. onChange={(e) =>
  259. setOpenNoteModal((oldState) => ({
  260. ...oldState,
  261. note: e.target.value,
  262. }))
  263. }
  264. />
  265. </div>
  266. <div className="files-custom-modal-buttons">
  267. <Button
  268. type="button"
  269. variant="contained"
  270. className="c-btn c-btn--primary-outlined"
  271. onClick={() =>
  272. setOpenNoteModal({ open: false, note: null, streamId: "" })
  273. }
  274. >
  275. Close
  276. </Button>
  277. <Button
  278. type="button"
  279. variant="contained"
  280. className="c-btn c-btn--primary"
  281. onClick={updateFileNoteHandler}
  282. >
  283. Save Changes
  284. </Button>
  285. </div>
  286. </CustomModal>
  287. <DocsFilters
  288. open={toggleFiltersDrawer}
  289. handleClose={handleToggleFiltersDrawer}
  290. setPage={setPage}
  291. setFile={setFile}
  292. />
  293. <div
  294. style={{
  295. display: "flex",
  296. justifyContent: "space-between",
  297. position: "relative",
  298. width: "calc(100% - 144px)",
  299. }}
  300. >
  301. <div>
  302. <Fade in={isSearchFieldVisible} timeout={500} className="proba">
  303. {input}
  304. </Fade>
  305. <Fade in={isSearchFieldVisible} timeout={500}>
  306. <div
  307. style={{
  308. position: "absolute",
  309. zIndex: 10000,
  310. left: 780,
  311. top: 20,
  312. marginTop: 0,
  313. }}
  314. >
  315. <img src={searchImage} />
  316. </div>
  317. </Fade>
  318. </div>
  319. <div style={{ display: "flex" }}>
  320. {isSearchFieldVisible ? (
  321. ""
  322. ) : (
  323. <IconButton
  324. className="c-btn c-btn--primary-outlined candidate-btn search-field userPageBtn ml-20px no-padding custom-filter-button"
  325. onClick={() => setIsSearchFieldVisible(true)}
  326. >
  327. {t("candidates.search")}
  328. <img
  329. src={searchImage}
  330. alt="filter"
  331. className="candidates-image"
  332. />
  333. </IconButton>
  334. )}
  335. <FilterButton onShowFilters={handleToggleFiltersDrawer} />
  336. </div>
  337. </div>
  338. <div
  339. style={{
  340. display: "flex",
  341. marginTop: "39px",
  342. flexDirection: "column",
  343. justifyContent: "space-between",
  344. minHeight: "500px",
  345. width: "100%",
  346. }}
  347. >
  348. <div className="table-cont">
  349. <div
  350. style={{
  351. display: "flex",
  352. height: "300px",
  353. width: "calc(100% - 144px)",
  354. }}
  355. >
  356. <table
  357. className={"usersTable-users mini"}
  358. style={{
  359. width: file === null ? "100%" : "400px",
  360. height: "50%",
  361. }}
  362. >
  363. <thead>
  364. <tr
  365. className="headingRow headingRowFiles"
  366. style={{ cursor: "pointer" }}
  367. >
  368. {file === null ? <th>Putanja dokumenta</th> : ""}
  369. <th>Naziv dokumenta</th>
  370. {file === null ? <th>Tip dokumenta</th> : ""}
  371. {file === null ? <th>Veličina dokumenta</th> : ""}
  372. {file === null ? <th>Obrisi dokument</th> : ""}
  373. {file === null ? <th>Note</th> : ""}
  374. <th>Preuzmi dokument</th>
  375. </tr>
  376. </thead>
  377. <tbody>
  378. {data.data?.map((n, index) => (
  379. <tr
  380. key={index}
  381. className="secondaryRow"
  382. onClick={() =>
  383. displayFile(n.file_stream, n.stream_id, n.file_type)
  384. }
  385. >
  386. {file === null ? (
  387. <td className="docs-name">{n.fileName}</td>
  388. ) : (
  389. ""
  390. )}
  391. <td className="docs-name">{n.title}</td>
  392. {file === null ? <td>{n.file_type && n.file_type}</td> : ""}
  393. {file === null ? (
  394. <td className="profession">{n.cached_file_size}kB</td>
  395. ) : (
  396. ""
  397. )}
  398. {file === null ? (
  399. <td className="profession">
  400. <IconButton
  401. className="c-btn c-btn--primary-outlined files-view-page-delete-btn"
  402. onClick={() =>
  403. setFileForDelete({
  404. open: true,
  405. title: n.title,
  406. streamId: n.stream_id,
  407. })
  408. }
  409. >
  410. <img
  411. style={{ width: "12px", height: "12px" }}
  412. src={deleteIcon}
  413. />
  414. </IconButton>
  415. </td>
  416. ) : (
  417. ""
  418. )}
  419. {file === null ? (
  420. <td className="profession">
  421. <IconButton
  422. className="c-btn c-btn--primary-outlined files-view-page-delete-btn"
  423. onClick={() => {
  424. setOpenNoteModal({
  425. open: true,
  426. note: n.note,
  427. streamId: n.stream_id,
  428. });
  429. }}
  430. >
  431. <img
  432. style={{ width: "12px", height: "12px" }}
  433. src={editIcon}
  434. />
  435. </IconButton>
  436. </td>
  437. ) : (
  438. ""
  439. )}
  440. <td>
  441. <div onClick={stopPropagation}>
  442. <a
  443. className="applicant-cv-button"
  444. style={{
  445. width: "100px",
  446. height: "40px",
  447. padding: 8,
  448. }}
  449. download={n.title}
  450. href={getPathForFile(n.file_type, n.file_stream)}
  451. >
  452. {t("common.download")}
  453. </a>
  454. </div>
  455. </td>
  456. </tr>
  457. ))}
  458. </tbody>
  459. </table>
  460. {file !== null ? (
  461. <div
  462. style={{
  463. width: "830px",
  464. marginLeft: "30px",
  465. overflowY: "visible",
  466. }}
  467. >
  468. <FileViewer
  469. fileType={file.fileType}
  470. filePath={getPathForFile(file.fileType, file.fileStream)}
  471. onError={onError}
  472. />
  473. </div>
  474. ) : (
  475. ""
  476. )}
  477. </div>
  478. <Pagination
  479. size={"small"}
  480. count={getTotalNumberOfPages(data)}
  481. color="primary"
  482. className="candidates-pagination"
  483. onChange={handleChange}
  484. shape="rounded"
  485. page={page}
  486. />
  487. </div>
  488. <div
  489. style={{
  490. display: "flex",
  491. justifyContent: "flex-end",
  492. marginBottom: "35px",
  493. }}
  494. ></div>
  495. </div>
  496. </div>
  497. );
  498. };
  499. FileTable.propTypes = {
  500. trigger: PropTypes.number,
  501. };
  502. export default FileTable;