All GCC vendors and employee details for IT department management
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2516 lines
106 KiB

​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  1. <!DOCTYPE html>
  2. <html xmlns:th="http://www.thymeleaf.org" lang="en">
  3. <head>
  4. <script th:inline="javascript">
  5. (function () {
  6. const currentUser = /*[[${session.tap_usrName}]]*/ null;
  7. if (currentUser && sessionStorage.getItem("tap_usrName") !== currentUser) {
  8. window.location.replace(/*[[@{/gcctapaldetailsmanagement/login}]]*/ '/gcctapaldetailsmanagement/login');
  9. }
  10. })();
  11. </script>
  12. <meta charset="UTF-8">
  13. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  14. <title>GCC Tapal Tracker - Dashboard</title>
  15. <!-- CSRF Tokens -->
  16. <meta th:if="${_csrf != null}" name="_csrf" th:content="${_csrf.token}" />
  17. <meta th:if="${_csrf != null}" name="_csrf_header" th:content="${_csrf.headerName}" />
  18. <!-- Google Fonts -->
  19. <link
  20. href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Poppins:wght@300;400;500;600;700&display=swap"
  21. rel="stylesheet">
  22. <!-- Bootstrap & FontAwesome -->
  23. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
  24. <link rel="stylesheet" th:href="@{/assets/font-awesome-6.5.2/css/all.min.css}" type="text/css">
  25. <!-- Flatpickr Datepicker & Chart.js & SheetJS -->
  26. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
  27. <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
  28. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  29. <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
  30. <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
  31. <style>
  32. .text-info-dark {
  33. color: #075985 !important;
  34. font-weight: 600;
  35. }
  36. .text-warning-dark {
  37. color: #92400e !important;
  38. font-weight: 600;
  39. }
  40. .text-success-dark {
  41. color: #166534 !important;
  42. font-weight: bold;
  43. }
  44. .text-danger-dark {
  45. color: #991b1b !important;
  46. font-weight: bold;
  47. }
  48. body {
  49. background-color: #f4f7f6;
  50. font-family: 'Poppins', sans-serif;
  51. color: #333;
  52. }
  53. .header {
  54. background: linear-gradient(135deg, #09303d, #0f5c78);
  55. color: #fff;
  56. padding: 15px 30px;
  57. display: flex;
  58. justify-content: space-between;
  59. align-items: center;
  60. box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
  61. }
  62. .logo-left {
  63. display: flex;
  64. align-items: center;
  65. gap: 15px;
  66. }
  67. .logo-left img {
  68. width: 60px;
  69. height: 60px;
  70. }
  71. .logo-text h2 {
  72. font-family: 'Outfit', sans-serif;
  73. font-size: 1.1rem;
  74. font-weight: 700;
  75. margin: 0;
  76. line-height: 1.2;
  77. letter-spacing: 0.5px;
  78. }
  79. .logo-text p {
  80. font-size: 0.7rem;
  81. margin: 0;
  82. color: #aed6f1;
  83. }
  84. .header-title h2 {
  85. font-family: 'Outfit', sans-serif;
  86. font-size: 1.5rem;
  87. font-weight: 700;
  88. margin: 0;
  89. }
  90. .user-menu {
  91. display: flex;
  92. align-items: center;
  93. gap: 15px;
  94. }
  95. .user-badge {
  96. background: rgba(255, 255, 255, 0.15);
  97. padding: 8px 16px;
  98. border-radius: 20px;
  99. font-size: 0.85rem;
  100. font-weight: 600;
  101. display: flex;
  102. align-items: center;
  103. gap: 8px;
  104. }
  105. .btn-logout {
  106. background: #e74c3c;
  107. color: #fff;
  108. border: none;
  109. padding: 8px 16px;
  110. border-radius: 20px;
  111. font-size: 0.85rem;
  112. font-weight: 600;
  113. cursor: pointer;
  114. transition: all 0.3s;
  115. text-decoration: none;
  116. display: inline-flex;
  117. align-items: center;
  118. gap: 6px;
  119. }
  120. .btn-logout:hover {
  121. background: #c0392b;
  122. color: #fff;
  123. transform: translateY(-1.5px);
  124. }
  125. .main-container {
  126. max-width: 95%;
  127. margin: 35px auto;
  128. padding: 0 15px;
  129. }
  130. /* Stats Cards styling */
  131. /* Category Grid (3 Cards) */
  132. .category-grid {
  133. display: grid;
  134. grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  135. gap: 20px;
  136. margin-bottom: 35px;
  137. }
  138. .category-card {
  139. background: #ffffff;
  140. border: 1px solid #eef2f6;
  141. border-radius: 16px;
  142. padding: 24px;
  143. box-shadow: 0 5px 20px rgba(0, 0, 0, 0.02);
  144. transition: all 0.3s ease;
  145. position: relative;
  146. overflow: hidden;
  147. cursor: pointer;
  148. display: flex;
  149. align-items: center;
  150. justify-content: space-between;
  151. }
  152. .category-card::before {
  153. content: '';
  154. position: absolute;
  155. top: 0;
  156. left: 0;
  157. width: 5px;
  158. height: 100%;
  159. }
  160. /* Total Card Accent */
  161. .category-card.card-total::before {
  162. background-color: #2563eb;
  163. }
  164. .category-card.card-total.active {
  165. border-color: #2563eb;
  166. box-shadow: 0 10px 25px rgba(37, 99, 235, 0.15);
  167. background-color: #eff6ff;
  168. }
  169. .card-total .card-icon {
  170. color: #2563eb;
  171. }
  172. /* Completed Card Accent */
  173. .category-card.card-completed::before {
  174. background-color: #166534;
  175. }
  176. .category-card.card-completed.active {
  177. border-color: #166534;
  178. box-shadow: 0 10px 25px rgba(22, 101, 52, 0.15);
  179. background-color: #f0fdf4;
  180. }
  181. .card-completed .card-icon {
  182. color: #166534;
  183. }
  184. /* Pending Card Accent */
  185. .category-card.card-pending::before {
  186. background-color: #991b1b;
  187. }
  188. .category-card.card-pending.active {
  189. border-color: #991b1b;
  190. box-shadow: 0 10px 25px rgba(153, 27, 27, 0.15);
  191. background-color: #fef2f2;
  192. }
  193. .card-pending .card-icon {
  194. color: #991b1b;
  195. }
  196. .category-card:hover {
  197. transform: translateY(-4px);
  198. box-shadow: 0 8px 25px rgba(0, 0, 0, 0.06);
  199. }
  200. .card-left {
  201. display: flex;
  202. flex-direction: column;
  203. gap: 5px;
  204. }
  205. .card-title {
  206. font-size: 0.88rem;
  207. font-weight: 600;
  208. color: #64748b;
  209. margin: 0;
  210. text-transform: uppercase;
  211. letter-spacing: 0.5px;
  212. }
  213. .card-value {
  214. font-family: 'Outfit', sans-serif;
  215. font-size: 2.1rem;
  216. font-weight: 700;
  217. color: #09303d;
  218. margin: 0;
  219. line-height: 1;
  220. }
  221. .card-icon {
  222. font-size: 2.2rem;
  223. opacity: 0.85;
  224. }
  225. /* Filter Card styling */
  226. .filter-card {
  227. background: #fff;
  228. border-radius: 16px;
  229. border: 1px solid #eef2f6;
  230. box-shadow: 0 8px 30px rgba(0, 0, 0, 0.02);
  231. padding: 24px;
  232. margin-bottom: 30px;
  233. }
  234. .filter-grid {
  235. display: grid;
  236. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  237. gap: 20px;
  238. align-items: end;
  239. }
  240. .filter-group {
  241. display: flex;
  242. flex-direction: column;
  243. gap: 6px;
  244. }
  245. .filter-label {
  246. font-size: 0.75rem;
  247. font-weight: 600;
  248. color: #64748b;
  249. text-transform: uppercase;
  250. }
  251. .filter-input {
  252. border: 1.5px solid #e2e8f0;
  253. border-radius: 8px;
  254. padding: 8px 12px;
  255. font-size: 0.85rem;
  256. outline: none;
  257. transition: all 0.3s;
  258. background-color: #fff;
  259. }
  260. .filter-input:focus {
  261. border-color: #0f5c78;
  262. box-shadow: 0 0 0 3px rgba(15, 92, 120, 0.15);
  263. }
  264. .btn-filter-search {
  265. background-color: #0f5c78;
  266. color: #fff;
  267. border: none;
  268. border-radius: 8px;
  269. padding: 9px 24px;
  270. font-size: 0.85rem;
  271. font-weight: 600;
  272. cursor: pointer;
  273. transition: all 0.3s;
  274. display: inline-flex;
  275. align-items: center;
  276. gap: 8px;
  277. height: 38px;
  278. }
  279. .btn-filter-search:hover {
  280. background-color: #09303d;
  281. }
  282. .btn-filter-reset {
  283. background-color: #f1f5f9;
  284. color: #475569;
  285. border: 1.5px solid #e2e8f0;
  286. border-radius: 8px;
  287. padding: 8px 20px;
  288. font-size: 0.85rem;
  289. font-weight: 600;
  290. cursor: pointer;
  291. transition: all 0.3s;
  292. display: inline-flex;
  293. align-items: center;
  294. gap: 6px;
  295. height: 38px;
  296. }
  297. .btn-filter-reset:hover {
  298. background-color: #e2e8f0;
  299. }
  300. /* Table card styling */
  301. .table-card {
  302. background: #fff;
  303. border-radius: 16px;
  304. padding: 24px;
  305. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
  306. margin-bottom: 30px;
  307. }
  308. .table-card-header {
  309. display: flex;
  310. justify-content: space-between;
  311. align-items: center;
  312. margin-bottom: 20px;
  313. flex-wrap: wrap;
  314. gap: 15px;
  315. }
  316. .table-card-title {
  317. font-family: 'Outfit', sans-serif;
  318. font-size: 1.3rem;
  319. font-weight: 700;
  320. color: #09303d;
  321. margin: 0;
  322. display: flex;
  323. align-items: center;
  324. gap: 10px;
  325. }
  326. .active-filter-badge {
  327. background-color: #e0f2fe;
  328. color: #0369a1;
  329. font-size: 0.72rem;
  330. padding: 4px 10px;
  331. border-radius: 12px;
  332. font-weight: 600;
  333. }
  334. .btn-add-record {
  335. background-color: #0f5c78;
  336. color: #fff;
  337. border: none;
  338. border-radius: 8px;
  339. padding: 8px 16px;
  340. font-size: 0.85rem;
  341. font-weight: 600;
  342. cursor: pointer;
  343. transition: all 0.3s;
  344. display: inline-flex;
  345. align-items: center;
  346. gap: 6px;
  347. }
  348. .btn-add-record:hover {
  349. background-color: #09303d;
  350. transform: translateY(-1px);
  351. }
  352. .table-controls-row {
  353. display: flex;
  354. justify-content: space-between;
  355. align-items: center;
  356. margin-bottom: 15px;
  357. flex-wrap: wrap;
  358. gap: 10px;
  359. }
  360. .table-controls-left,
  361. .table-controls-right {
  362. display: flex;
  363. align-items: center;
  364. gap: 8px;
  365. }
  366. .grid-table {
  367. border-collapse: separate;
  368. border-spacing: 0;
  369. width: 100%;
  370. }
  371. .grid-table th {
  372. background-color: #f8fafc;
  373. color: #475569;
  374. font-weight: 600;
  375. text-transform: uppercase;
  376. font-size: 0.75rem;
  377. padding: 12px 16px;
  378. border-bottom: 2px solid #e2e8f0;
  379. letter-spacing: 0.5px;
  380. }
  381. .grid-table td {
  382. padding: 14px 16px;
  383. border-bottom: 1px solid #f1f5f9;
  384. font-size: 0.82rem;
  385. color: #334155;
  386. }
  387. .grid-table tbody tr:hover td {
  388. background-color: #f8fafc;
  389. }
  390. /* Status badge styles */
  391. .status-badge {
  392. font-size: 0.72rem;
  393. font-weight: 600;
  394. padding: 4px 10px;
  395. border-radius: 12px;
  396. display: inline-block;
  397. text-align: center;
  398. }
  399. /* Type badge styles */
  400. .type-badge {
  401. font-size: 0.72rem;
  402. font-weight: 600;
  403. padding: 4px 10px;
  404. border-radius: 6px;
  405. display: inline-block;
  406. }
  407. .type-sd {
  408. background-color: #fdf2f8;
  409. color: #9d174d;
  410. }
  411. .type-tax-invoice {
  412. background-color: #ecfdf5;
  413. color: #047857;
  414. }
  415. .type-emd {
  416. background-color: #f0f9ff;
  417. color: #0369a1;
  418. }
  419. .type-asd {
  420. background-color: #fff1f2;
  421. color: #be123c;
  422. }
  423. .type-agreement {
  424. background-color: #faf5ff;
  425. color: #6b21a8;
  426. }
  427. .type-bank-guarantee {
  428. background-color: #fffbeb;
  429. color: #b45309;
  430. }
  431. .type-delivery-chellan {
  432. background-color: #f0fdfa;
  433. color: #0f766e;
  434. }
  435. /* Action Buttons inside Grid */
  436. .btn-grid-action {
  437. width: 32px;
  438. height: 32px;
  439. border-radius: 6px;
  440. display: inline-flex;
  441. align-items: center;
  442. justify-content: center;
  443. border: none;
  444. cursor: pointer;
  445. transition: all 0.2s;
  446. font-size: 0.85rem;
  447. }
  448. .btn-grid-edit {
  449. background-color: #e0f2fe;
  450. color: #0369a1;
  451. }
  452. .btn-grid-edit:hover {
  453. background-color: #bae6fd;
  454. }
  455. .btn-grid-delete {
  456. background-color: #fee2e2;
  457. color: #b91c1c;
  458. }
  459. .btn-grid-delete:hover {
  460. background-color: #fecaca;
  461. }
  462. .btn-grid-pdf {
  463. background-color: #fef3c7;
  464. color: #d97706;
  465. }
  466. .btn-grid-pdf:hover {
  467. background-color: #fde68a;
  468. }
  469. /* Custom Modal headers styling */
  470. .modal-custom-header {
  471. background: linear-gradient(135deg, #09303d, #0f5c78);
  472. color: #fff;
  473. padding: 16px 24px;
  474. display: flex;
  475. justify-content: space-between;
  476. align-items: center;
  477. border-bottom: none;
  478. border-top-left-radius: 12px;
  479. border-top-right-radius: 12px;
  480. }
  481. .modal-custom-title {
  482. font-family: 'Outfit', sans-serif;
  483. font-size: 1.15rem;
  484. font-weight: 700;
  485. margin: 0;
  486. display: flex;
  487. align-items: center;
  488. gap: 10px;
  489. }
  490. .modal-content {
  491. border-radius: 12px;
  492. border: none;
  493. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
  494. }
  495. /* Loading overlay spinner */
  496. #loading-overlay {
  497. position: fixed;
  498. top: 0;
  499. left: 0;
  500. width: 100%;
  501. height: 100%;
  502. background: rgba(255, 255, 255, 0.8);
  503. z-index: 9999;
  504. display: flex;
  505. flex-direction: column;
  506. align-items: center;
  507. justify-content: center;
  508. gap: 15px;
  509. }
  510. .spinner-inner {
  511. width: 50px;
  512. height: 50px;
  513. border: 4px solid #e2e8f0;
  514. border-top-color: #0f5c78;
  515. border-radius: 50%;
  516. animation: spin 1s linear infinite;
  517. }
  518. @keyframes spin {
  519. to {
  520. transform: rotate(360deg);
  521. }
  522. }
  523. /* Custom Scrollbar */
  524. ::-webkit-scrollbar {
  525. width: 8px;
  526. height: 8px;
  527. }
  528. ::-webkit-scrollbar-track {
  529. background: #f1f5f9;
  530. }
  531. ::-webkit-scrollbar-thumb {
  532. background: #cbd5e1;
  533. border-radius: 4px;
  534. }
  535. ::-webkit-scrollbar-thumb:hover {
  536. background: #94a3b8;
  537. }
  538. /* Responsive Media Queries */
  539. @media (max-width: 991.98px) {
  540. .header {
  541. flex-direction: column;
  542. gap: 15px;
  543. text-align: center;
  544. padding: 15px;
  545. }
  546. .logo-left {
  547. flex-direction: column;
  548. gap: 8px;
  549. align-items: center;
  550. }
  551. .logo-text {
  552. text-align: center;
  553. }
  554. .user-menu {
  555. width: 100%;
  556. justify-content: center;
  557. flex-wrap: wrap;
  558. gap: 10px;
  559. }
  560. .main-container {
  561. max-width: 100%;
  562. margin: 15px auto;
  563. }
  564. }
  565. </style>
  566. </head>
  567. <body>
  568. <!-- Loading Spinner Overlay -->
  569. <div id="loading-overlay" style="display: none;">
  570. <div class="spinner-inner"></div>
  571. <span class="fw-semibold text-secondary" id="loading-text">Loading data...</span>
  572. </div>
  573. <!-- Header Panel -->
  574. <header class="header">
  575. <div class="logo-left">
  576. <img th:src="@{/assets/header-images/gcc1.png}" alt="Greater Chennai Corporation Logo">
  577. <div class="logo-text">
  578. <h2>GREATER CHENNAI CORPORATION</h2>
  579. <p>IT CELL MANAGEMENT SERVICES</p>
  580. </div>
  581. </div>
  582. <div class="header-title d-none d-md-block">
  583. <h2>Tapal Tracker Dashboard</h2>
  584. </div>
  585. <div class="user-menu" style="display: flex; align-items: center; gap: 15px;">
  586. <!-- Latest Update Badge -->
  587. <div id="latest-update-badge" class="d-flex align-items-center gap-2 p-2 px-3 rounded text-white"
  588. style="display: none !important; background-color: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); font-size: 0.8rem; font-family: 'Outfit', sans-serif; cursor: pointer; transition: all 0.3s;"
  589. onclick="openLatestUpdatesModal()">
  590. <i class="fa-solid fa-bell text-warning fa-bounce"></i>
  591. <div>
  592. Latest Update: <span class="fw-bold text-warning" id="latest-update-count">0</span> File(s) on
  593. <span class="fw-semibold" id="latest-update-time">-</span>
  594. </div>
  595. </div>
  596. <div class="user-badge">
  597. <i class="fa-solid fa-user-tie"></i>
  598. <span th:text="${usrName}">Admin</span>
  599. </div>
  600. <a th:href="@{/gcctapaldetailsmanagement/logout}" class="btn-logout">
  601. <i class="fa-solid fa-sign-out-alt"></i> Log Out
  602. </a>
  603. </div>
  604. </header>
  605. <!-- Main Workspace -->
  606. <main class="main-container">
  607. <!-- Top statistics metrics grids (3 Cards) -->
  608. <div class="category-grid" id="stats-container">
  609. <!-- Total Records -->
  610. <div class="category-card card-total" onclick="openModalForCount('total', null, 'Total Tapals')">
  611. <div class="card-left">
  612. <h3 class="card-title">Total Records</h3>
  613. <p class="card-value" id="count-total">0</p>
  614. </div>
  615. <i class="fa-solid fa-folder-open card-icon"></i>
  616. </div>
  617. <!-- Completed Tapals -->
  618. <div class="category-card card-completed"
  619. onclick="openModalForCount('completed_custom', null, 'Completed Tapals')">
  620. <div class="card-left">
  621. <h3 class="card-title">Completed Tapals</h3>
  622. <p class="card-value" id="count-completed">0</p>
  623. </div>
  624. <i class="fa-solid fa-circle-check card-icon"></i>
  625. </div>
  626. <!-- Pending Tapals -->
  627. <div class="category-card card-pending"
  628. onclick="openModalForCount('pending_custom', null, 'Pending Tapals')">
  629. <div class="card-left">
  630. <h3 class="card-title">Pending Tapals</h3>
  631. <p class="card-value" id="count-pending">0</p>
  632. </div>
  633. <i class="fa-solid fa-clock card-icon"></i>
  634. </div>
  635. </div>
  636. <div class="row g-4 mb-4">
  637. <!-- Summary breakdown Table Card -->
  638. <div class="col-lg-8">
  639. <div class="card h-100 border-0 shadow-sm" style="border-radius: 16px;">
  640. <div class="card-header bg-white py-3 border-0">
  641. <h4 class="card-title mb-0 fw-bold"
  642. style="font-family: 'Outfit', sans-serif; color: #09303d; font-size: 1.15rem;">
  643. <i class="fa-solid fa-chart-line me-2 text-primary"></i>Pending Tapals aging count
  644. </h4>
  645. </div>
  646. <div class="card-body p-3">
  647. <div class="table-responsive">
  648. <table class="table table-bordered text-center align-middle mb-0"
  649. style="font-size: 0.82rem; border-color: #e2e8f0;">
  650. <thead class="table-light text-secondary fw-bold" style="font-size: 0.75rem;">
  651. <tr id="summary-thead-tr">
  652. <th style="background-color: #f8fafc; color: #475569;">Total No of Tapals</th>
  653. <th style="background-color: #f0fdf4; color: #166534;">Completed</th>
  654. <th style="background-color: #fef2f2; color: #991b1b;">Pending</th>
  655. <th style="background-color: #fafaf9; color: #44403c;">1 to 15 Days</th>
  656. <th style="background-color: #fafaf9; color: #44403c;">16 to 30 Days</th>
  657. <th style="background-color: #fafaf9; color: #44403c;">31 to 60 Days</th>
  658. <th style="background-color: #fafaf9; color: #44403c;">61 to 90 Days</th>
  659. <th style="background-color: #fafaf9; color: #44403c;">91 to 120 Days</th>
  660. <th style="background-color: #fafaf9; color: #44403c;">Over 120 Days</th>
  661. </tr>
  662. </thead>
  663. <tbody>
  664. <tr class="fw-bold" style="font-size: 1rem; color: #1e293b;" id="summary-tbody-tr">
  665. <td id="summary-total" class="cursor-pointer text-primary"
  666. style="background-color: #f8fafc;"
  667. onclick="openModalForCount('total', null, 'Total Tapals')">0</td>
  668. <td id="summary-completed" class="cursor-pointer text-success"
  669. onclick="openModalForCount('completed_custom', null, 'Completed Tapals')">0
  670. </td>
  671. <td id="summary-pending" class="cursor-pointer text-danger"
  672. style="color: #991b1b;"
  673. onclick="openModalForCount('pending_custom', null, 'Pending Tapals')">0</td>
  674. <td id="summary-1-15" class="cursor-pointer"
  675. onclick="openModalForCount('age', [0, 15], 'Age: 1 to 15 Days')">0</td>
  676. <td id="summary-16-30" class="cursor-pointer"
  677. onclick="openModalForCount('age', [16, 30], 'Age: 16 to 30 Days')">0</td>
  678. <td id="summary-31-60" class="cursor-pointer"
  679. onclick="openModalForCount('age', [31, 60], 'Age: 31 to 60 Days')">0</td>
  680. <td id="summary-61-90" class="cursor-pointer"
  681. onclick="openModalForCount('age', [61, 90], 'Age: 61 to 90 Days')">0</td>
  682. <td id="summary-91-120" class="cursor-pointer"
  683. onclick="openModalForCount('age', [91, 120], 'Age: 91 to 120 Days')">0</td>
  684. <td id="summary-over-120" class="cursor-pointer text-danger"
  685. onclick="openModalForCount('age', [121, null], 'Age: Over 120 Days')">0</td>
  686. </tr>
  687. </tbody>
  688. </table>
  689. </div>
  690. <p class="text-muted mt-2 mb-0" style="font-size: 0.72rem; font-style: italic;">
  691. <i class="fa-solid fa-circle-info me-1"></i>Tip: Click on any count cell to filter the
  692. detailed tracker list below.
  693. </p>
  694. </div>
  695. </div>
  696. </div>
  697. <!-- Pie Chart Card -->
  698. <div class="col-lg-4">
  699. <div class="card h-100 border-0 shadow-sm" style="border-radius: 16px;">
  700. <div class="card-header bg-white py-3 border-0">
  701. <h4 class="card-title mb-0 fw-bold"
  702. style="font-family: 'Outfit', sans-serif; color: #09303d; font-size: 1.15rem;">
  703. <i class="fa-solid fa-chart-pie me-2 text-primary"></i>Status Percentage
  704. </h4>
  705. </div>
  706. <div class="card-body d-flex align-items-center justify-content-center p-2"
  707. style="position: relative; height: 180px;">
  708. <canvas id="statusPieChart" style="max-height: 100%; max-width: 100%;"></canvas>
  709. </div>
  710. </div>
  711. </div>
  712. </div>
  713. <!-- Filter Card -->
  714. <div class="filter-card">
  715. <div class="filter-grid">
  716. <!-- Category Filter -->
  717. <div class="filter-group">
  718. <label class="filter-label" for="filter-category">Category</label>
  719. <select id="filter-category" class="filter-input" onchange="handleCategoryFilterChange(this.value)">
  720. <option value="all">- All Categories -</option>
  721. </select>
  722. </div>
  723. <!-- Status Filter -->
  724. <div class="filter-group">
  725. <label class="filter-label" for="filter-status">Status</label>
  726. <select id="filter-status" class="filter-input" onchange="loadList()">
  727. <option value="">- All Statuses -</option>
  728. </select>
  729. </div>
  730. <!-- Text Search -->
  731. <div class="filter-group">
  732. <label class="filter-label" for="search-text">Search Keyword</label>
  733. <input type="text" id="search-text" class="filter-input" placeholder="Tapal No, sender, subject..."
  734. oninput="handleSearchInput(this.value)">
  735. </div>
  736. <!-- Action buttons -->
  737. <button class="btn-filter-search" onclick="loadList()">
  738. <i class="fa-solid fa-magnifying-glass"></i> Filter
  739. </button>
  740. <button class="btn-filter-reset" onclick="resetFilters()">
  741. <i class="fa-solid fa-rotate-right"></i> Reset
  742. </button>
  743. </div>
  744. </div>
  745. <!-- Table Card Section -->
  746. <div class="table-card">
  747. <div class="table-card-header">
  748. <h3 class="table-card-title">
  749. <i class="fa-solid fa-table-list text-primary"></i>
  750. <span>Tapal Tracking Details</span>
  751. <span class="active-filter-badge" id="active-category-badge">Category: All Categories</span>
  752. <span class="active-filter-badge ms-2" id="click-filter-badge"
  753. style="display: none; background-color: #fee2e2; color: #b91c1c;"></span>
  754. </h3>
  755. <div class="d-flex align-items-center gap-2">
  756. <button class="btn btn-sm btn-success fw-bold px-3 d-flex align-items-center gap-2"
  757. style="border-radius: 8px; height: 36px; font-size: 0.85rem;"
  758. onclick="exportMainTableToExcel()">
  759. <i class="fa-solid fa-file-excel"></i> Export to Excel
  760. </button>
  761. <button th:if="${isAdmin}"
  762. class="btn btn-sm btn-danger fw-bold px-3 d-flex align-items-center gap-2"
  763. style="border-radius: 8px; height: 36px; font-size: 0.85rem; margin: 0;"
  764. onclick="openDeletedRecordsModal()">
  765. <i class="fa-solid fa-trash-can"></i> Deleted Records
  766. </button>
  767. <button th:if="${isAdmin}" class="btn-add-record" onclick="openAddModal()"
  768. style="margin: 0; height: 36px;">
  769. <i class="fa-solid fa-circle-plus"></i> Add Tapal Tracking Record
  770. </button>
  771. </div>
  772. </div>
  773. <!-- Table Controls: Page size & Real-time Search -->
  774. <div class="table-controls-row">
  775. <div class="table-controls-left">
  776. <span class="text-secondary fw-semibold" style="font-size: 0.85rem;">Show</span>
  777. <select id="table-page-size" class="form-select form-select-sm" style="width: 80px;"
  778. onchange="changePageSize(this.value)">
  779. <option value="10" selected>10</option>
  780. <option value="20">20</option>
  781. <option value="30">30</option>
  782. <option value="45">45</option>
  783. <option value="60">60</option>
  784. </select>
  785. <span class="text-secondary fw-semibold" style="font-size: 0.85rem;">entries</span>
  786. </div>
  787. <div class="table-controls-right">
  788. <span class="text-secondary fw-semibold" style="font-size: 0.85rem;">Search:</span>
  789. <input type="text" id="table-search" class="form-control form-control-sm" style="width: 250px;"
  790. placeholder="Search in this table..." oninput="handleTableSearch(this.value)">
  791. </div>
  792. </div>
  793. <!-- Responsive Table Grid -->
  794. <div class="table-responsive">
  795. <table class="table grid-table table-striped table-hover align-middle" id="tracking-table">
  796. <thead>
  797. <tr>
  798. <th class="text-center" style="width: 50px;">Sr. No.</th>
  799. <th>Tapal No.</th>
  800. <th onclick="sortMainTable('tapal_received_date')"
  801. style="cursor: pointer; user-select: none;">
  802. Received Date <span id="main-sort-tapal_received_date-icon"><i
  803. class="fa-solid fa-sort ms-1" style="opacity: 0.4;"></i></span></th>
  804. <th>Request From</th>
  805. <th>Subject</th>
  806. <th class="text-center">Category</th>
  807. <th>Status</th>
  808. <th>Remarks</th>
  809. <th>OC File copy </th>
  810. <th onclick="sortMainTable('updated_date')" style="cursor: pointer; user-select: none;">Last
  811. Updated At <span id="main-sort-updated_date-icon"><i class="fa-solid fa-sort ms-1"
  812. style="opacity: 0.4;"></i></span></th>
  813. <th>Last Updated By</th>
  814. <th class="text-center" style="width: 120px;">Actions</th>
  815. </tr>
  816. </thead>
  817. <tbody id="tracking-tbody">
  818. <!-- Populated dynamically via JS -->
  819. </tbody>
  820. </table>
  821. <div id="no-records-found" class="empty-state"
  822. style="display: none; padding: 40px; text-align: center;">
  823. <i class="fa-solid fa-folder-open empty-icon"
  824. style="font-size: 3rem; color: #cbd5e1; margin-bottom: 15px;"></i>
  825. <h4>No Records Found</h4>
  826. <p class="text-secondary">No Tapal tracking records match the active criteria.</p>
  827. </div>
  828. </div>
  829. <!-- Pagination Controls -->
  830. <div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mt-3"
  831. id="pagination-controls">
  832. <div id="pagination-info" class="text-secondary" style="font-size: 0.82rem; font-weight: 500;">
  833. Showing 0 to 0 of 0 entries
  834. </div>
  835. <nav aria-label="Table Page Navigation">
  836. <ul class="pagination pagination-sm mb-0 align-items-center gap-1" id="pagination-list">
  837. <!-- Populated dynamically -->
  838. </ul>
  839. </nav>
  840. </div>
  841. </div>
  842. </main>
  843. <!-- Save / Edit Record Modal -->
  844. <div class="modal fade" id="recordModal" tabindex="-1" aria-labelledby="recordModalLabel" aria-hidden="true">
  845. <div class="modal-dialog modal-lg modal-dialog-centered">
  846. <div class="modal-content">
  847. <div class="modal-custom-header">
  848. <h5 class="modal-custom-title" id="recordModalLabel">
  849. <i class="fa-solid fa-circle-plus"></i> Add/Edit Tapal Record
  850. </h5>
  851. <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
  852. aria-label="Close"></button>
  853. </div>
  854. <div class="modal-body">
  855. <form id="recordForm" onsubmit="saveRecord(event)">
  856. <input type="hidden" id="form-id" name="id">
  857. <div class="row g-3">
  858. <!-- Tapal No. -->
  859. <div class="col-md-6">
  860. <label for="form-tapal-no" class="form-label fw-semibold text-secondary">Tapal No. <span
  861. class="text-danger">*</span></label>
  862. <input type="text" class="form-control" id="form-tapal-no"
  863. placeholder="Enter Tapal Number" required>
  864. </div>
  865. <!-- Received Date -->
  866. <div class="col-md-6">
  867. <label for="form-received-date" class="form-label fw-semibold text-secondary">Received
  868. Date <span class="text-danger">*</span></label>
  869. <input type="date" class="form-control" id="form-received-date" required
  870. onclick="this.showPicker()">
  871. </div>
  872. <!-- Request From -->
  873. <div class="col-md-6">
  874. <label for="form-request-from" class="form-label fw-semibold text-secondary">Request
  875. From <span class="text-danger">*</span></label>
  876. <input type="text" class="form-control" id="form-request-from"
  877. placeholder="Enter Sender / Request From" required>
  878. </div>
  879. <!-- Category -->
  880. <div class="col-md-6">
  881. <label for="form-category" class="form-label fw-semibold text-secondary">Category <span
  882. class="text-danger">*</span></label>
  883. <select class="form-select" id="form-category" required>
  884. </select>
  885. </div>
  886. <!-- Subject -->
  887. <div class="col-md-12">
  888. <label for="form-subject" class="form-label fw-semibold text-secondary">Subject <span
  889. class="text-danger">*</span></label>
  890. <input type="text" class="form-control" id="form-subject"
  891. placeholder="Enter subject/matter" required>
  892. </div>
  893. <!-- Status -->
  894. <div class="col-md-6">
  895. <label for="form-status" class="form-label fw-semibold text-secondary">Status <span
  896. class="text-danger">*</span></label>
  897. <select class="form-select" id="form-status" required>
  898. </select>
  899. </div>
  900. <!-- Tapal File Attachment -->
  901. <div class="col-md-6">
  902. <label for="form-tapal-file" class="form-label fw-semibold text-secondary">Tapal File
  903. Attachment (Optional)</label>
  904. <input type="file" class="form-control" id="form-tapal-file" accept=".pdf,image/*">
  905. <div id="form-existing-tapal-container" class="mt-2" style="display: none;">
  906. <span class="text-secondary fw-semibold">Current Tapal File:</span>
  907. <a id="form-existing-tapal-link" href="#" target="_blank"
  908. class="ms-1 text-primary fw-bold text-decoration-none">
  909. <i class="fa-solid fa-file-pdf"></i> View File
  910. </a>
  911. </div>
  912. </div>
  913. <!-- OC File Attachment -->
  914. <div class="col-md-6">
  915. <label for="form-oc-file" class="form-label fw-semibold text-secondary">OC File
  916. Attachment (Optional)</label>
  917. <input type="file" class="form-control" id="form-oc-file" accept=".pdf,image/*">
  918. <div id="form-existing-oc-container" class="mt-2" style="display: none;">
  919. <span class="text-secondary fw-semibold">Current OC File:</span>
  920. <a id="form-existing-oc-link" href="#" target="_blank"
  921. class="ms-1 text-info fw-bold text-decoration-none">
  922. <i class="fa-solid fa-file-pdf"></i> View OC File
  923. </a>
  924. </div>
  925. </div>
  926. <!-- Remarks -->
  927. <div class="col-md-12">
  928. <label for="form-remarks" class="form-label fw-semibold text-secondary">Remarks</label>
  929. <textarea class="form-control" id="form-remarks" rows="2"
  930. placeholder="Additional remarks..."></textarea>
  931. </div>
  932. </div>
  933. <div class="d-flex justify-content-end gap-2 mt-4">
  934. <button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">Cancel</button>
  935. <button type="submit" class="btn btn-primary px-4"
  936. style="background-color: #0f5c78; border-color: #0f5c78;">
  937. <i class="fa-solid fa-save"></i> Save Record
  938. </button>
  939. </div>
  940. </form>
  941. </div>
  942. </div>
  943. </div>
  944. </div>
  945. <!-- History Modal -->
  946. <div class="modal fade" id="historyModal" tabindex="-1" aria-labelledby="historyModalLabel" aria-hidden="true">
  947. <div class="modal-dialog modal-xl modal-dialog-centered">
  948. <div class="modal-content">
  949. <div class="modal-custom-header" style="background-color: #2c3e50;">
  950. <h5 class="modal-custom-title" id="historyModalLabel">
  951. <i class="fa-solid fa-history"></i> Update History Log
  952. </h5>
  953. <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
  954. aria-label="Close"></button>
  955. </div>
  956. <div class="modal-body p-4">
  957. <div id="history-details-section" class="mb-4 p-3 bg-light rounded border">
  958. <h6 class="fw-bold mb-2">Tapal Reference Info</h6>
  959. <div class="row g-2" style="font-size: 0.85rem;">
  960. <div class="col-md-4"><span class="text-secondary fw-semibold">Tapal No:</span> <span
  961. id="hist-ref-no" class="fw-bold">-</span></div>
  962. <div class="col-md-4"><span class="text-secondary fw-semibold">Sender:</span> <span
  963. id="hist-ref-sender">-</span></div>
  964. <div class="col-md-4"><span class="text-secondary fw-semibold">Subject:</span> <span
  965. id="hist-ref-subject">-</span></div>
  966. </div>
  967. </div>
  968. <div class="table-responsive" style="max-height: 450px;">
  969. <table class="table table-bordered table-hover align-middle mb-0" style="font-size: 0.8rem;">
  970. <thead class="table-light sticky-top" style="z-index: 1;">
  971. <tr>
  972. <th style="width: 50px;" class="text-center">S.No</th>
  973. <th>Received Date</th>
  974. <th>Subject</th>
  975. <th>Category</th>
  976. <th>Request From</th>
  977. <th>Status</th>
  978. <th>Remarks</th>
  979. <th>Last Updated At</th>
  980. <th>Last Updated By</th>
  981. <th class="text-center" style="width: 150px;">Documents</th>
  982. </tr>
  983. </thead>
  984. <tbody id="history-tbody">
  985. <!-- Populated dynamically -->
  986. </tbody>
  987. </table>
  988. <div id="no-history-found" class="text-center text-muted py-5" style="display: none;">
  989. <i class="fa-solid fa-history fs-1 mb-3 text-secondary" style="opacity: 0.5;"></i>
  990. <h5>No History Found</h5>
  991. <p>No historical updates recorded for this Tapal record.</p>
  992. </div>
  993. </div>
  994. </div>
  995. </div>
  996. </div>
  997. </div>
  998. <!-- Deleted Records Modal -->
  999. <div class="modal fade" id="deletedRecordsModal" tabindex="-1" aria-labelledby="deletedRecordsModalLabel"
  1000. aria-hidden="true">
  1001. <div class="modal-dialog modal-xl modal-dialog-centered">
  1002. <div class="modal-content">
  1003. <div class="modal-custom-header" style="background-color: #c0392b;">
  1004. <h5 class="modal-custom-title" id="deletedRecordsModalLabel">
  1005. <i class="fa-solid fa-trash-can"></i> Deleted Tapal Records
  1006. </h5>
  1007. <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
  1008. aria-label="Close"></button>
  1009. </div>
  1010. <div class="modal-body p-4">
  1011. <div class="table-responsive" style="max-height: 450px;">
  1012. <table class="table table-bordered table-striped align-middle mb-0" style="font-size: 0.8rem;">
  1013. <thead class="table-light sticky-top" style="z-index: 1;">
  1014. <tr>
  1015. <th class="text-center" style="width: 50px;">S.No</th>
  1016. <th>Tapal No.</th>
  1017. <th>Received Date</th>
  1018. <th>Request From</th>
  1019. <th>Subject</th>
  1020. <th>Category</th>
  1021. <th>Status</th>
  1022. <th>Deletion Remarks</th>
  1023. <th>Deleted Date</th>
  1024. <th>Deleted By</th>
  1025. <th class="text-center" style="width: 130px;">Actions</th>
  1026. </tr>
  1027. </thead>
  1028. <tbody id="deleted-tracking-tbody">
  1029. <!-- Populated dynamically via JS -->
  1030. </tbody>
  1031. </table>
  1032. <div id="deleted-no-records-found" class="empty-state text-center py-5" style="display: none;">
  1033. <i class="fa-solid fa-trash-arrow-up empty-icon"
  1034. style="font-size: 2.5rem; color: #cbd5e1; margin-bottom: 10px;"></i>
  1035. <h5>No Deleted Records</h5>
  1036. <p>No deleted Tapal tracking records found in the database.</p>
  1037. </div>
  1038. </div>
  1039. </div>
  1040. </div>
  1041. </div>
  1042. </div>
  1043. <!-- Count Details List Modal -->
  1044. <div class="modal fade" id="countDetailsModal" tabindex="-1" aria-labelledby="countDetailsModalLabel"
  1045. aria-hidden="true">
  1046. <div class="modal-dialog modal-xl modal-dialog-centered">
  1047. <div class="modal-content">
  1048. <div class="modal-custom-header" style="background-color: #0f5c78;">
  1049. <h5 class="modal-custom-title" id="countDetailsModalLabel">
  1050. <i class="fa-solid fa-file-invoice"></i> Tapal Details - <span
  1051. id="countDetailsModalTypeTitle">Total No of Tapals</span>
  1052. </h5>
  1053. <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
  1054. aria-label="Close"></button>
  1055. </div>
  1056. <div class="modal-body p-4">
  1057. <div class="table-responsive" style="max-height: 450px;">
  1058. <table class="table table-bordered table-hover align-middle mb-0" style="font-size: 0.8rem;">
  1059. <thead class="table-light sticky-top" style="z-index: 1;">
  1060. <tr>
  1061. <th class="text-center" style="width: 50px;">Sr. No.</th>
  1062. <th>Tapal No.</th>
  1063. <th>Received Date</th>
  1064. <th>Request From</th>
  1065. <th>Subject</th>
  1066. <th>Category</th>
  1067. <th>Status</th>
  1068. <th>Remarks</th>
  1069. <th>Last Updated Date</th>
  1070. <th>Last Updated By</th>
  1071. <th class="text-center" style="width: 120px;">Actions</th>
  1072. </tr>
  1073. </thead>
  1074. <tbody id="modal-tracking-tbody">
  1075. <!-- Populated dynamically -->
  1076. </tbody>
  1077. </table>
  1078. <div id="modal-no-records-found" class="empty-state text-center py-5" style="display: none;">
  1079. <i class="fa-solid fa-folder-open empty-icon"
  1080. style="font-size: 2.5rem; color: #cbd5e1; margin-bottom: 10px;"></i>
  1081. <h5>No Records Found</h5>
  1082. <p>No Tapal tracking records match the active criteria.</p>
  1083. </div>
  1084. </div>
  1085. </div>
  1086. </div>
  1087. </div>
  1088. </div>
  1089. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
  1090. <!-- Custom Page Scripting -->
  1091. <script th:inline="javascript">
  1092. const contextPath = /*[[@{/}]]*/ '';
  1093. const isAdmin = /*[[${isAdmin}]]*/ false;
  1094. let activeCategory = 'all';
  1095. let allRecords = [];
  1096. let allBackendData = [];
  1097. function getLocalDateString(dateTimeStr) {
  1098. if (!dateTimeStr) return null;
  1099. try {
  1100. const d = new Date(dateTimeStr);
  1101. if (isNaN(d.getTime())) return null;
  1102. const dd = String(d.getDate()).padStart(2, '0');
  1103. const MM = String(d.getMonth() + 1).padStart(2, '0');
  1104. const yyyy = d.getFullYear();
  1105. return `${yyyy}-${MM}-${dd}`;
  1106. } catch (e) {
  1107. return null;
  1108. }
  1109. }
  1110. let currentFilteredRecords = [];
  1111. let recordModalObj = null;
  1112. let historyModalObj = null;
  1113. let deletedRecordsModalObj = null;
  1114. let countDetailsModalObj = null;
  1115. let currentPage = 1;
  1116. let pageSize = 20;
  1117. let activeClickFilter = null;
  1118. let statusChartInstance = null;
  1119. let activeTypes = [];
  1120. let activeStatus = [];
  1121. document.addEventListener('DOMContentLoaded', async () => {
  1122. recordModalObj = new bootstrap.Modal(document.getElementById('recordModal'));
  1123. historyModalObj = new bootstrap.Modal(document.getElementById('historyModal'));
  1124. deletedRecordsModalObj = new bootstrap.Modal(document.getElementById('deletedRecordsModal'));
  1125. countDetailsModalObj = new bootstrap.Modal(document.getElementById('countDetailsModal'));
  1126. await loadTypesAndVendors();
  1127. loadCounts();
  1128. loadList();
  1129. });
  1130. // Helper to map category name to a normalized filter key
  1131. function getCategoryCode(typeName) {
  1132. return String(typeName).toLowerCase().trim();
  1133. }
  1134. async function loadTypesAndVendors() {
  1135. try {
  1136. const [typesRes, statusRes] = await Promise.all([
  1137. fetch(contextPath + 'gcctapaldetailsmanagement/api/categories?_=' + new Date().getTime()),
  1138. fetch(contextPath + 'gcctapaldetailsmanagement/api/statuslist?_=' + new Date().getTime())
  1139. ]);
  1140. activeTypes = await typesRes.json();
  1141. const allFetched = await statusRes.json();
  1142. activeStatus = allFetched.filter(s => {
  1143. const name = String(s.status_name).toLowerCase().trim();
  1144. return !name.includes('duplicate') && !name.includes('no action') && !name.includes('action required');
  1145. });
  1146. populateDropdowns();
  1147. } catch (err) {
  1148. console.error("Error loading categories or statuses:", err);
  1149. }
  1150. }
  1151. function getStatusKey(statusName) {
  1152. if (!statusName) return 'default';
  1153. const name = String(statusName).toLowerCase().trim();
  1154. if (name.includes('complete') || name.includes('approve') || name.includes('paid')) {
  1155. return 'completed';
  1156. }
  1157. if (name.includes('awaiting') || name.includes('details')) {
  1158. return 'awaiting details';
  1159. }
  1160. if (name.includes('dept') || name.includes('uac') || name.includes('fm unit')) {
  1161. return 'pending with dept';
  1162. }
  1163. if (name.includes('pending') || name.includes('initiated') || name.includes('eoffice') || name.includes('e-office')) {
  1164. return 'pending';
  1165. }
  1166. return 'default';
  1167. }
  1168. const statusColors = {
  1169. 'completed': { bg: '#f0fdf4', text: '#166534', badgeClass: 'text-success-dark' },
  1170. 'awaiting details': { bg: '#fffbeb', text: '#92400e', badgeClass: 'text-warning-dark' },
  1171. 'pending with dept': { bg: '#f0f9ff', text: '#075985', badgeClass: 'text-info-dark' },
  1172. 'pending': { bg: '#fef2f2', text: '#991b1b', badgeClass: 'text-danger-dark' }
  1173. };
  1174. function getStatusStyle(statusName) {
  1175. const key = getStatusKey(statusName);
  1176. return statusColors[key] || { bg: '#f1f5f9', text: '#475569', badgeClass: 'text-secondary' };
  1177. }
  1178. function populateDropdowns() {
  1179. // Populate filter-category
  1180. const filterCatSelect = document.getElementById('filter-category');
  1181. if (filterCatSelect) {
  1182. filterCatSelect.innerHTML = '<option value="all">- All Categories -</option>';
  1183. activeTypes.forEach(t => {
  1184. const catCode = getCategoryCode(t.category_name);
  1185. const opt = document.createElement('option');
  1186. opt.value = catCode;
  1187. opt.textContent = t.category_name;
  1188. filterCatSelect.appendChild(opt);
  1189. });
  1190. }
  1191. // Populate form-category
  1192. const formCatSelect = document.getElementById('form-category');
  1193. if (formCatSelect) {
  1194. formCatSelect.innerHTML = '<option value="">-- Select Category --</option>';
  1195. activeTypes.forEach(t => {
  1196. const opt = document.createElement('option');
  1197. opt.value = t.id;
  1198. opt.textContent = t.category_name;
  1199. formCatSelect.appendChild(opt);
  1200. });
  1201. }
  1202. // Populate filter-status
  1203. const filterStatusSelect = document.getElementById('filter-status');
  1204. if (filterStatusSelect) {
  1205. filterStatusSelect.innerHTML = '<option value="">- All Statuses -</option>';
  1206. activeStatus.forEach(s => {
  1207. const opt = document.createElement('option');
  1208. opt.value = s.id;
  1209. opt.textContent = s.status_name;
  1210. filterStatusSelect.appendChild(opt);
  1211. });
  1212. }
  1213. // Populate form-status
  1214. const formStatusSelect = document.getElementById('form-status');
  1215. if (formStatusSelect) {
  1216. formStatusSelect.innerHTML = '<option value="">-- Select Status --</option>';
  1217. activeStatus.forEach(s => {
  1218. const opt = document.createElement('option');
  1219. opt.value = s.id;
  1220. opt.textContent = s.status_name;
  1221. formStatusSelect.appendChild(opt);
  1222. });
  1223. }
  1224. }
  1225. function getStatusName(statusVal) {
  1226. if (statusVal === null || statusVal === undefined || String(statusVal).trim() === '') {
  1227. return '-';
  1228. }
  1229. const cleanVal = String(statusVal).trim();
  1230. const statusObj = activeStatus.find(s => String(s.id) === cleanVal);
  1231. if (statusObj) {
  1232. return statusObj.status_name;
  1233. }
  1234. return statusVal;
  1235. }
  1236. function showLoader(show, text = 'Processing...') {
  1237. const overlay = document.getElementById('loading-overlay');
  1238. const overlayText = document.getElementById('loading-text');
  1239. if (overlay) {
  1240. overlayText.textContent = text;
  1241. overlay.style.display = show ? 'flex' : 'none';
  1242. }
  1243. }
  1244. async function loadCounts() {
  1245. try {
  1246. const response = await fetch(contextPath + 'gcctapaldetailsmanagement/api/counts?_=' + new Date().getTime());
  1247. const counts = await response.json();
  1248. // Update static cards
  1249. const countTotalEl = document.getElementById('count-total');
  1250. if (countTotalEl) countTotalEl.textContent = counts.total || 0;
  1251. const countCompletedEl = document.getElementById('count-completed');
  1252. if (countCompletedEl) countCompletedEl.textContent = counts.completed || 0;
  1253. const countPendingEl = document.getElementById('count-pending');
  1254. if (countPendingEl) countPendingEl.textContent = counts.pending || 0;
  1255. // Render aging stats in the summary table
  1256. document.getElementById('summary-total').textContent = counts.total || 0;
  1257. document.getElementById('summary-completed').textContent = counts.completed || 0;
  1258. document.getElementById('summary-pending').textContent = counts.pending || 0;
  1259. document.getElementById('summary-1-15').textContent = counts.age_1_15 || 0;
  1260. document.getElementById('summary-16-30').textContent = counts.age_16_30 || 0;
  1261. document.getElementById('summary-31-60').textContent = counts.age_31_60 || 0;
  1262. document.getElementById('summary-61-90').textContent = counts.age_61_90 || 0;
  1263. document.getElementById('summary-91-120').textContent = counts.age_91_120 || 0;
  1264. document.getElementById('summary-over-120').textContent = counts.age_over_120 || 0;
  1265. renderPieChart(counts);
  1266. } catch (err) {
  1267. console.error("Error loading counts:", err);
  1268. }
  1269. }
  1270. function renderPieChart(counts) {
  1271. const ctx = document.getElementById('statusPieChart');
  1272. if (!ctx) return;
  1273. const completed = counts.completed || 0;
  1274. const pending = counts.pending || 0;
  1275. const total = counts.total || 0;
  1276. if (total === 0) {
  1277. if (statusChartInstance) statusChartInstance.destroy();
  1278. return;
  1279. }
  1280. const data = {
  1281. labels: ['Completed', 'Pending'],
  1282. datasets: [{
  1283. data: [completed, pending],
  1284. backgroundColor: ['#22c55e', '#ef4444'],
  1285. hoverOffset: 6,
  1286. borderWidth: 2,
  1287. borderColor: '#ffffff'
  1288. }]
  1289. };
  1290. if (statusChartInstance) {
  1291. statusChartInstance.data = data;
  1292. statusChartInstance.update();
  1293. } else {
  1294. statusChartInstance = new Chart(ctx, {
  1295. type: 'doughnut',
  1296. data: data,
  1297. options: {
  1298. responsive: true,
  1299. maintainAspectRatio: false,
  1300. plugins: {
  1301. legend: {
  1302. position: 'bottom',
  1303. labels: {
  1304. boxWidth: 12,
  1305. font: { size: 10, family: 'Poppins' }
  1306. }
  1307. }
  1308. },
  1309. cutout: '70%'
  1310. }
  1311. });
  1312. }
  1313. }
  1314. async function handleCardClick(categoryKey) {
  1315. activeCategory = categoryKey;
  1316. // Sync filter dropdown selection
  1317. const filterCatSelect = document.getElementById('filter-category');
  1318. if (filterCatSelect) {
  1319. filterCatSelect.value = categoryKey;
  1320. }
  1321. // Highlight card
  1322. document.querySelectorAll('.stat-card').forEach(card => card.classList.remove('active'));
  1323. const selectedCard = document.getElementById('card-' + categoryKey);
  1324. if (selectedCard) {
  1325. selectedCard.classList.add('active');
  1326. }
  1327. // Set Title Badge text
  1328. const badge = document.getElementById('active-category-badge');
  1329. if (badge) {
  1330. if (categoryKey === 'all') {
  1331. badge.textContent = 'Category: All Categories';
  1332. } else if (categoryKey === 'status_not_updated') {
  1333. badge.textContent = 'Category: Category Not Updated';
  1334. } else {
  1335. const matched = activeTypes.find(t => getCategoryCode(t.category_name) === categoryKey);
  1336. badge.textContent = `Category: ${matched ? matched.category_name : categoryKey}`;
  1337. }
  1338. }
  1339. currentPage = 1;
  1340. loadList();
  1341. }
  1342. function handleCategoryFilterChange(categoryKey) {
  1343. handleCardClick(categoryKey);
  1344. }
  1345. let searchTimeout = null;
  1346. function handleSearchInput(query) {
  1347. clearTimeout(searchTimeout);
  1348. searchTimeout = setTimeout(() => {
  1349. currentPage = 1;
  1350. loadList();
  1351. }, 300);
  1352. }
  1353. function handleTableSearch(query) {
  1354. handleSearchInput(query);
  1355. }
  1356. // Sorting controls
  1357. let sortColumn = 'tapal_received_date';
  1358. let sortDirection = 'DESC';
  1359. function sortMainTable(colName) {
  1360. if (sortColumn === colName) {
  1361. sortDirection = (sortDirection === 'ASC') ? 'DESC' : 'ASC';
  1362. } else {
  1363. sortColumn = colName;
  1364. sortDirection = 'DESC';
  1365. }
  1366. // Update header sort icons
  1367. document.querySelectorAll('.grid-table th i').forEach(icon => {
  1368. icon.className = 'fa-solid fa-sort ms-1';
  1369. icon.style.opacity = '0.4';
  1370. });
  1371. const activeIcon = document.getElementById(`main-sort-${colName}-icon`)?.querySelector('i');
  1372. if (activeIcon) {
  1373. activeIcon.className = (sortDirection === 'ASC') ? 'fa-solid fa-sort-up ms-1' : 'fa-solid fa-sort-down ms-1';
  1374. activeIcon.style.opacity = '1';
  1375. }
  1376. loadList();
  1377. }
  1378. async function loadList() {
  1379. showLoader(true, 'Fetching list data...');
  1380. const params = new URLSearchParams();
  1381. if (activeCategory !== 'all') {
  1382. params.append('category', activeCategory);
  1383. }
  1384. const statusSelect = document.getElementById('filter-status');
  1385. if (statusSelect && statusSelect.value) {
  1386. params.append('status', statusSelect.value);
  1387. }
  1388. try {
  1389. const response = await fetch(contextPath + 'gcctapaldetailsmanagement/api/list?' + params.toString());
  1390. allBackendData = await response.json();
  1391. allRecords = [...allBackendData];
  1392. updateLatestUpdateBadge();
  1393. // Client-side local filtering based on Search Keywords and click badge criteria
  1394. filterAndRender();
  1395. } catch (err) {
  1396. console.error("Error loading records:", err);
  1397. showLoader(false);
  1398. }
  1399. }
  1400. function filterAndRender() {
  1401. let records = [...allRecords];
  1402. // 1. Search Query Local Filter
  1403. const searchVal = document.getElementById('search-text')?.value.toLowerCase().trim() || '';
  1404. const tblSearchVal = document.getElementById('table-search')?.value.toLowerCase().trim() || '';
  1405. const activeQuery = searchVal || tblSearchVal;
  1406. if (activeQuery) {
  1407. records = records.filter(row => {
  1408. return (row.tapal_no && String(row.tapal_no).toLowerCase().includes(activeQuery)) ||
  1409. (row.request_from && String(row.request_from).toLowerCase().includes(activeQuery)) ||
  1410. (row.subject && String(row.subject).toLowerCase().includes(activeQuery)) ||
  1411. (row.remarks && String(row.remarks).toLowerCase().includes(activeQuery));
  1412. });
  1413. }
  1414. // 2. Click statistics badge local filter (Total, Completed, Pending, Age Groups)
  1415. const clickBadge = document.getElementById('click-filter-badge');
  1416. if (activeClickFilter) {
  1417. clickBadge.style.display = 'inline-block';
  1418. clickBadge.innerHTML = `${activeClickFilter.label} <i class="fa-solid fa-circle-xmark ms-1 cursor-pointer" onclick="clearClickFilter()"></i>`;
  1419. const type = activeClickFilter.type;
  1420. const criteria = activeClickFilter.criteria;
  1421. if (type === 'completed_custom') {
  1422. records = records.filter(row => {
  1423. const rem = row.remarks ? String(row.remarks).trim() : '';
  1424. return rem !== '' && rem !== '-';
  1425. });
  1426. } else if (type === 'pending_custom') {
  1427. records = records.filter(row => {
  1428. const rem = row.remarks ? String(row.remarks).trim() : '';
  1429. return rem === '' || rem === '-';
  1430. });
  1431. } else if (type === 'age') {
  1432. const min = criteria[0];
  1433. const max = criteria[1];
  1434. records = records.filter(row => {
  1435. const rem = row.remarks ? String(row.remarks).trim() : '';
  1436. const isPending = (rem === '' || rem === '-');
  1437. if (!isPending) return false;
  1438. const age = calculateAgeDays(row.tapal_received_date);
  1439. if (max === null) {
  1440. return age >= min;
  1441. }
  1442. return age >= min && age <= max;
  1443. });
  1444. }
  1445. } else {
  1446. clickBadge.style.display = 'none';
  1447. }
  1448. // 3. Client Sort
  1449. records.sort((a, b) => {
  1450. let valA = a[sortColumn];
  1451. let valB = b[sortColumn];
  1452. if (valA === null || valA === undefined) valA = '';
  1453. if (valB === null || valB === undefined) valB = '';
  1454. // Handle string dates comparison
  1455. if (sortColumn.includes('date')) {
  1456. const dateA = new Date(valA).getTime() || 0;
  1457. const dateB = new Date(valB).getTime() || 0;
  1458. return (sortDirection === 'ASC') ? dateA - dateB : dateB - dateA;
  1459. }
  1460. if (typeof valA === 'string') {
  1461. return (sortDirection === 'ASC')
  1462. ? valA.localeCompare(valB, undefined, { numeric: true, sensitivity: 'base' })
  1463. : valB.localeCompare(valA, undefined, { numeric: true, sensitivity: 'base' });
  1464. }
  1465. return (sortDirection === 'ASC') ? valA - valB : valB - valA;
  1466. });
  1467. currentFilteredRecords = records;
  1468. renderTablePage();
  1469. }
  1470. function calculateAgeDays(dateStr) {
  1471. if (!dateStr) return 0;
  1472. const diffTime = Math.abs(new Date() - new Date(dateStr));
  1473. return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) - 1;
  1474. }
  1475. function clearClickFilter() {
  1476. activeClickFilter = null;
  1477. filterAndRender();
  1478. }
  1479. function openModalForCount(type, criteria, label) {
  1480. const params = new URLSearchParams();
  1481. if (type) params.append('clickType', type);
  1482. if (criteria) params.append('clickValue', Array.isArray(criteria) ? criteria.join(',') : criteria);
  1483. if (label) params.append('title', label);
  1484. if (activeCategory && activeCategory !== 'all') {
  1485. params.append('category', activeCategory);
  1486. }
  1487. window.location.href = contextPath + 'gcctapaldetailsmanagement/details?' + params.toString();
  1488. }
  1489. function renderTablePage() {
  1490. const tbody = document.getElementById('tracking-tbody');
  1491. if (!tbody) return;
  1492. tbody.innerHTML = '';
  1493. const total = currentFilteredRecords.length;
  1494. if (total === 0) {
  1495. document.getElementById('no-records-found').style.display = 'block';
  1496. document.getElementById('pagination-controls').style.display = 'none';
  1497. showLoader(false);
  1498. return;
  1499. }
  1500. document.getElementById('no-records-found').style.display = 'none';
  1501. document.getElementById('pagination-controls').style.display = 'flex';
  1502. const totalPages = Math.ceil(total / pageSize);
  1503. if (currentPage > totalPages) currentPage = totalPages;
  1504. if (currentPage < 1) currentPage = 1;
  1505. const startIdx = (currentPage - 1) * pageSize;
  1506. const endIdx = Math.min(startIdx + pageSize, total);
  1507. const pageRecords = currentFilteredRecords.slice(startIdx, endIdx);
  1508. pageRecords.forEach((row, index) => {
  1509. const tr = document.createElement('tr');
  1510. const dispStatus = String(row.resolved_status || getStatusName(row.status_id) || '').trim();
  1511. const statusStyle = getStatusStyle(dispStatus);
  1512. let tapalDocBtn = '-';
  1513. if (row.tapal_file_path) {
  1514. tapalDocBtn = `<a href="${contextPath + (row.tapal_file_path.startsWith('/') ? row.tapal_file_path.substring(1) : row.tapal_file_path)}" target="_blank" class="btn btn-sm btn-outline-primary py-0 px-2" style="font-size: 0.75rem;" title="View PDF"><i class="fa-solid fa-file-pdf"></i> View</a>`;
  1515. }
  1516. let ocDocBtn = '-';
  1517. if (row.oc_file_path) {
  1518. ocDocBtn = `<a href="${contextPath + (row.oc_file_path.startsWith('/') ? row.oc_file_path.substring(1) : row.oc_file_path)}" target="_blank" class="btn btn-sm btn-outline-info py-0 px-2" style="font-size: 0.75rem;" title="View OC File"><i class="fa-solid fa-file-image"></i> View</a>`;
  1519. }
  1520. tr.innerHTML = `
  1521. <td class="text-center fw-bold">${startIdx + index + 1}</td>
  1522. <td class="fw-semibold text-dark">${escapeHtml(text(row.tapal_no))}</td>
  1523. <td>${formatDate(row.tapal_received_date)}</td>
  1524. <td class="fw-semibold text-secondary">${escapeHtml(text(row.request_from))}</td>
  1525. <td>${escapeHtml(text(row.subject))}</td>
  1526. <td><span class="type-badge type-emd">${escapeHtml(text(row.resolved_category || row.cat_type_id))}</span></td>
  1527. <td><span class="status-badge" style="background-color: ${statusStyle.bg}; color: ${statusStyle.text};">${escapeHtml(dispStatus)}</span></td>
  1528. <td><small class="text-muted">${escapeHtml(text(row.remarks))}</small></td>
  1529. <td>${ocDocBtn}</td>
  1530. <td>${formatDateTime(row.updated_date)}</td>
  1531. <td class="fw-semibold text-secondary">${escapeHtml(text(row.updated_by_name || '-'))}</td>
  1532. `;
  1533. // Actions cell creation
  1534. const tdActions = document.createElement('td');
  1535. tdActions.className = 'text-center';
  1536. if (row.tapal_file_path) {
  1537. const btnViewPdf = document.createElement('button');
  1538. btnViewPdf.className = 'btn-grid-action btn-grid-pdf me-1';
  1539. btnViewPdf.innerHTML = '<i class="fa-solid fa-file-pdf"></i>';
  1540. btnViewPdf.title = 'View Tapal File';
  1541. btnViewPdf.onclick = () => {
  1542. let pdfUrl = row.tapal_file_path;
  1543. window.open(contextPath + (pdfUrl.startsWith('/') ? pdfUrl.substring(1) : pdfUrl), '_blank');
  1544. };
  1545. tdActions.appendChild(btnViewPdf);
  1546. }
  1547. if (isAdmin) {
  1548. const btnEdit = document.createElement('button');
  1549. btnEdit.className = 'btn-grid-action btn-grid-edit me-1';
  1550. btnEdit.innerHTML = '<i class="fa-solid fa-pen-to-square"></i>';
  1551. btnEdit.title = 'Edit Record';
  1552. btnEdit.onclick = () => openEditModal(row);
  1553. tdActions.appendChild(btnEdit);
  1554. const btnDelete = document.createElement('button');
  1555. btnDelete.className = 'btn-grid-action btn-grid-delete me-1';
  1556. btnDelete.innerHTML = '<i class="fa-solid fa-trash-can"></i>';
  1557. btnDelete.title = 'Delete Record';
  1558. btnDelete.onclick = () => confirmDeleteRecord(row.id);
  1559. tdActions.appendChild(btnDelete);
  1560. }
  1561. // History button
  1562. const btnHistory = document.createElement('button');
  1563. btnHistory.className = 'btn-grid-action btn-outline-secondary me-1';
  1564. btnHistory.style.border = '1px solid #64748b';
  1565. btnHistory.style.color = '#64748b';
  1566. btnHistory.style.backgroundColor = 'transparent';
  1567. btnHistory.innerHTML = '<i class="fa-solid fa-history"></i>';
  1568. btnHistory.title = 'View History Log';
  1569. btnHistory.onclick = () => showHistoryLog(row.tapal_no, row.request_from, row.subject);
  1570. tdActions.appendChild(btnHistory);
  1571. tr.appendChild(tdActions);
  1572. tbody.appendChild(tr);
  1573. });
  1574. // Pagination info
  1575. document.getElementById('pagination-info').textContent = `Showing ${startIdx + 1} to ${endIdx} of ${total} entries`;
  1576. renderPagination(totalPages);
  1577. showLoader(false);
  1578. }
  1579. function renderPagination(totalPages) {
  1580. const list = document.getElementById('pagination-list');
  1581. list.innerHTML = '';
  1582. // Previous page
  1583. const prevLi = document.createElement('li');
  1584. prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
  1585. prevLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="changePage(${currentPage - 1})"><i class="fa-solid fa-chevron-left"></i></a>`;
  1586. list.appendChild(prevLi);
  1587. // Page numbers
  1588. const maxPageButtons = 5;
  1589. let startPage = Math.max(1, currentPage - 2);
  1590. let endPage = Math.min(totalPages, startPage + maxPageButtons - 1);
  1591. if (endPage - startPage < maxPageButtons - 1) {
  1592. startPage = Math.max(1, endPage - maxPageButtons + 1);
  1593. }
  1594. for (let i = startPage; i <= endPage; i++) {
  1595. const li = document.createElement('li');
  1596. li.className = `page-item ${currentPage === i ? 'active' : ''}`;
  1597. li.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="changePage(${i})">${i}</a>`;
  1598. list.appendChild(li);
  1599. }
  1600. // Next page
  1601. const nextLi = document.createElement('li');
  1602. nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
  1603. nextLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="changePage(${currentPage + 1})"><i class="fa-solid fa-chevron-right"></i></a>`;
  1604. list.appendChild(nextLi);
  1605. }
  1606. function changePage(page) {
  1607. currentPage = page;
  1608. renderTablePage();
  1609. }
  1610. function changePageSize(size) {
  1611. pageSize = parseInt(size);
  1612. currentPage = 1;
  1613. renderTablePage();
  1614. }
  1615. // Add modal handling
  1616. function openAddModal() {
  1617. document.getElementById('recordForm').reset();
  1618. document.getElementById('form-id').value = '';
  1619. document.getElementById('form-existing-tapal-container').style.display = 'none';
  1620. document.getElementById('form-existing-oc-container').style.display = 'none';
  1621. document.getElementById('recordModalLabel').innerHTML = '<i class="fa-solid fa-circle-plus"></i> Add Tapal Record';
  1622. recordModalObj.show();
  1623. }
  1624. // Edit modal handling
  1625. function openEditModal(row) {
  1626. document.getElementById('recordForm').reset();
  1627. document.getElementById('form-id').value = row.id;
  1628. document.getElementById('form-tapal-no').value = row.tapal_no || '';
  1629. document.getElementById('form-received-date').value = getLocalDateString(row.tapal_received_date) || '';
  1630. document.getElementById('form-request-from').value = row.request_from || '';
  1631. document.getElementById('form-category').value = row.cat_type_id || '';
  1632. document.getElementById('form-subject').value = row.subject || '';
  1633. document.getElementById('form-status').value = row.status_id || '';
  1634. document.getElementById('form-remarks').value = row.remarks || '';
  1635. // Handle Existing Tapal Attachment file links
  1636. const tapalContainer = document.getElementById('form-existing-tapal-container');
  1637. if (row.tapal_file_path) {
  1638. tapalContainer.style.display = 'block';
  1639. document.getElementById('form-existing-tapal-link').href = contextPath + (row.tapal_file_path.startsWith('/') ? row.tapal_file_path.substring(1) : row.tapal_file_path);
  1640. } else {
  1641. tapalContainer.style.display = 'none';
  1642. }
  1643. // Handle Existing OC file link
  1644. const ocContainer = document.getElementById('form-existing-oc-container');
  1645. if (row.oc_file_path) {
  1646. ocContainer.style.display = 'block';
  1647. document.getElementById('form-existing-oc-link').href = contextPath + (row.oc_file_path.startsWith('/') ? row.oc_file_path.substring(1) : row.oc_file_path);
  1648. } else {
  1649. ocContainer.style.display = 'none';
  1650. }
  1651. document.getElementById('recordModalLabel').innerHTML = '<i class="fa-solid fa-pen-to-square"></i> Edit Tapal Record';
  1652. recordModalObj.show();
  1653. }
  1654. async function saveRecord(event) {
  1655. event.preventDefault();
  1656. const id = document.getElementById('form-id').value;
  1657. const tapal_no = document.getElementById('form-tapal-no').value;
  1658. const tapal_received_date = document.getElementById('form-received-date').value;
  1659. const request_from = document.getElementById('form-request-from').value;
  1660. const cat_type_id = document.getElementById('form-category').value;
  1661. const subject = document.getElementById('form-subject').value;
  1662. const status_id = document.getElementById('form-status').value;
  1663. const remarks = document.getElementById('form-remarks').value;
  1664. showLoader(true, 'Uploading attachments...');
  1665. let tapalFilePath = '';
  1666. // If editing, preserve the old value if no new file is uploaded
  1667. if (id) {
  1668. const matched = allRecords.find(r => String(r.id) === String(id));
  1669. if (matched) tapalFilePath = matched.tapal_file_path || '';
  1670. }
  1671. let ocFilePath = '';
  1672. if (id) {
  1673. const matched = allRecords.find(r => String(r.id) === String(id));
  1674. if (matched) ocFilePath = matched.oc_file_path || '';
  1675. }
  1676. const csrfHeaders = getCsrfHeaders();
  1677. // 1. Upload Tapal File
  1678. const tapalFileInput = document.getElementById('form-tapal-file');
  1679. if (tapalFileInput && tapalFileInput.files.length > 0) {
  1680. const formData = new FormData();
  1681. formData.append('file', tapalFileInput.files[0]);
  1682. formData.append('type', 'tapal');
  1683. try {
  1684. const uploadResponse = await fetch(contextPath + 'gcctapaldetailsmanagement/api/upload-file', {
  1685. method: 'POST',
  1686. headers: csrfHeaders,
  1687. body: formData
  1688. });
  1689. const uploadResult = await uploadResponse.json();
  1690. if (uploadResult.success) {
  1691. tapalFilePath = uploadResult.filePath;
  1692. } else {
  1693. Swal.fire('Error', uploadResult.error || 'Failed to upload Tapal File.', 'error');
  1694. showLoader(false);
  1695. return;
  1696. }
  1697. } catch (err) {
  1698. console.error("Tapal File Upload error:", err);
  1699. Swal.fire('Error', "Failed to upload Tapal File due to server connection error.", 'error');
  1700. showLoader(false);
  1701. return;
  1702. }
  1703. }
  1704. // 2. Upload OC File
  1705. const ocFileInput = document.getElementById('form-oc-file');
  1706. if (ocFileInput && ocFileInput.files.length > 0) {
  1707. const formData = new FormData();
  1708. formData.append('file', ocFileInput.files[0]);
  1709. formData.append('type', 'oc');
  1710. try {
  1711. const uploadResponse = await fetch(contextPath + 'gcctapaldetailsmanagement/api/upload-file', {
  1712. method: 'POST',
  1713. headers: csrfHeaders,
  1714. body: formData
  1715. });
  1716. const uploadResult = await uploadResponse.json();
  1717. if (uploadResult.success) {
  1718. ocFilePath = uploadResult.filePath;
  1719. } else {
  1720. Swal.fire('Error', uploadResult.error || 'Failed to upload OC File.', 'error');
  1721. showLoader(false);
  1722. return;
  1723. }
  1724. } catch (err) {
  1725. console.error("OC File Upload error:", err);
  1726. Swal.fire('Error', "Failed to upload OC File due to connection error.", 'error');
  1727. showLoader(false);
  1728. return;
  1729. }
  1730. }
  1731. // 3. Save JSON Payload
  1732. const payload = {
  1733. id, tapal_no, tapal_received_date, request_from, cat_type_id,
  1734. subject, status_id, remarks, tapal_file_path: tapalFilePath, oc_file_path: ocFilePath
  1735. };
  1736. try {
  1737. showLoader(true, 'Saving details...');
  1738. const response = await fetch(contextPath + 'gcctapaldetailsmanagement/api/save', {
  1739. method: 'POST',
  1740. headers: {
  1741. 'Content-Type': 'application/json',
  1742. ...csrfHeaders
  1743. },
  1744. body: JSON.stringify(payload)
  1745. });
  1746. const data = await response.json();
  1747. if (data.success) {
  1748. recordModalObj.hide();
  1749. Swal.fire('Success', 'Tapal details saved successfully!', 'success');
  1750. loadCounts();
  1751. loadList();
  1752. } else {
  1753. Swal.fire('Error', data.error || 'Failed to save record details.', 'error');
  1754. showLoader(false);
  1755. }
  1756. } catch (err) {
  1757. console.error("Save Error:", err);
  1758. Swal.fire('Error', "Failed to save record details due to connection error.", 'error');
  1759. showLoader(false);
  1760. }
  1761. }
  1762. // Delete record confirmation
  1763. function confirmDeleteRecord(id) {
  1764. Swal.fire({
  1765. title: 'Enter Reason for Deletion',
  1766. input: 'textarea',
  1767. inputPlaceholder: 'Type deletion remarks here...',
  1768. inputAttributes: {
  1769. 'aria-label': 'Type deletion remarks here'
  1770. },
  1771. showCancelButton: true,
  1772. confirmButtonText: 'Delete Record',
  1773. confirmButtonColor: '#d33',
  1774. cancelButtonText: 'Cancel',
  1775. preConfirm: (remarks) => {
  1776. if (!remarks || !remarks.trim()) {
  1777. Swal.showValidationMessage('Deletion remarks are required!');
  1778. }
  1779. return remarks;
  1780. }
  1781. }).then((result) => {
  1782. if (result.isConfirmed) {
  1783. deleteRecordCall(id, result.value);
  1784. }
  1785. });
  1786. }
  1787. async function deleteRecordCall(id, remarks) {
  1788. showLoader(true, 'Deleting record...');
  1789. const csrfHeaders = getCsrfHeaders();
  1790. const params = new URLSearchParams();
  1791. params.append('id', id);
  1792. params.append('remarks', remarks);
  1793. try {
  1794. const response = await fetch(contextPath + 'gcctapaldetailsmanagement/api/delete?' + params.toString(), {
  1795. method: 'POST',
  1796. headers: csrfHeaders
  1797. });
  1798. const data = await response.json();
  1799. if (data.success) {
  1800. Swal.fire('Deleted!', 'Tapal record has been deleted.', 'success');
  1801. loadCounts();
  1802. loadList();
  1803. } else {
  1804. Swal.fire('Error', data.error || 'Failed to delete record.', 'error');
  1805. showLoader(false);
  1806. }
  1807. } catch (err) {
  1808. console.error("Delete Error:", err);
  1809. Swal.fire('Error', "Connection failed.", 'error');
  1810. showLoader(false);
  1811. }
  1812. }
  1813. // History Update Log modal
  1814. async function showHistoryLog(tapalNo, sender, subject) {
  1815. document.getElementById('hist-ref-no').textContent = tapalNo || '-';
  1816. document.getElementById('hist-ref-sender').textContent = sender || '-';
  1817. document.getElementById('hist-ref-subject').textContent = subject || '-';
  1818. const tbody = document.getElementById('history-tbody');
  1819. tbody.innerHTML = '';
  1820. document.getElementById('no-history-found').style.display = 'none';
  1821. try {
  1822. showLoader(true, 'Fetching logs...');
  1823. const response = await fetch(contextPath + 'gcctapaldetailsmanagement/api/history?tapalNo=' + encodeURIComponent(tapalNo));
  1824. const historyList = await response.json();
  1825. showLoader(false);
  1826. if (!historyList || historyList.length === 0) {
  1827. document.getElementById('no-history-found').style.display = 'block';
  1828. } else {
  1829. historyList.forEach((hist, index) => {
  1830. const tr = document.createElement('tr');
  1831. const dispStatus = hist.resolved_status || getStatusName(hist.status_id) || '';
  1832. let docButtons = '-';
  1833. if (hist.tapal_file_path || hist.oc_file_path) {
  1834. docButtons = '';
  1835. if (hist.tapal_file_path) {
  1836. docButtons += `<a href="${contextPath + (hist.tapal_file_path.startsWith('/') ? hist.tapal_file_path.substring(1) : hist.tapal_file_path)}" target="_blank" class="btn btn-sm btn-outline-primary py-0 px-2 me-1" style="font-size: 0.7rem;" title="View File"><i class="fa-solid fa-file-pdf"></i> File</a>`;
  1837. }
  1838. if (hist.oc_file_path) {
  1839. docButtons += `<a href="${contextPath + (hist.oc_file_path.startsWith('/') ? hist.oc_file_path.substring(1) : hist.oc_file_path)}" target="_blank" class="btn btn-sm btn-outline-info py-0 px-2" style="font-size: 0.7rem;" title="View OC File"><i class="fa-solid fa-file-image"></i> OC</a>`;
  1840. }
  1841. }
  1842. tr.innerHTML = `
  1843. <td class="text-center fw-bold">${index + 1}</td>
  1844. <td>${formatDate(hist.tapal_received_date)}</td>
  1845. <td>${escapeHtml(text(hist.subject))}</td>
  1846. <td><span class="type-badge type-emd">${escapeHtml(text(hist.resolved_category || hist.cat_type_id))}</span></td>
  1847. <td class="fw-semibold text-secondary">${escapeHtml(text(hist.request_from))}</td>
  1848. <td><span class="status-badge" style="background-color: ${getStatusStyle(dispStatus).bg}; color: ${getStatusStyle(dispStatus).text};">${escapeHtml(dispStatus)}</span></td>
  1849. <td><small class="text-muted">${escapeHtml(text(hist.remarks))}</small></td>
  1850. <td>${formatDateTime(hist.updated_date)}</td>
  1851. <td class="fw-semibold text-secondary">${escapeHtml(text(hist.updated_by_name || hist.updated_by))}</td>
  1852. <td class="text-center">${docButtons}</td>
  1853. `;
  1854. tbody.appendChild(tr);
  1855. });
  1856. }
  1857. historyModalObj.show();
  1858. } catch (err) {
  1859. console.error("History Log Fetch error:", err);
  1860. showLoader(false);
  1861. }
  1862. }
  1863. // Deleted Records modal
  1864. async function openDeletedRecordsModal() {
  1865. const tbody = document.getElementById('deleted-tracking-tbody');
  1866. tbody.innerHTML = '';
  1867. document.getElementById('deleted-no-records-found').style.display = 'none';
  1868. try {
  1869. showLoader(true, 'Fetching deleted records...');
  1870. const response = await fetch(contextPath + 'gcctapaldetailsmanagement/api/deleted-list?_=' + new Date().getTime());
  1871. const deletedList = await response.json();
  1872. showLoader(false);
  1873. if (!deletedList || deletedList.length === 0) {
  1874. document.getElementById('deleted-no-records-found').style.display = 'block';
  1875. } else {
  1876. deletedList.forEach((row, index) => {
  1877. const tr = document.createElement('tr');
  1878. const dispStatus = row.resolved_status || getStatusName(row.status_id) || '';
  1879. tr.innerHTML = `
  1880. <td class="text-center fw-bold">${index + 1}</td>
  1881. <td class="fw-bold">${escapeHtml(text(row.tapal_no))}</td>
  1882. <td>${formatDate(row.tapal_received_date)}</td>
  1883. <td class="fw-semibold text-secondary">${escapeHtml(text(row.request_from))}</td>
  1884. <td>${escapeHtml(text(row.subject))}</td>
  1885. <td><span class="type-badge type-emd">${escapeHtml(text(row.resolved_category || row.cat_type_id))}</span></td>
  1886. <td><span class="status-badge" style="background-color: ${getStatusStyle(dispStatus).bg}; color: ${getStatusStyle(dispStatus).text};">${escapeHtml(dispStatus)}</span></td>
  1887. <td class="text-danger fw-semibold"><small>${escapeHtml(text(row.remarks))}</small></td>
  1888. <td>${formatDateTime(row.updated_date)}</td>
  1889. <td class="fw-semibold text-secondary">${escapeHtml(text(row.updated_by_name || row.updated_by))}</td>
  1890. `;
  1891. const tdActions = document.createElement('td');
  1892. tdActions.className = 'text-center';
  1893. const btnRestore = document.createElement('button');
  1894. btnRestore.className = 'btn btn-xs btn-success fw-bold py-1 px-2';
  1895. btnRestore.style.fontSize = '0.72rem';
  1896. btnRestore.innerHTML = '<i class="fa-solid fa-trash-arrow-up"></i> Restore';
  1897. btnRestore.onclick = () => restoreDeletedRecord(row.id);
  1898. tdActions.appendChild(btnRestore);
  1899. tr.appendChild(tdActions);
  1900. tbody.appendChild(tr);
  1901. });
  1902. }
  1903. deletedRecordsModalObj.show();
  1904. } catch (err) {
  1905. console.error("Deleted records list load error:", err);
  1906. showLoader(false);
  1907. }
  1908. }
  1909. async function restoreDeletedRecord(id) {
  1910. showLoader(true, 'Restoring record...');
  1911. const csrfHeaders = getCsrfHeaders();
  1912. const params = new URLSearchParams();
  1913. params.append('id', id);
  1914. try {
  1915. const response = await fetch(contextPath + 'gcctapaldetailsmanagement/api/restore?' + params.toString(), {
  1916. method: 'POST',
  1917. headers: csrfHeaders
  1918. });
  1919. const data = await response.json();
  1920. if (data.success) {
  1921. deletedRecordsModalObj.hide();
  1922. Swal.fire('Restored!', 'The Tapal record has been successfully restored.', 'success');
  1923. loadCounts();
  1924. loadList();
  1925. } else {
  1926. Swal.fire('Error', data.error || 'Failed to restore record.', 'error');
  1927. showLoader(false);
  1928. }
  1929. } catch (err) {
  1930. console.error("Restore Error:", err);
  1931. Swal.fire('Error', "Failed to restore record due to connection issues.", 'error');
  1932. showLoader(false);
  1933. }
  1934. }
  1935. // Export Main Table records directly to XLSX using SheetJS
  1936. function exportMainTableToExcel() {
  1937. if (!currentFilteredRecords || currentFilteredRecords.length === 0) {
  1938. Swal.fire('Info', 'No records to export.', 'info');
  1939. return;
  1940. }
  1941. const tempTable = document.createElement('table');
  1942. const thead = document.createElement('thead');
  1943. thead.innerHTML = `
  1944. <tr>
  1945. <th>Sr. No.</th>
  1946. <th>Tapal No.</th>
  1947. <th>Received Date</th>
  1948. <th>Request From</th>
  1949. <th>Subject</th>
  1950. <th>Category</th>
  1951. <th>Status</th>
  1952. <th>Remarks</th>
  1953. <th>Tapal File Attachment</th>
  1954. <th>OC File Attachment</th>
  1955. <th>Last Updated At</th>
  1956. <th>Last Updated By</th>
  1957. </tr>
  1958. `;
  1959. tempTable.appendChild(thead);
  1960. const tbody = document.createElement('tbody');
  1961. currentFilteredRecords.forEach((row, index) => {
  1962. const tr = document.createElement('tr');
  1963. const dispStatus = String(row.resolved_status || getStatusName(row.status_id) || '').trim();
  1964. tr.innerHTML = `
  1965. <td>${index + 1}</td>
  1966. <td>${text(row.tapal_no)}</td>
  1967. <td>${formatDate(row.tapal_received_date)}</td>
  1968. <td>${text(row.request_from)}</td>
  1969. <td>${text(row.subject)}</td>
  1970. <td>${text(row.resolved_category || row.cat_type_id)}</td>
  1971. <td>${text(dispStatus)}</td>
  1972. <td>${text(row.remarks)}</td>
  1973. <td>${row.tapal_file_path ? 'Uploaded' : 'Not Uploaded'}</td>
  1974. <td>${row.oc_file_path ? 'Uploaded' : 'Not Uploaded'}</td>
  1975. <td>${formatDateTime(row.updated_date)}</td>
  1976. <td>${text(row.updated_by_name || '-')}</td>
  1977. `;
  1978. tbody.appendChild(tr);
  1979. });
  1980. tempTable.appendChild(tbody);
  1981. const wb = XLSX.utils.table_to_book(tempTable, { raw: true, sheet: "Sheet1" });
  1982. XLSX.writeFile(wb, 'Tapal_Tracking_Details_Report.xlsx');
  1983. }
  1984. function resetFilters() {
  1985. document.getElementById('search-text').value = '';
  1986. document.getElementById('table-search').value = '';
  1987. document.getElementById('filter-status').value = '';
  1988. handleCardClick('all');
  1989. }
  1990. // Helpers and Formatters
  1991. const text = val => (val === null || val === undefined || String(val).trim() === '') ? '-' : val;
  1992. const formatDate = dateStr => {
  1993. if (!dateStr) return '-';
  1994. const parts = dateStr.split('-');
  1995. if (parts.length === 3) {
  1996. return `${parts[2]}/${parts[1]}/${parts[0]}`;
  1997. }
  1998. return dateStr;
  1999. };
  2000. const formatDateTime = dateTimeStr => {
  2001. if (!dateTimeStr) return '-';
  2002. try {
  2003. const d = new Date(dateTimeStr);
  2004. if (!isNaN(d.getTime())) {
  2005. const dd = String(d.getDate()).padStart(2, '0');
  2006. const MM = String(d.getMonth() + 1).padStart(2, '0');
  2007. const yyyy = d.getFullYear();
  2008. const HH = String(d.getHours()).padStart(2, '0');
  2009. const mm = String(d.getMinutes()).padStart(2, '0');
  2010. const ss = String(d.getSeconds()).padStart(2, '0');
  2011. return `${dd}/${MM}/${yyyy} ${HH}:${mm}:${ss}`;
  2012. }
  2013. } catch (e) {
  2014. console.error(e);
  2015. }
  2016. return dateTimeStr;
  2017. };
  2018. // --- LATEST UPDATES CORNER BADGE LOGIC ---
  2019. function updateLatestUpdateBadge() {
  2020. const badge = document.getElementById('latest-update-badge');
  2021. if (!badge) return;
  2022. if (!allRecords || allRecords.length === 0) {
  2023. badge.style.setProperty('display', 'none', 'important');
  2024. return;
  2025. }
  2026. // Find the maximum updated_date
  2027. let maxTime = 0;
  2028. let maxDateStr = null;
  2029. allRecords.forEach(r => {
  2030. if (r.updated_date) {
  2031. const time = new Date(r.updated_date).getTime();
  2032. if (!isNaN(time) && time > maxTime) {
  2033. maxTime = time;
  2034. maxDateStr = r.updated_date;
  2035. }
  2036. }
  2037. });
  2038. if (maxTime === 0 || !maxDateStr) {
  2039. badge.style.setProperty('display', 'none', 'important');
  2040. return;
  2041. }
  2042. const latestDateOnly = getLocalDateString(maxDateStr);
  2043. // Count how many files share this exact latest update date stamp
  2044. let count = 0;
  2045. allRecords.forEach(r => {
  2046. if (r.updated_date && getLocalDateString(r.updated_date) === latestDateOnly) {
  2047. count++;
  2048. }
  2049. });
  2050. // Formatted date string helper
  2051. const formatDateTimeHelper = dateTimeStr => {
  2052. if (!dateTimeStr) return '-';
  2053. try {
  2054. const d = new Date(dateTimeStr);
  2055. if (isNaN(d.getTime())) return dateTimeStr;
  2056. const pad = n => String(n).padStart(2, '0');
  2057. return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
  2058. } catch (e) {
  2059. return dateTimeStr;
  2060. }
  2061. };
  2062. const countEl = document.getElementById('latest-update-count');
  2063. const timeEl = document.getElementById('latest-update-time');
  2064. if (countEl) countEl.textContent = count;
  2065. if (timeEl) timeEl.textContent = formatDateTimeHelper(maxDateStr);
  2066. badge.style.setProperty('display', 'flex', 'important');
  2067. badge.setAttribute('data-latest-date', latestDateOnly);
  2068. }
  2069. function openLatestUpdatesModal() {
  2070. const badge = document.getElementById('latest-update-badge');
  2071. if (!badge) return;
  2072. const latestDate = badge.getAttribute('data-latest-date');
  2073. if (!latestDate) return;
  2074. let dateDisplay = '-';
  2075. try {
  2076. const d = new Date(latestDate);
  2077. if (!isNaN(d.getTime())) {
  2078. const pad = n => String(n).padStart(2, '0');
  2079. dateDisplay = `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()}`;
  2080. }
  2081. } catch (e) { }
  2082. // Filter allRecords to find files updated on the latest date
  2083. const files = allRecords.filter(r => {
  2084. return r.updated_date && getLocalDateString(r.updated_date) === latestDate;
  2085. });
  2086. if (files.length === 0) return;
  2087. // Set modal title
  2088. const titleEl = document.getElementById('countDetailsModalTypeTitle');
  2089. if (titleEl) {
  2090. titleEl.textContent = `Latest Updates (${dateDisplay})`;
  2091. }
  2092. // Populate the modal table body
  2093. const tbody = document.getElementById('modal-tracking-tbody');
  2094. if (!tbody) return;
  2095. tbody.innerHTML = '';
  2096. files.forEach((row, index) => {
  2097. const tr = document.createElement('tr');
  2098. const dispStatus = String(row.resolved_status || getStatusName(row.status_id) || '').trim();
  2099. const statusStyle = getStatusStyle(dispStatus);
  2100. tr.innerHTML = `
  2101. <td class="text-center fw-bold">${index + 1}</td>
  2102. <td class="fw-semibold text-dark">${escapeHtml(text(row.tapal_no))}</td>
  2103. <td>${formatDate(row.tapal_received_date)}</td>
  2104. <td class="fw-semibold text-secondary">${escapeHtml(text(row.request_from))}</td>
  2105. <td>${escapeHtml(text(row.subject))}</td>
  2106. <td><span class="type-badge type-emd">${escapeHtml(text(row.resolved_category || row.cat_type_id))}</span></td>
  2107. <td><span class="status-badge" style="background-color: ${statusStyle.bg}; color: ${statusStyle.text};">${escapeHtml(dispStatus)}</span></td>
  2108. <td><small class="text-muted">${escapeHtml(text(row.remarks))}</small></td>
  2109. <td>${formatDateTime(row.updated_date)}</td>
  2110. <td class="fw-semibold text-secondary">${escapeHtml(text(row.updated_by_name || '-'))}</td>
  2111. `;
  2112. // Actions cell
  2113. const tdActions = document.createElement('td');
  2114. tdActions.className = 'text-center';
  2115. if (row.tapal_file_path) {
  2116. const btnViewPdf = document.createElement('button');
  2117. btnViewPdf.className = 'btn-grid-action btn-grid-pdf me-1';
  2118. btnViewPdf.innerHTML = '<i class="fa-solid fa-file-pdf"></i>';
  2119. btnViewPdf.title = 'View Tapal File';
  2120. btnViewPdf.onclick = () => {
  2121. let pdfUrl = row.tapal_file_path;
  2122. window.open(contextPath + (pdfUrl.startsWith('/') ? pdfUrl.substring(1) : pdfUrl), '_blank');
  2123. };
  2124. tdActions.appendChild(btnViewPdf);
  2125. }
  2126. if (isAdmin) {
  2127. const btnEdit = document.createElement('button');
  2128. btnEdit.className = 'btn-grid-action btn-grid-edit me-1';
  2129. btnEdit.innerHTML = '<i class="fa-solid fa-pen-to-square"></i>';
  2130. btnEdit.title = 'Edit Record';
  2131. btnEdit.onclick = () => {
  2132. if (countDetailsModalObj) countDetailsModalObj.hide();
  2133. openEditModal(row);
  2134. };
  2135. tdActions.appendChild(btnEdit);
  2136. const btnDelete = document.createElement('button');
  2137. btnDelete.className = 'btn-grid-action btn-grid-delete me-1';
  2138. btnDelete.innerHTML = '<i class="fa-solid fa-trash-can"></i>';
  2139. btnDelete.title = 'Delete Record';
  2140. btnDelete.onclick = () => {
  2141. if (countDetailsModalObj) countDetailsModalObj.hide();
  2142. confirmDeleteRecord(row.id);
  2143. };
  2144. tdActions.appendChild(btnDelete);
  2145. }
  2146. // History button
  2147. const btnHistory = document.createElement('button');
  2148. btnHistory.className = 'btn-grid-action btn-outline-secondary me-1';
  2149. btnHistory.style.border = '1px solid #64748b';
  2150. btnHistory.style.color = '#64748b';
  2151. btnHistory.style.backgroundColor = 'transparent';
  2152. btnHistory.innerHTML = '<i class="fa-solid fa-history"></i>';
  2153. btnHistory.title = 'View History Log';
  2154. btnHistory.onclick = () => {
  2155. if (countDetailsModalObj) countDetailsModalObj.hide();
  2156. showHistoryLog(row.tapal_no, row.request_from, row.subject);
  2157. };
  2158. tdActions.appendChild(btnHistory);
  2159. tr.appendChild(tdActions);
  2160. tbody.appendChild(tr);
  2161. });
  2162. // Show Bootstrap Modal
  2163. if (!countDetailsModalObj) {
  2164. countDetailsModalObj = new bootstrap.Modal(document.getElementById('countDetailsModal'));
  2165. }
  2166. countDetailsModalObj.show();
  2167. }
  2168. function getCsrfHeaders() {
  2169. const tokenMeta = document.querySelector('meta[name="_csrf"]');
  2170. const headerMeta = document.querySelector('meta[name="_csrf_header"]');
  2171. if (tokenMeta && headerMeta) {
  2172. return { [headerMeta.getAttribute('content')]: tokenMeta.getAttribute('content') };
  2173. }
  2174. return {};
  2175. }
  2176. function escapeHtml(string) {
  2177. const matchHtmlRegExp = /["'&<>]/;
  2178. const str = '' + string;
  2179. const match = matchHtmlRegExp.exec(str);
  2180. if (!match) {
  2181. return str;
  2182. }
  2183. let escape;
  2184. let html = '';
  2185. let index = 0;
  2186. let lastIndex = 0;
  2187. for (index = match.index; index < str.length; index++) {
  2188. switch (str.charCodeAt(index)) {
  2189. case 34: // "
  2190. escape = '&quot;';
  2191. break;
  2192. case 38: // &
  2193. escape = '&amp;';
  2194. break;
  2195. case 39: // '
  2196. escape = '&#39;';
  2197. break;
  2198. case 60: // <
  2199. escape = '&lt;';
  2200. break;
  2201. case 62: // >
  2202. escape = '&gt;';
  2203. break;
  2204. default:
  2205. continue;
  2206. }
  2207. if (lastIndex !== index) {
  2208. html += str.substring(lastIndex, index);
  2209. }
  2210. lastIndex = index + 1;
  2211. html += escape;
  2212. }
  2213. return lastIndex !== index
  2214. ? html + str.substring(lastIndex, index)
  2215. : html;
  2216. }
  2217. </script>
  2218. </body>
  2219. </html>