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.

2150 lines
83 KiB

​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  1. <!DOCTYPE html>
  2. <html xmlns:th="http://www.thymeleaf.org" lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>GCC Payment Tracker - Dashboard</title>
  7. <!-- CSRF Tokens -->
  8. <meta th:if="${_csrf != null}" name="_csrf" th:content="${_csrf.token}" />
  9. <meta th:if="${_csrf != null}" name="_csrf_header" th:content="${_csrf.headerName}" />
  10. <!-- Google Fonts -->
  11. <link
  12. href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Poppins:wght@300;400;500;600;700&display=swap"
  13. rel="stylesheet">
  14. <!-- Bootstrap & FontAwesome -->
  15. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
  16. <link rel="stylesheet" th:href="@{/assets/font-awesome-6.5.2/css/all.min.css}" type="text/css">
  17. <!-- Flatpickr Datepicker -->
  18. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
  19. <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
  20. <!-- Custom Style Sheet mapping to theme -->
  21. <style>
  22. body {
  23. background-color: #f4f7f6;
  24. font-family: 'Poppins', sans-serif;
  25. color: #333;
  26. }
  27. .header {
  28. background: linear-gradient(135deg, #09303d, #0f5c78);
  29. color: #fff;
  30. padding: 15px 30px;
  31. display: flex;
  32. justify-content: space-between;
  33. align-items: center;
  34. box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
  35. }
  36. .logo-left {
  37. display: flex;
  38. align-items: center;
  39. gap: 15px;
  40. }
  41. .logo-left img {
  42. width: 60px;
  43. height: 60px;
  44. }
  45. .logo-text h2 {
  46. font-family: 'Outfit', sans-serif;
  47. font-size: 1.1rem;
  48. font-weight: 700;
  49. margin: 0;
  50. line-height: 1.2;
  51. letter-spacing: 0.5px;
  52. }
  53. .logo-text p {
  54. font-size: 0.7rem;
  55. margin: 0;
  56. color: #aed6f1;
  57. }
  58. .header-title h2 {
  59. font-family: 'Outfit', sans-serif;
  60. font-size: 1.5rem;
  61. font-weight: 700;
  62. margin: 0;
  63. }
  64. .user-menu {
  65. display: flex;
  66. align-items: center;
  67. gap: 15px;
  68. }
  69. .user-badge {
  70. background: rgba(255, 255, 255, 0.15);
  71. padding: 8px 16px;
  72. border-radius: 20px;
  73. font-size: 0.85rem;
  74. font-weight: 600;
  75. display: flex;
  76. align-items: center;
  77. gap: 8px;
  78. }
  79. .btn-logout {
  80. background: #e74c3c;
  81. color: #fff;
  82. border: none;
  83. padding: 8px 16px;
  84. border-radius: 20px;
  85. font-size: 0.85rem;
  86. font-weight: 600;
  87. cursor: pointer;
  88. transition: all 0.3s;
  89. text-decoration: none;
  90. display: inline-flex;
  91. align-items: center;
  92. gap: 6px;
  93. }
  94. .btn-logout:hover {
  95. background: #c0392b;
  96. color: #fff;
  97. transform: translateY(-1.5px);
  98. }
  99. .main-container {
  100. max-width: 95%;
  101. margin: 35px auto;
  102. padding: 0 15px;
  103. }
  104. /* 4 Category Cards Dashboard */
  105. .category-grid {
  106. display: grid;
  107. grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  108. gap: 20px;
  109. margin-bottom: 35px;
  110. }
  111. .category-card {
  112. background: #ffffff;
  113. border: 1px solid #eef2f6;
  114. border-radius: 16px;
  115. padding: 24px;
  116. box-shadow: 0 5px 20px rgba(0, 0, 0, 0.02);
  117. transition: all 0.3s ease;
  118. position: relative;
  119. overflow: hidden;
  120. cursor: pointer;
  121. display: flex;
  122. align-items: center;
  123. justify-content: space-between;
  124. }
  125. .category-card::before {
  126. content: '';
  127. position: absolute;
  128. top: 0;
  129. left: 0;
  130. width: 5px;
  131. height: 100%;
  132. }
  133. /* SD Card Accent */
  134. .category-card.card-sd::before {
  135. background-color: #0f5c78;
  136. }
  137. .category-card.card-sd.active {
  138. border-color: #0f5c78;
  139. box-shadow: 0 10px 25px rgba(15, 92, 120, 0.15);
  140. background-color: #f0f7f9;
  141. }
  142. /* Invoice Card Accent */
  143. .category-card.card-invoice::before {
  144. background-color: #27ae60;
  145. }
  146. .category-card.card-invoice.active {
  147. border-color: #27ae60;
  148. box-shadow: 0 10px 25px rgba(39, 174, 96, 0.15);
  149. background-color: #f1fbf4;
  150. }
  151. /* EMD Card Accent */
  152. .category-card.card-emd::before {
  153. background-color: #8e44ad;
  154. }
  155. .category-card.card-emd.active {
  156. border-color: #8e44ad;
  157. box-shadow: 0 10px 25px rgba(142, 68, 173, 0.15);
  158. background-color: #f9f2fc;
  159. }
  160. /* BG Card Accent */
  161. .category-card.card-bg::before {
  162. background-color: #1e40af;
  163. }
  164. .category-card.card-bg.active {
  165. border-color: #1e40af;
  166. box-shadow: 0 10px 25px rgba(30, 64, 175, 0.15);
  167. background-color: #eff6ff;
  168. }
  169. /* DC Card Accent */
  170. .category-card.card-dc::before {
  171. background-color: #b91c1c;
  172. }
  173. .category-card.card-dc.active {
  174. border-color: #b91c1c;
  175. box-shadow: 0 10px 25px rgba(185, 28, 28, 0.15);
  176. background-color: #fef2f2;
  177. }
  178. /* ASD Card Accent */
  179. .category-card.card-asd::before {
  180. background-color: #d35400;
  181. }
  182. .category-card.card-asd.active {
  183. border-color: #d35400;
  184. box-shadow: 0 10px 25px rgba(211, 84, 0, 0.15);
  185. background-color: #fdf5e6;
  186. }
  187. /* Agreement Card Accent */
  188. .category-card.card-agreement::before {
  189. background-color: #db2777;
  190. }
  191. .category-card.card-agreement.active {
  192. border-color: #db2777;
  193. box-shadow: 0 10px 25px rgba(219, 39, 119, 0.15);
  194. background-color: #fdf2f8;
  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-icon {
  206. font-size: 2.2rem;
  207. opacity: 0.85;
  208. transition: all 0.3s;
  209. }
  210. .card-sd .card-icon {
  211. color: #0f5c78;
  212. }
  213. .card-invoice .card-icon {
  214. color: #27ae60;
  215. }
  216. .card-emd .card-icon {
  217. color: #8e44ad;
  218. }
  219. .card-bg .card-icon {
  220. color: #1e40af;
  221. }
  222. .card-dc .card-icon {
  223. color: #b91c1c;
  224. }
  225. .card-asd .card-icon {
  226. color: #d35400;
  227. }
  228. .card-agreement .card-icon {
  229. color: #db2777;
  230. }
  231. .category-card:hover .card-icon {
  232. transform: scale(1.1);
  233. }
  234. .card-title {
  235. font-family: 'Outfit', sans-serif;
  236. font-size: 0.95rem;
  237. font-weight: 700;
  238. color: #64748b;
  239. text-transform: uppercase;
  240. letter-spacing: 0.5px;
  241. margin: 0;
  242. }
  243. .card-value {
  244. font-family: 'Outfit', sans-serif;
  245. font-size: 1.85rem;
  246. font-weight: 800;
  247. color: #1e293b;
  248. margin: 0;
  249. }
  250. /* Filter Section */
  251. .filter-card {
  252. background: #ffffff;
  253. border-radius: 16px;
  254. border: 1px solid #eef2f6;
  255. box-shadow: 0 8px 30px rgba(0, 0, 0, 0.02);
  256. padding: 24px;
  257. margin-bottom: 30px;
  258. }
  259. .filter-grid {
  260. display: grid;
  261. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  262. gap: 20px;
  263. align-items: end;
  264. }
  265. .filter-group {
  266. display: flex;
  267. flex-direction: column;
  268. gap: 8px;
  269. }
  270. .filter-label {
  271. font-size: 0.8rem;
  272. font-weight: 700;
  273. color: #64748b;
  274. text-transform: uppercase;
  275. letter-spacing: 0.5px;
  276. }
  277. .filter-input {
  278. height: 44px;
  279. border-radius: 10px;
  280. border: 1.5px solid #cbd5e1;
  281. padding: 10px 16px;
  282. font-size: 0.9rem;
  283. font-weight: 500;
  284. color: #334155;
  285. background-color: #f8fafc;
  286. transition: all 0.3s ease;
  287. }
  288. .filter-input:focus {
  289. outline: none;
  290. border-color: #0f5c78;
  291. background-color: #ffffff;
  292. box-shadow: 0 0 0 3px rgba(15, 92, 120, 0.15);
  293. }
  294. .btn-filter-search {
  295. height: 44px;
  296. border-radius: 10px;
  297. border: none;
  298. background: linear-gradient(135deg, #0f5c78, #1781a6);
  299. color: #ffffff;
  300. font-weight: 600;
  301. font-size: 0.9rem;
  302. transition: all 0.3s;
  303. display: flex;
  304. align-items: center;
  305. justify-content: center;
  306. gap: 8px;
  307. cursor: pointer;
  308. box-shadow: 0 4px 10px rgba(15, 92, 120, 0.25);
  309. }
  310. .btn-filter-search:hover {
  311. background: linear-gradient(135deg, #1781a6, #0f5c78);
  312. transform: translateY(-1.5px);
  313. }
  314. .btn-filter-reset {
  315. height: 44px;
  316. border-radius: 10px;
  317. border: 1.5px solid #cbd5e1;
  318. background-color: #ffffff;
  319. color: #475569;
  320. font-weight: 600;
  321. font-size: 0.9rem;
  322. transition: all 0.3s;
  323. display: flex;
  324. align-items: center;
  325. justify-content: center;
  326. gap: 8px;
  327. cursor: pointer;
  328. }
  329. .btn-filter-reset:hover {
  330. background-color: #f1f5f9;
  331. color: #0f5c78;
  332. border-color: #0f5c78;
  333. }
  334. /* Table Card Section */
  335. .table-card {
  336. background: #ffffff;
  337. border-radius: 16px;
  338. border: 1px solid #eef2f6;
  339. box-shadow: 0 8px 30px rgba(0, 0, 0, 0.02);
  340. padding: 24px;
  341. }
  342. .table-card-header {
  343. display: flex;
  344. justify-content: space-between;
  345. align-items: center;
  346. margin-bottom: 20px;
  347. flex-wrap: wrap;
  348. gap: 15px;
  349. }
  350. .table-card-title {
  351. font-family: 'Outfit', sans-serif;
  352. font-size: 1.25rem;
  353. font-weight: 700;
  354. color: #1e293b;
  355. margin: 0;
  356. display: flex;
  357. align-items: center;
  358. gap: 10px;
  359. }
  360. .active-filter-badge {
  361. background-color: #e2e8f0;
  362. color: #475569;
  363. font-size: 0.78rem;
  364. font-weight: 700;
  365. padding: 5px 12px;
  366. border-radius: 20px;
  367. text-transform: uppercase;
  368. }
  369. .btn-add-record {
  370. background: linear-gradient(135deg, #0f5c78, #1781a6);
  371. color: #ffffff;
  372. border: none;
  373. padding: 10px 20px;
  374. border-radius: 10px;
  375. font-size: 0.9rem;
  376. font-weight: 600;
  377. cursor: pointer;
  378. transition: all 0.3s;
  379. display: inline-flex;
  380. align-items: center;
  381. gap: 8px;
  382. box-shadow: 0 4px 10px rgba(15, 92, 120, 0.2);
  383. text-decoration: none;
  384. }
  385. .btn-add-record:hover {
  386. background: linear-gradient(135deg, #1781a6, #0f5c78);
  387. transform: translateY(-2px);
  388. color: #ffffff;
  389. }
  390. /* Custom Data Grid Table */
  391. .grid-table {
  392. font-size: 0.85rem;
  393. }
  394. .grid-table th {
  395. background-color: #f1f5f9 !important;
  396. color: #475569 !important;
  397. font-weight: 700;
  398. text-transform: uppercase;
  399. font-size: 0.75rem;
  400. letter-spacing: 0.5px;
  401. padding: 14px 10px;
  402. border-bottom: 2px solid #cbd5e1 !important;
  403. vertical-align: middle;
  404. }
  405. .grid-table td {
  406. padding: 12px 10px;
  407. vertical-align: middle;
  408. border-bottom: 1px solid #e2e8f0;
  409. }
  410. .grid-table tbody tr:hover {
  411. background-color: #f8fafc;
  412. }
  413. /* Badges for status */
  414. .status-badge {
  415. font-size: 0.72rem;
  416. font-weight: 700;
  417. padding: 4px 10px;
  418. border-radius: 12px;
  419. text-transform: uppercase;
  420. display: inline-block;
  421. }
  422. .status-pending {
  423. background-color: #fef9c3;
  424. color: #854d0e;
  425. }
  426. .status-awaiting-details {
  427. background-color: #ffedd5;
  428. color: #c2410c;
  429. }
  430. .status-pending-with-dept {
  431. background-color: #e0f2fe;
  432. color: #0369a1;
  433. }
  434. .status-completed {
  435. background-color: #d1fae5;
  436. color: #065f46;
  437. }
  438. .status-no-action {
  439. background-color: #f1f5f9;
  440. color: #475569;
  441. }
  442. .status-processed {
  443. background-color: #dbeafe;
  444. color: #1e40af;
  445. }
  446. .status-approved {
  447. background-color: #d1fae5;
  448. color: #065f46;
  449. }
  450. .status-paid {
  451. background-color: #dcfce7;
  452. color: #166534;
  453. }
  454. .status-rejected {
  455. background-color: #fee2e2;
  456. color: #991b1b;
  457. }
  458. .status-default {
  459. background-color: #f1f5f9;
  460. color: #475569;
  461. }
  462. /* Type Badges */
  463. .type-badge {
  464. font-size: 0.72rem;
  465. font-weight: 700;
  466. padding: 4px 10px;
  467. border-radius: 12px;
  468. text-transform: uppercase;
  469. display: inline-block;
  470. }
  471. .type-sd {
  472. background-color: #f0f7f9;
  473. color: #0f5c78;
  474. border: 1px solid rgba(15, 92, 120, 0.25);
  475. }
  476. .type-tax-invoice {
  477. background-color: #f1fbf4;
  478. color: #27ae60;
  479. border: 1px solid rgba(39, 174, 96, 0.25);
  480. }
  481. .type-emd {
  482. background-color: #f9f2fc;
  483. color: #8e44ad;
  484. border: 1px solid rgba(142, 68, 173, 0.25);
  485. }
  486. .type-bank-guarantee {
  487. background-color: #eff6ff;
  488. color: #1e40af;
  489. border: 1px solid rgba(30, 64, 175, 0.25);
  490. }
  491. .type-delivery-chellan {
  492. background-color: #fef2f2;
  493. color: #b91c1c;
  494. border: 1px solid rgba(185, 28, 28, 0.25);
  495. }
  496. .type-asd {
  497. background-color: #fdf5e6;
  498. color: #d35400;
  499. border: 1px solid rgba(211, 84, 0, 0.25);
  500. }
  501. .type-agreement {
  502. background-color: #fdf2f8;
  503. color: #db2777;
  504. border: 1px solid rgba(219, 39, 119, 0.25);
  505. }
  506. .btn-grid-action {
  507. width: 32px;
  508. height: 32px;
  509. border-radius: 8px;
  510. border: none;
  511. display: inline-flex;
  512. align-items: center;
  513. justify-content: center;
  514. font-size: 0.85rem;
  515. cursor: pointer;
  516. transition: all 0.2s;
  517. }
  518. .btn-grid-edit {
  519. background-color: #e0f2fe;
  520. color: #0369a1;
  521. }
  522. .btn-grid-edit:hover {
  523. background-color: #0369a1;
  524. color: #ffffff;
  525. transform: scale(1.08);
  526. }
  527. .btn-grid-delete {
  528. background-color: #fee2e2;
  529. color: #b91c1c;
  530. }
  531. .btn-grid-delete:hover {
  532. background-color: #b91c1c;
  533. color: #ffffff;
  534. transform: scale(1.08);
  535. }
  536. .btn-grid-pdf {
  537. background-color: #fef2f2;
  538. color: #b91c1c;
  539. border: 1px solid rgba(185, 28, 28, 0.15) !important;
  540. }
  541. .btn-grid-pdf:hover {
  542. background-color: #b91c1c;
  543. color: #ffffff;
  544. transform: scale(1.08);
  545. }
  546. /* Loading Overlay styling */
  547. #loading-overlay {
  548. position: fixed;
  549. top: 0;
  550. left: 0;
  551. width: 100vw;
  552. height: 100vh;
  553. background: rgba(255, 255, 255, 0.75);
  554. backdrop-filter: blur(8px);
  555. -webkit-backdrop-filter: blur(8px);
  556. z-index: 99999;
  557. display: none;
  558. flex-direction: column;
  559. align-items: center;
  560. justify-content: center;
  561. opacity: 0;
  562. pointer-events: none;
  563. transition: opacity 0.3s ease;
  564. }
  565. #loading-overlay.active {
  566. opacity: 1;
  567. pointer-events: all;
  568. display: flex;
  569. }
  570. .loader-spinner-container {
  571. background: white;
  572. padding: 2.5rem 3.5rem;
  573. border-radius: 16px;
  574. box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08);
  575. border: 1px solid rgba(15, 92, 120, 0.1);
  576. display: flex;
  577. flex-direction: column;
  578. align-items: center;
  579. gap: 1.25rem;
  580. }
  581. .loader-spinner {
  582. font-size: 3rem;
  583. color: #0f5c78;
  584. }
  585. .loader-text {
  586. font-family: 'Outfit', 'Inter', sans-serif;
  587. font-size: 1.1rem;
  588. font-weight: 600;
  589. color: #0f5c78;
  590. letter-spacing: 0.5px;
  591. }
  592. .empty-state {
  593. text-align: center;
  594. padding: 60px 20px;
  595. color: #64748b;
  596. }
  597. .empty-icon {
  598. font-size: 3.5rem;
  599. color: #cbd5e1;
  600. margin-bottom: 15px;
  601. }
  602. /* Custom Modal Header Styling */
  603. .modal-custom-header {
  604. background-color: #0f5c78;
  605. color: white;
  606. border-top-left-radius: 14px;
  607. border-top-right-radius: 14px;
  608. padding: 18px 24px;
  609. display: flex;
  610. justify-content: space-between;
  611. align-items: center;
  612. }
  613. .modal-custom-title {
  614. font-weight: 700;
  615. font-size: 1.25rem;
  616. display: flex;
  617. align-items: center;
  618. gap: 10px;
  619. margin: 0;
  620. font-family: 'Outfit', sans-serif;
  621. }
  622. .modal-content {
  623. border-radius: 14px;
  624. border: none;
  625. box-shadow: 0 15px 50px rgba(0, 0, 0, 0.25);
  626. }
  627. /* Pagination and Table Controls Styling */
  628. .pagination .page-item .page-link {
  629. color: #0f5c78;
  630. border-color: #cbd5e1;
  631. font-weight: 500;
  632. padding: 6px 12px;
  633. transition: all 0.2s;
  634. }
  635. .pagination .page-item.active .page-link {
  636. background-color: #0f5c78;
  637. border-color: #0f5c78;
  638. color: #ffffff;
  639. }
  640. .pagination .page-item .page-link:hover {
  641. background-color: #e2e8f0;
  642. border-color: #0f5c78;
  643. color: #0f5c78;
  644. }
  645. .pagination .page-item.disabled .page-link {
  646. color: #94a3b8;
  647. border-color: #cbd5e1;
  648. background-color: #f1f5f9;
  649. }
  650. .table-controls-row {
  651. display: flex;
  652. justify-content: space-between;
  653. align-items: center;
  654. margin-bottom: 20px;
  655. flex-wrap: wrap;
  656. gap: 15px;
  657. }
  658. .table-controls-left,
  659. .table-controls-right {
  660. display: flex;
  661. align-items: center;
  662. gap: 8px;
  663. }
  664. </style>
  665. </head>
  666. <body>
  667. <!-- Fullscreen Loading Overlay -->
  668. <div id="loading-overlay">
  669. <div class="loader-spinner-container">
  670. <i class="fa-solid fa-spinner fa-spin loader-spinner"></i>
  671. <span class="loader-text" id="loader-overlay-text">Processing...</span>
  672. </div>
  673. </div>
  674. <!-- Header Bar -->
  675. <div class="header">
  676. <div class="logo-left">
  677. <img th:src="@{/assets/header-images/gcc1.png}" alt="Greater Chennai Corporation Logo">
  678. <div class="logo-text">
  679. <h2>GREATER CHENNAI CORPORATION</h2>
  680. <p>பெருநகர சென்னை மாநகராட்சி</p>
  681. </div>
  682. </div>
  683. <div class="header-title">
  684. <h2>Payment Processing Tracker</h2>
  685. </div>
  686. <div class="user-menu">
  687. <span class="user-badge">
  688. <i class="fa-solid fa-user-shield"></i>
  689. <span th:text="${usrName}">Guest</span>
  690. </span>
  691. <a th:href="@{/gccservicepaymenttracker/logout}" class="btn-logout">
  692. <i class="fa-solid fa-right-from-bracket"></i> Logout
  693. </a>
  694. </div>
  695. </div>
  696. <div class="main-container">
  697. <!-- Category Grid (5 Cards) -->
  698. <div class="category-grid">
  699. <!-- Security Deposit -->
  700. <div class="category-card card-sd" onclick="selectCategory('sd', this)">
  701. <div class="card-left">
  702. <h3 class="card-title">Security Deposit</h3>
  703. <p class="card-value" id="count-sd">0</p>
  704. </div>
  705. <i class="fa-solid fa-wallet card-icon"></i>
  706. </div>
  707. <!-- Tax Invoice -->
  708. <div class="category-card card-invoice" onclick="selectCategory('tax invoice', this)">
  709. <div class="card-left">
  710. <h3 class="card-title">Tax Invoice</h3>
  711. <p class="card-value" id="count-invoice">0</p>
  712. </div>
  713. <i class="fa-solid fa-file-invoice-dollar card-icon"></i>
  714. </div>
  715. <!-- EMD -->
  716. <div class="category-card card-emd" onclick="selectCategory('emd', this)">
  717. <div class="card-left">
  718. <h3 class="card-title">EMD</h3>
  719. <p class="card-value" id="count-emd">0</p>
  720. </div>
  721. <i class="fa-solid fa-file-signature card-icon"></i>
  722. </div>
  723. <!-- Bank Guarantee commented out
  724. <div class="category-card card-bg" onclick="selectCategory('bank guarantee', this)">
  725. <div class="card-left">
  726. <h3 class="card-title">Bank Guarantee</h3>
  727. <p class="card-value" id="count-bg">0</p>
  728. </div>
  729. <i class="fa-solid fa-shield-halved card-icon"></i>
  730. </div>
  731. -->
  732. <!-- Delivery Chellan commented out
  733. <div class="category-card card-dc" onclick="selectCategory('delivery chellan', this)">
  734. <div class="card-left">
  735. <h3 class="card-title">Delivery Chellan</h3>
  736. <p class="card-value" id="count-dc">0</p>
  737. </div>
  738. <i class="fa-solid fa-truck-ramp-box card-icon"></i>
  739. </div>
  740. -->
  741. <!-- ASD -->
  742. <div class="category-card card-asd" onclick="selectCategory('asd', this)">
  743. <div class="card-left">
  744. <h3 class="card-title">ASD</h3>
  745. <p class="card-value" id="count-asd">0</p>
  746. </div>
  747. <i class="fa-solid fa-vault card-icon"></i>
  748. </div>
  749. <!-- Agreement -->
  750. <div class="category-card card-agreement" onclick="selectCategory('agreement', this)">
  751. <div class="card-left">
  752. <h3 class="card-title">Agreement</h3>
  753. <p class="card-value" id="count-agreement">0</p>
  754. </div>
  755. <i class="fa-solid fa-file-contract card-icon"></i>
  756. </div>
  757. </div>
  758. <!-- Filter Card -->
  759. <div class="filter-card">
  760. <div class="filter-grid">
  761. <!-- Date range picker
  762. <div class="filter-group">
  763. <label class="filter-label" for="filter-from-date">From Date</label>
  764. <input type="text" id="filter-from-date" class="filter-input" placeholder="dd/mm/yyyy" readonly>
  765. </div>
  766. <div class="filter-group">
  767. <label class="filter-label" for="filter-to-date">To Date</label>
  768. <input type="text" id="filter-to-date" class="filter-input" placeholder="dd/mm/yyyy" readonly>
  769. </div>
  770. -->
  771. <!-- Type Filter -->
  772. <div class="filter-group">
  773. <label class="filter-label" for="filter-type">Type</label>
  774. <select id="filter-type" class="filter-input" onchange="handleTypeFilterChange(this.value)">
  775. <option value="all">- All Types -</option>
  776. <!-- Commented out static options to load from database
  777. <option value="sd">Security Deposit</option>
  778. <option value="tax invoice">Tax Invoice</option>
  779. <option value="emd">EMD</option>
  780. <option value="asd">ASD</option>
  781. <option value="agreement">Agreement</option>
  782. -->
  783. </select>
  784. </div>
  785. <!-- Status Filter -->
  786. <div class="filter-group">
  787. <label class="filter-label" for="filter-status">Status</label>
  788. <select id="filter-status" class="filter-input" onchange="loadList()">
  789. <option value="">- All Statuses -</option>
  790. <option value="Awaiting Details">Awaiting Details</option>
  791. <option value="Pending with Dept">Pending with Dept</option>
  792. <option value="Completed">Completed</option>
  793. <option value="No Action">No Action</option>
  794. <option value="Pending">Pending</option>
  795. </select>
  796. </div>
  797. <!-- Text Search -->
  798. <div class="filter-group">
  799. <label class="filter-label" for="search-text">Search Keyword</label>
  800. <input type="text" id="search-text" class="filter-input" placeholder="Vendor, subject, invoice #..."
  801. oninput="handleSearchInput(this.value)">
  802. </div>
  803. <!-- Action buttons -->
  804. <button class="btn-filter-search" onclick="loadList()">
  805. <i class="fa-solid fa-magnifying-glass"></i> Filter
  806. </button>
  807. <button class="btn-filter-reset" onclick="resetFilters()">
  808. <i class="fa-solid fa-rotate-right"></i> Reset
  809. </button>
  810. </div>
  811. </div>
  812. <!-- Table Card Section -->
  813. <div class="table-card">
  814. <div class="table-card-header">
  815. <h3 class="table-card-title">
  816. <i class="fa-solid fa-table-list text-primary"></i>
  817. <span>Payment Tracking Details</span>
  818. <span class="active-filter-badge" id="active-category-badge">Type: All Types</span>
  819. </h3>
  820. <button th:if="${isAdmin}" class="btn-add-record" onclick="openAddModal()">
  821. <i class="fa-solid fa-circle-plus"></i> Add Payment Tracking Record
  822. </button>
  823. </div>
  824. <!-- Table Controls: Page size & Real-time Search -->
  825. <div class="table-controls-row">
  826. <div class="table-controls-left">
  827. <span class="text-secondary fw-semibold" style="font-size: 0.85rem;">Show</span>
  828. <select id="table-page-size" class="form-select form-select-sm" style="width: 80px;"
  829. onchange="changePageSize(this.value)">
  830. <option value="10">10</option>
  831. <option value="20" selected>20</option>
  832. <option value="30">30</option>
  833. <option value="40">40</option>
  834. <option value="50">50</option>
  835. </select>
  836. <span class="text-secondary fw-semibold" style="font-size: 0.85rem;">entries</span>
  837. </div>
  838. <div class="table-controls-right">
  839. <span class="text-secondary fw-semibold" style="font-size: 0.85rem;">Search:</span>
  840. <input type="text" id="table-search" class="form-control form-control-sm" style="width: 250px;"
  841. placeholder="Search in this table..." oninput="handleTableSearch(this.value)">
  842. </div>
  843. </div>
  844. <!-- Responsive Table Grid -->
  845. <div class="table-responsive">
  846. <table class="table grid-table table-striped table-hover align-middle" id="tracking-table">
  847. <!-- Commented out old thead code
  848. <thead>
  849. <tr>
  850. <th class="text-center" style="width: 50px;">S.No</th>
  851. <th>Received Date</th>
  852. <th>Type</th>
  853. <th>Vendor Name</th>
  854. <th>Subject</th>
  855. <th>Amount</th>
  856. <th>Invoice No</th>
  857. <th>Receipt No</th>
  858. <th>Invoice Date</th>
  859. <th>Physical File No</th>
  860. <th>Remarks</th>
  861. <th>TAT</th>
  862. <th>Status</th>
  863. <th>Followup</th>
  864. <th>My Followup</th>
  865. <th class="text-center" style="width: 100px;">Actions</th>
  866. </tr>
  867. </thead>
  868. -->
  869. <thead>
  870. <tr>
  871. <th class="text-center" style="width: 50px;">Sr. No.</th>
  872. <th>Received Date</th>
  873. <th>Invoice Date</th>
  874. <th>Physical File No.</th>
  875. <th>eFile No./ Receipt No.</th>
  876. <th>Subject</th>
  877. <th>Type (SD/ ASD)</th>
  878. <th>Vendor Name</th>
  879. <th>Amount</th>
  880. <th>Age of Bill (Days)</th>
  881. <th>Status (Pending / Completed)</th>
  882. <th>Remarks</th>
  883. <th>Updated on</th>
  884. <th class="text-center" style="width: 100px;">Actions</th>
  885. </tr>
  886. </thead>
  887. <tbody id="tracking-tbody">
  888. <!-- Populated dynamically via JS -->
  889. </tbody>
  890. </table>
  891. <div id="no-records-found" class="empty-state" style="display: none;">
  892. <i class="fa-solid fa-folder-open empty-icon"></i>
  893. <h4>No Records Found</h4>
  894. <p>No payment tracking records match the active criteria.</p>
  895. </div>
  896. </div>
  897. <!-- Pagination Controls -->
  898. <div class="d-flex justify-content-between align-items-center mt-3 flex-wrap gap-2">
  899. <div class="text-secondary fw-semibold" style="font-size: 0.85rem;" id="pagination-info">
  900. Showing 0 to 0 of 0 entries
  901. </div>
  902. <nav aria-label="Table navigation">
  903. <ul class="pagination pagination-sm mb-0" id="pagination-controls">
  904. <!-- Populated dynamically via JS -->
  905. </ul>
  906. </nav>
  907. </div>
  908. </div>
  909. </div>
  910. <!-- Add/Edit Modal Form -->
  911. <div class="modal fade" id="recordModal" tabindex="-1" aria-labelledby="recordModalLabel" aria-hidden="true">
  912. <div class="modal-dialog modal-lg modal-dialog-centered">
  913. <div class="modal-content">
  914. <div class="modal-custom-header">
  915. <h5 class="modal-custom-title" id="recordModalLabel">
  916. <i class="fa-solid fa-file-invoice-dollar"></i> Add/Edit Record
  917. </h5>
  918. <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
  919. aria-label="Close"></button>
  920. </div>
  921. <div class="modal-body">
  922. <form id="recordForm" onsubmit="saveRecord(event)">
  923. <input type="hidden" id="form-id" name="id">
  924. <div class="row g-3">
  925. <!-- Received Date -->
  926. <div class="col-md-6">
  927. <label for="form-received-date" class="form-label fw-semibold text-secondary">Received
  928. Date <span class="text-danger">*</span></label>
  929. <input type="date" class="form-control" id="form-received-date" required
  930. onclick="this.showPicker()">
  931. </div>
  932. <!-- Type -->
  933. <div class="col-md-6">
  934. <label for="form-type" class="form-label fw-semibold text-secondary">Type <span
  935. class="text-danger">*</span></label>
  936. <select class="form-select" id="form-type" required>
  937. <!-- Commented out static options to load from database
  938. <option value="sd">Security Deposit</option>
  939. <option value="tax invoice">Tax Invoice</option>
  940. <option value="emd">EMD</option>
  941. <option value="asd">ASD</option>
  942. <option value="agreement">Agreement</option>
  943. -->
  944. </select>
  945. </div>
  946. <!-- Vendor Name (Dynamic dropdown + Write-in) -->
  947. <div class="col-md-6">
  948. <label for="form-vendor-select" class="form-label fw-semibold text-secondary">Vendor
  949. Name <span class="text-danger">*</span></label>
  950. <select class="form-select mb-2" id="form-vendor-select"
  951. onchange="toggleCustomVendor()">
  952. <option value="">-- Select Vendor --</option>
  953. <!-- Commented out model-populated options
  954. <option th:each="vendor : ${vendorDetails}" th:value="${vendor.vendor_name}"
  955. th:text="${vendor.vendor_name}"></option>
  956. -->
  957. <option value="__custom__">-- Write-in custom vendor --</option>
  958. </select>
  959. <input type="text" class="form-control" id="form-vendor-custom"
  960. placeholder="Enter Custom Vendor Name" style="display: none;">
  961. </div>
  962. <!-- Subject -->
  963. <div class="col-md-6">
  964. <label for="form-subject" class="form-label fw-semibold text-secondary">Subject <span
  965. class="text-danger">*</span></label>
  966. <input type="text" class="form-control" id="form-subject"
  967. placeholder="Enter subject/matter" required>
  968. </div>
  969. <!-- Amount -->
  970. <div class="col-md-6">
  971. <label for="form-amount" class="form-label fw-semibold text-secondary">Amount <span
  972. class="text-danger">*</span></label>
  973. <input type="text" class="form-control" id="form-amount"
  974. placeholder="Enter amount (e.g. 50,000)" required>
  975. </div>
  976. <!-- Invoice No -->
  977. <div class="col-md-6">
  978. <label for="form-invoice-no" class="form-label fw-semibold text-secondary">Invoice
  979. No</label>
  980. <input type="text" class="form-control" id="form-invoice-no"
  981. placeholder="Enter invoice number">
  982. </div>
  983. <!-- Receipt No -->
  984. <div class="col-md-6">
  985. <label for="form-receiptno" class="form-label fw-semibold text-secondary">Receipt
  986. No</label>
  987. <input type="text" class="form-control" id="form-receiptno"
  988. placeholder="Enter Receipt No">
  989. </div>
  990. <!-- Invoice Date -->
  991. <div class="col-md-6">
  992. <label for="form-invoice-date" class="form-label fw-semibold text-secondary">Invoice
  993. Date</label>
  994. <input type="date" class="form-control" id="form-invoice-date"
  995. onclick="this.showPicker()">
  996. </div>
  997. <!-- Physical File No -->
  998. <div class="col-md-6">
  999. <label for="form-physical-file-no"
  1000. class="form-label fw-semibold text-secondary">Physical File No</label>
  1001. <input type="text" class="form-control" id="form-physical-file-no"
  1002. placeholder="Enter physical file reference number">
  1003. </div>
  1004. <!-- TAT -->
  1005. <!-- <div class="col-md-6">
  1006. <label for="form-tat" class="form-label fw-semibold text-secondary">Turnaround Time
  1007. (TAT)</label>
  1008. <input type="text" class="form-control" id="form-tat" placeholder="e.g. 5 days">
  1009. </div> -->
  1010. <!-- Status -->
  1011. <div class="col-md-6">
  1012. <label for="form-status" class="form-label fw-semibold text-secondary">Status <span
  1013. class="text-danger">*</span></label>
  1014. <select class="form-select" id="form-status" required>
  1015. <option value="Awaiting Details">Awaiting Details</option>
  1016. <option value="Pending with Dept">Pending with Dept</option>
  1017. <option value="Completed">Completed</option>
  1018. <option value="No Action">No Action</option>
  1019. <option value="Pending">Pending</option>
  1020. </select>
  1021. </div>
  1022. <!-- Followup commented out
  1023. <div class="col-md-6">
  1024. <label for="form-followup"
  1025. class="form-label fw-semibold text-secondary">Follow-up</label>
  1026. <input type="text" class="form-control" id="form-followup" placeholder="Follow-up note">
  1027. </div>
  1028. -->
  1029. <!-- My Followup commented out
  1030. <div class="col-md-6">
  1031. <label for="form-myfollowup" class="form-label fw-semibold text-secondary">My
  1032. Follow-up</label>
  1033. <input type="text" class="form-control" id="form-myfollowup"
  1034. placeholder="Internal follow-up note">
  1035. </div>
  1036. -->
  1037. <!-- Order By commented out
  1038. <div class="col-md-6">
  1039. <label for="form-orderby" class="form-label fw-semibold text-secondary">Order By</label>
  1040. <input type="text" class="form-control" id="form-orderby"
  1041. placeholder="Reference order description">
  1042. </div>
  1043. -->
  1044. <!-- PDF Attachment -->
  1045. <div class="col-md-12">
  1046. <label for="form-pdf" class="form-label fw-semibold text-secondary">PDF Attachment
  1047. (Optional)</label>
  1048. <input type="file" class="form-control" id="form-pdf" accept="application/pdf">
  1049. <div id="form-existing-pdf-container" class="mt-2" style="display: none;">
  1050. <span class="text-secondary fw-semibold">Current Document:</span>
  1051. <a id="form-existing-pdf-link" href="#" target="_blank"
  1052. class="ms-1 text-primary fw-bold text-decoration-none">
  1053. <i class="fa-solid fa-file-pdf"></i> View PDF
  1054. </a>
  1055. </div>
  1056. </div>
  1057. <!-- Remarks -->
  1058. <div class="col-12">
  1059. <label for="form-remarks" class="form-label fw-semibold text-secondary">Remarks</label>
  1060. <textarea class="form-control" id="form-remarks" rows="2"
  1061. placeholder="Additional remarks..."></textarea>
  1062. </div>
  1063. </div>
  1064. <div class="d-flex justify-content-end gap-2 mt-4">
  1065. <button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">Cancel</button>
  1066. <button type="submit" class="btn btn-primary px-4"
  1067. style="background-color: #0f5c78; border-color: #0f5c78;">
  1068. <i class="fa-solid fa-save"></i> Save Record
  1069. </button>
  1070. </div>
  1071. </form>
  1072. </div>
  1073. </div>
  1074. </div>
  1075. </div>
  1076. <!-- Bootstrap & FontAwesome bundles -->
  1077. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
  1078. <!-- Custom Page Scripting -->
  1079. <script th:inline="javascript">
  1080. const contextPath = /*[[@{/}]]*/ '';
  1081. const isAdmin = /*[[${isAdmin}]]*/ false;
  1082. let activeCategory = 'all';
  1083. let allRecords = [];
  1084. let recordModalObj = null;
  1085. let currentPage = 1;
  1086. let pageSize = 20;
  1087. // Dynamic lists for types and vendors
  1088. let activeTypes = [];
  1089. let activeVendors = [];
  1090. // Date Pickers
  1091. // let fromDatePicker, toDatePicker;
  1092. document.addEventListener('DOMContentLoaded', async () => {
  1093. recordModalObj = new bootstrap.Modal(document.getElementById('recordModal'));
  1094. // initDatePickers();
  1095. await loadTypesAndVendors();
  1096. loadCounts();
  1097. loadList();
  1098. });
  1099. // Helper to map type_name to a category code
  1100. function getCategoryCode(typeName) {
  1101. const type = String(typeName).toLowerCase().trim();
  1102. if (type === 'asd' || type.includes('asd') || type.includes('additional security')) {
  1103. return 'asd';
  1104. } else if (type.includes('agreement')) {
  1105. return 'agreement';
  1106. } else if (type === 'sd' || (type.includes('security') && !type.includes('additional') && !type.includes('asd'))) {
  1107. return 'sd';
  1108. } else if (type.includes('invoice') || type.includes('bill')) {
  1109. return 'tax invoice';
  1110. } else if (type === 'emd' || type.includes('earnest')) {
  1111. return 'emd';
  1112. } else if (type.includes('guarantee') || type === 'bg') {
  1113. return 'bank guarantee';
  1114. } else if (type.includes('chellan') || type.includes('challan') || type === 'dc') {
  1115. return 'delivery chellan';
  1116. }
  1117. return type;
  1118. }
  1119. // Fetch types and vendors from DB endpoints
  1120. async function loadTypesAndVendors() {
  1121. try {
  1122. const [typesRes, vendorsRes] = await Promise.all([
  1123. fetch(contextPath + 'gccservicepaymenttracker/api/types?_=' + new Date().getTime()),
  1124. fetch(contextPath + 'gccservicepaymenttracker/api/vendors?_=' + new Date().getTime())
  1125. ]);
  1126. activeTypes = await typesRes.json();
  1127. activeVendors = await vendorsRes.json();
  1128. populateDropdowns();
  1129. } catch (err) {
  1130. console.error("Error loading types or vendors:", err);
  1131. }
  1132. }
  1133. // Populate selects with dynamic options
  1134. function populateDropdowns() {
  1135. // Populate filter-type
  1136. const filterTypeSelect = document.getElementById('filter-type');
  1137. if (filterTypeSelect) {
  1138. filterTypeSelect.innerHTML = '<option value="all">- All Types -</option>';
  1139. activeTypes.forEach(t => {
  1140. const categoryCode = getCategoryCode(t.type_name);
  1141. const opt = document.createElement('option');
  1142. opt.value = categoryCode;
  1143. opt.textContent = t.type_name;
  1144. filterTypeSelect.appendChild(opt);
  1145. });
  1146. }
  1147. // Populate form-type
  1148. const formTypeSelect = document.getElementById('form-type');
  1149. if (formTypeSelect) {
  1150. formTypeSelect.innerHTML = '<option value="">-- Select Type --</option>';
  1151. activeTypes.forEach(t => {
  1152. const opt = document.createElement('option');
  1153. opt.value = t.id;
  1154. opt.textContent = t.type_name;
  1155. formTypeSelect.appendChild(opt);
  1156. });
  1157. }
  1158. // Populate form-vendor-select
  1159. const formVendorSelect = document.getElementById('form-vendor-select');
  1160. if (formVendorSelect) {
  1161. formVendorSelect.innerHTML = '<option value="">-- Select Vendor --</option>';
  1162. activeVendors.forEach(v => {
  1163. const opt = document.createElement('option');
  1164. opt.value = v.id;
  1165. opt.textContent = v.vendor_name;
  1166. formVendorSelect.appendChild(opt);
  1167. });
  1168. const customOpt = document.createElement('option');
  1169. customOpt.value = '__custom__';
  1170. customOpt.textContent = '-- Write-in custom vendor --';
  1171. formVendorSelect.appendChild(customOpt);
  1172. }
  1173. }
  1174. // Show/hide loader overlay
  1175. function showLoader(show, text = 'Processing...') {
  1176. const overlay = document.getElementById('loading-overlay');
  1177. const overlayText = document.getElementById('loader-overlay-text');
  1178. overlayText.textContent = text;
  1179. if (show) {
  1180. overlay.classList.add('active');
  1181. } else {
  1182. overlay.classList.remove('active');
  1183. }
  1184. }
  1185. // Get CSRF details
  1186. function getCsrfHeaders() {
  1187. const csrfToken = document.querySelector("meta[name='_csrf']")?.getAttribute("content");
  1188. const csrfHeader = document.querySelector("meta[name='_csrf_header']")?.getAttribute("content");
  1189. const headers = {};
  1190. if (csrfToken && csrfHeader) {
  1191. headers[csrfHeader] = csrfToken;
  1192. }
  1193. return headers;
  1194. }
  1195. // Category count loaders
  1196. function loadCounts() {
  1197. fetch(contextPath + 'gccservicepaymenttracker/api/counts?_=' + new Date().getTime())
  1198. .then(response => response.json())
  1199. .then(data => {
  1200. document.getElementById('count-sd').textContent = data.sd || 0;
  1201. document.getElementById('count-invoice').textContent = data['tax invoice'] || 0;
  1202. document.getElementById('count-emd').textContent = data.emd || 0;
  1203. const bgEl = document.getElementById('count-bg');
  1204. if (bgEl) bgEl.textContent = data['bank guarantee'] || 0;
  1205. const dcEl = document.getElementById('count-dc');
  1206. if (dcEl) dcEl.textContent = data['delivery chellan'] || 0;
  1207. document.getElementById('count-asd').textContent = data.asd || 0;
  1208. document.getElementById('count-agreement').textContent = data.agreement || 0;
  1209. })
  1210. .catch(err => console.error("Error loading category counts:", err));
  1211. }
  1212. // Handles select change of card
  1213. function selectCategory(category, cardElement) {
  1214. const isActive = cardElement.classList.contains('active');
  1215. // Remove active class from all
  1216. document.querySelectorAll('.category-card').forEach(el => el.classList.remove('active'));
  1217. const filterTypeSelect = document.getElementById('filter-type');
  1218. if (isActive) {
  1219. activeCategory = 'all';
  1220. document.getElementById('active-category-badge').textContent = 'Type: All Types';
  1221. if (filterTypeSelect) {
  1222. filterTypeSelect.value = 'all';
  1223. }
  1224. } else {
  1225. cardElement.classList.add('active');
  1226. activeCategory = category;
  1227. if (filterTypeSelect) {
  1228. filterTypeSelect.value = category;
  1229. }
  1230. // Update badge text
  1231. const labels = {
  1232. 'sd': 'Security Deposit',
  1233. 'tax invoice': 'Tax Invoice',
  1234. 'emd': 'EMD',
  1235. 'asd': 'ASD',
  1236. 'agreement': 'Agreement'
  1237. };
  1238. document.getElementById('active-category-badge').textContent = 'Type: ' + (labels[category] || category);
  1239. }
  1240. // Refresh list
  1241. loadList();
  1242. }
  1243. // Loading and Rendering list data
  1244. function loadList() {
  1245. showLoader(true, 'Loading payment tracking details...');
  1246. const statusVal = document.getElementById('filter-status').value;
  1247. const searchVal = document.getElementById('search-text').value;
  1248. // Sync search-text to table-search when hitting main filter search
  1249. const tblSearch = document.getElementById('table-search');
  1250. if (tblSearch) {
  1251. tblSearch.value = searchVal;
  1252. }
  1253. // Build query params
  1254. const params = new URLSearchParams();
  1255. if (activeCategory && activeCategory !== 'all') {
  1256. params.append('category', activeCategory);
  1257. }
  1258. if (statusVal) params.append('status', statusVal);
  1259. // Add cache buster
  1260. params.append('_', new Date().getTime());
  1261. fetch(contextPath + 'gccservicepaymenttracker/api/list?' + params.toString())
  1262. .then(response => response.json())
  1263. .then(data => {
  1264. allRecords = data;
  1265. currentPage = 1;
  1266. renderTable();
  1267. })
  1268. .catch(err => {
  1269. console.error("Error loading details:", err);
  1270. showLoader(false);
  1271. });
  1272. }
  1273. function renderTable() {
  1274. const tbody = document.getElementById('tracking-tbody');
  1275. const noRecords = document.getElementById('no-records-found');
  1276. tbody.innerHTML = '';
  1277. const searchVal = document.getElementById('table-search')?.value || '';
  1278. let query = searchVal.toLowerCase().trim();
  1279. let filtered = allRecords.filter(row => {
  1280. if (!query) return true;
  1281. const vName = String(row.resolved_vendor || row.vendor_name || '').toLowerCase();
  1282. const tName = String(row.resolved_type || row.type || '').toLowerCase();
  1283. return (
  1284. vName.includes(query) ||
  1285. tName.includes(query) ||
  1286. (row.subject && String(row.subject).toLowerCase().includes(query)) ||
  1287. (row.amount && String(row.amount).toLowerCase().includes(query)) ||
  1288. (row.invoice_no && String(row.invoice_no).toLowerCase().includes(query)) ||
  1289. (row.receiptno && String(row.receiptno).toLowerCase().includes(query)) ||
  1290. (row.physical_file_no && String(row.physical_file_no).toLowerCase().includes(query)) ||
  1291. (row.remarks && String(row.remarks).toLowerCase().includes(query)) ||
  1292. (row.tat && String(row.tat).toLowerCase().includes(query)) ||
  1293. (row.status && String(row.status).toLowerCase().includes(query))
  1294. );
  1295. });
  1296. showLoader(false);
  1297. const totalRecords = filtered.length;
  1298. if (totalRecords === 0) {
  1299. noRecords.style.display = 'block';
  1300. document.getElementById('pagination-info').textContent = 'Showing 0 to 0 of 0 entries';
  1301. document.getElementById('pagination-controls').innerHTML = '';
  1302. return;
  1303. }
  1304. noRecords.style.display = 'none';
  1305. // Paginate
  1306. const totalPages = Math.ceil(totalRecords / pageSize);
  1307. if (currentPage > totalPages) {
  1308. currentPage = totalPages;
  1309. }
  1310. if (currentPage < 1) {
  1311. currentPage = 1;
  1312. }
  1313. const startIndex = (currentPage - 1) * pageSize;
  1314. const endIndex = Math.min(startIndex + pageSize, totalRecords);
  1315. const paginatedData = filtered.slice(startIndex, endIndex);
  1316. // Update Pagination Info
  1317. document.getElementById('pagination-info').textContent =
  1318. `Showing ${startIndex + 1} to ${endIndex} of ${totalRecords} entries`;
  1319. paginatedData.forEach((row, pageIndex) => {
  1320. const globalIndex = startIndex + pageIndex;
  1321. const tr = document.createElement('tr');
  1322. // S.No
  1323. const tdIndex = document.createElement('td');
  1324. tdIndex.className = 'text-center fw-semibold text-secondary';
  1325. tdIndex.textContent = globalIndex + 1;
  1326. tr.appendChild(tdIndex);
  1327. // Helper for blank values
  1328. const text = val => (val === null || val === undefined || String(val).trim() === '') ? '-' : val;
  1329. // Format Dates (yyyy-MM-dd to dd/MM/yyyy)
  1330. const formatDate = dateStr => {
  1331. if (!dateStr) return '-';
  1332. const parts = dateStr.split('-');
  1333. if (parts.length === 3) {
  1334. return `${parts[2]}/${parts[1]}/${parts[0]}`;
  1335. }
  1336. return dateStr;
  1337. };
  1338. // Format Date-Times (yyyy-MM-ddTHH:mm:ss... to dd/MM/yyyy HH:mm:ss)
  1339. const formatDateTime = dateTimeStr => {
  1340. if (!dateTimeStr) return '-';
  1341. try {
  1342. const tParts = dateTimeStr.split('T');
  1343. const datePart = tParts[0];
  1344. const timePart = tParts.length > 1 ? tParts[1].split('.')[0] : '';
  1345. const dateParts = datePart.split('-');
  1346. if (dateParts.length === 3) {
  1347. const formattedDate = `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`;
  1348. return timePart ? `${formattedDate} ${timePart}` : formattedDate;
  1349. }
  1350. } catch (e) {
  1351. console.error(e);
  1352. }
  1353. return dateTimeStr;
  1354. };
  1355. // Calculate age of bill (days) dynamically from received date:
  1356. const calculateAgeOfBill = receivedDateStr => {
  1357. if (!receivedDateStr) return '0';
  1358. try {
  1359. const recDate = new Date(receivedDateStr);
  1360. if (isNaN(recDate.getTime())) return '0';
  1361. recDate.setHours(0, 0, 0, 0);
  1362. const today = new Date();
  1363. today.setHours(0, 0, 0, 0);
  1364. const graceDate = new Date(recDate);
  1365. graceDate.setDate(graceDate.getDate() + 7);
  1366. if (today <= graceDate) {
  1367. return '0';
  1368. } else {
  1369. const diffTime = today.getTime() - graceDate.getTime();
  1370. const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
  1371. return String(diffDays);
  1372. }
  1373. } catch (e) {
  1374. console.error("Error calculating age of bill:", e);
  1375. return '0';
  1376. }
  1377. };
  1378. const getTypeBadge = typeVal => {
  1379. const cleanType = String(typeVal || '').trim().toLowerCase();
  1380. let label = cleanType;
  1381. let badgeClass = 'type-sd';
  1382. if (cleanType === 'asd' || cleanType.includes('asd') || cleanType.includes('additional security')) {
  1383. label = 'ASD';
  1384. badgeClass = 'type-asd';
  1385. } else if (cleanType.includes('agreement')) {
  1386. label = 'Agreement';
  1387. badgeClass = 'type-agreement';
  1388. } else if (cleanType === 'sd' || (cleanType.includes('security') && !cleanType.includes('additional') && !cleanType.includes('asd'))) {
  1389. label = 'Security Deposit';
  1390. badgeClass = 'type-sd';
  1391. } else if (cleanType.includes('invoice') || cleanType.includes('bill')) {
  1392. label = 'Tax Invoice';
  1393. badgeClass = 'type-tax-invoice';
  1394. } else if (cleanType === 'emd' || cleanType.includes('earnest')) {
  1395. label = 'EMD';
  1396. badgeClass = 'type-emd';
  1397. } else if (cleanType.includes('guarantee') || cleanType === 'bg') {
  1398. label = 'Bank Guarantee';
  1399. badgeClass = 'type-bank-guarantee';
  1400. } else if (cleanType.includes('chellan') || cleanType.includes('challan') || cleanType === 'dc') {
  1401. label = 'Delivery Chellan';
  1402. badgeClass = 'type-delivery-chellan';
  1403. } else {
  1404. // Fallback: check if cleanType is a numeric ID from activeTypes
  1405. const typeObj = activeTypes.find(t => String(t.id) === cleanType);
  1406. if (typeObj) {
  1407. return getTypeBadge(typeObj.type_name);
  1408. }
  1409. label = typeVal || '-';
  1410. badgeClass = 'status-default';
  1411. }
  1412. return `<span class="type-badge ${badgeClass}">${label}</span>`;
  1413. };
  1414. tr.innerHTML += `
  1415. <td>${formatDate(row.received_date)}</td>
  1416. <td>${formatDate(row.invoice_date)}</td>
  1417. <td>${text(row.physical_file_no)}</td>
  1418. <td>${text(row.receiptno)}</td>
  1419. <td>${text(row.subject)}</td>
  1420. <td>${getTypeBadge(row.resolved_type || row.type)}</td>
  1421. <td class="fw-semibold text-secondary">${text(row.resolved_vendor || row.vendor_name)}</td>
  1422. <td class="fw-semibold text-dark">&#8377; ${text(row.amount)}</td>
  1423. <td>${calculateAgeOfBill(row.received_date)}</td>
  1424. <td><span class="status-badge status-${String(row.status || '').toLowerCase().trim().replace(/\s+/g, '-')}">${text(row.status)}</span></td>
  1425. <td><small class="text-muted">${text(row.remarks)}</small></td>
  1426. <td>${formatDateTime(row.updated_date)}</td>
  1427. `;
  1428. // Actions cell
  1429. const tdActions = document.createElement('td');
  1430. tdActions.className = 'text-center';
  1431. if (row.file_path) {
  1432. const btnViewPdf = document.createElement('button');
  1433. btnViewPdf.className = 'btn-grid-action btn-grid-pdf me-1';
  1434. btnViewPdf.innerHTML = '<i class="fa-solid fa-file-pdf"></i>';
  1435. btnViewPdf.title = 'View PDF Document';
  1436. btnViewPdf.onclick = () => {
  1437. let pdfUrl = row.file_path;
  1438. if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
  1439. pdfUrl = contextPath + pdfUrl.substring(1);
  1440. } else {
  1441. pdfUrl = contextPath + pdfUrl;
  1442. }
  1443. window.open(pdfUrl, '_blank');
  1444. };
  1445. tdActions.appendChild(btnViewPdf);
  1446. }
  1447. if (isAdmin) {
  1448. const btnEdit = document.createElement('button');
  1449. btnEdit.className = 'btn-grid-action btn-grid-edit me-1';
  1450. btnEdit.innerHTML = '<i class="fa-solid fa-pen-to-square"></i>';
  1451. btnEdit.title = 'Edit Record';
  1452. btnEdit.onclick = () => openEditModal(row);
  1453. const btnDelete = document.createElement('button');
  1454. btnDelete.className = 'btn-grid-action btn-grid-delete';
  1455. btnDelete.innerHTML = '<i class="fa-solid fa-trash-can"></i>';
  1456. btnDelete.title = 'Delete Record';
  1457. btnDelete.onclick = () => deleteRecord(row.id);
  1458. tdActions.appendChild(btnEdit);
  1459. tdActions.appendChild(btnDelete);
  1460. }
  1461. tr.appendChild(tdActions);
  1462. tbody.appendChild(tr);
  1463. });
  1464. // Rebuild Pagination Controls
  1465. buildPaginationControls(totalPages);
  1466. }
  1467. function buildPaginationControls(totalPages) {
  1468. const container = document.getElementById('pagination-controls');
  1469. container.innerHTML = '';
  1470. if (totalPages <= 1) {
  1471. return;
  1472. }
  1473. // Previous Button
  1474. const prevLi = document.createElement('li');
  1475. prevLi.className = 'page-item' + (currentPage === 1 ? ' disabled' : '');
  1476. prevLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${currentPage - 1})">Previous</a>`;
  1477. container.appendChild(prevLi);
  1478. // Page numbers
  1479. const maxVisible = 5;
  1480. let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2));
  1481. let endPage = Math.min(totalPages, startPage + maxVisible - 1);
  1482. if (endPage - startPage + 1 < maxVisible) {
  1483. startPage = Math.max(1, endPage - maxVisible + 1);
  1484. }
  1485. if (startPage > 1) {
  1486. const firstLi = document.createElement('li');
  1487. firstLi.className = 'page-item';
  1488. firstLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(1)">1</a>`;
  1489. container.appendChild(firstLi);
  1490. if (startPage > 2) {
  1491. const dotsLi = document.createElement('li');
  1492. dotsLi.className = 'page-item disabled';
  1493. dotsLi.innerHTML = `<span class="page-link">...</span>`;
  1494. container.appendChild(dotsLi);
  1495. }
  1496. }
  1497. for (let i = startPage; i <= endPage; i++) {
  1498. const pageLi = document.createElement('li');
  1499. pageLi.className = 'page-item' + (i === currentPage ? ' active' : '');
  1500. pageLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${i})">${i}</a>`;
  1501. container.appendChild(pageLi);
  1502. }
  1503. if (endPage < totalPages) {
  1504. if (endPage < totalPages - 1) {
  1505. const dotsLi = document.createElement('li');
  1506. dotsLi.className = 'page-item disabled';
  1507. dotsLi.innerHTML = `<span class="page-link">...</span>`;
  1508. container.appendChild(dotsLi);
  1509. }
  1510. const lastLi = document.createElement('li');
  1511. lastLi.className = 'page-item';
  1512. lastLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${totalPages})">${totalPages}</a>`;
  1513. container.appendChild(lastLi);
  1514. }
  1515. // Next Button
  1516. const nextLi = document.createElement('li');
  1517. nextLi.className = 'page-item' + (currentPage === totalPages ? ' disabled' : '');
  1518. nextLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${currentPage + 1})">Next</a>`;
  1519. container.appendChild(nextLi);
  1520. }
  1521. function goToPage(page) {
  1522. currentPage = page;
  1523. renderTable();
  1524. }
  1525. function changePageSize(size) {
  1526. pageSize = parseInt(size, 10);
  1527. currentPage = 1;
  1528. renderTable();
  1529. }
  1530. // Handles search in local table search input
  1531. function handleTableSearch(keyword) {
  1532. const filterSearch = document.getElementById('search-text');
  1533. if (filterSearch) {
  1534. filterSearch.value = keyword;
  1535. }
  1536. currentPage = 1;
  1537. renderTable();
  1538. }
  1539. function handleSearchInput(keyword) {
  1540. const tblSearch = document.getElementById('table-search');
  1541. if (tblSearch) {
  1542. tblSearch.value = keyword;
  1543. }
  1544. currentPage = 1;
  1545. renderTable();
  1546. }
  1547. function handleTypeFilterChange(category) {
  1548. activeCategory = category;
  1549. // Remove active class from all top cards
  1550. document.querySelectorAll('.category-card').forEach(el => el.classList.remove('active'));
  1551. if (category !== 'all') {
  1552. const classMap = {
  1553. 'sd': 'card-sd',
  1554. 'tax invoice': 'card-invoice',
  1555. 'emd': 'card-emd',
  1556. 'asd': 'card-asd',
  1557. 'agreement': 'card-agreement'
  1558. };
  1559. const cardClass = classMap[category];
  1560. if (cardClass) {
  1561. const cardElement = document.querySelector('.' + cardClass);
  1562. if (cardElement) {
  1563. cardElement.classList.add('active');
  1564. }
  1565. }
  1566. const labels = {
  1567. 'sd': 'Security Deposit',
  1568. 'tax invoice': 'Tax Invoice',
  1569. 'emd': 'EMD',
  1570. 'asd': 'ASD',
  1571. 'agreement': 'Agreement'
  1572. };
  1573. document.getElementById('active-category-badge').textContent = 'Type: ' + (labels[category] || category);
  1574. } else {
  1575. document.getElementById('active-category-badge').textContent = 'Type: All Types';
  1576. }
  1577. loadList();
  1578. }
  1579. function resetFilters() {
  1580. document.getElementById('filter-status').value = '';
  1581. document.getElementById('search-text').value = '';
  1582. const filterType = document.getElementById('filter-type');
  1583. if (filterType) {
  1584. filterType.value = 'all';
  1585. }
  1586. activeCategory = 'all';
  1587. document.querySelectorAll('.category-card').forEach(el => el.classList.remove('active'));
  1588. document.getElementById('active-category-badge').textContent = 'Type: All Types';
  1589. const tblSearch = document.getElementById('table-search');
  1590. if (tblSearch) {
  1591. tblSearch.value = '';
  1592. }
  1593. currentPage = 1;
  1594. loadList();
  1595. }
  1596. // Toggle text field for dynamic vendor select dropdown
  1597. function toggleCustomVendor() {
  1598. const select = document.getElementById('form-vendor-select');
  1599. const customInput = document.getElementById('form-vendor-custom');
  1600. if (select.value === '__custom__') {
  1601. customInput.style.display = 'block';
  1602. customInput.required = true;
  1603. } else {
  1604. customInput.style.display = 'none';
  1605. customInput.required = false;
  1606. customInput.value = '';
  1607. }
  1608. }
  1609. // Edit/Add Modals
  1610. function openAddModal() {
  1611. document.getElementById('recordModalLabel').innerHTML = '<i class="fa-solid fa-circle-plus"></i> Add Payment Tracking Record';
  1612. document.getElementById('recordForm').reset();
  1613. document.getElementById('form-id').value = '';
  1614. // Set type to active category by default
  1615. const formTypeSelect = document.getElementById('form-type');
  1616. formTypeSelect.value = '';
  1617. if (activeCategory && activeCategory !== 'all') {
  1618. for (let option of formTypeSelect.options) {
  1619. if (option.value) {
  1620. const typeObj = activeTypes.find(t => String(t.id) === String(option.value));
  1621. if (typeObj && getCategoryCode(typeObj.type_name) === activeCategory) {
  1622. formTypeSelect.value = option.value;
  1623. break;
  1624. }
  1625. }
  1626. }
  1627. }
  1628. document.getElementById('form-status').value = 'Pending';
  1629. document.getElementById('form-vendor-custom').style.display = 'none';
  1630. document.getElementById('form-vendor-custom').required = false;
  1631. // Reset PDF upload elements
  1632. document.getElementById('form-pdf').value = '';
  1633. document.getElementById('form-existing-pdf-container').style.display = 'none';
  1634. recordModalObj.show();
  1635. }
  1636. function openEditModal(row) {
  1637. document.getElementById('recordModalLabel').innerHTML = '<i class="fa-solid fa-file-signature"></i> Edit Payment Tracking Record';
  1638. document.getElementById('recordForm').reset();
  1639. document.getElementById('form-id').value = row.id;
  1640. document.getElementById('form-received-date').value = row.received_date || '';
  1641. // Set type select
  1642. const typeSelect = document.getElementById('form-type');
  1643. const rowType = String(row.type || '').trim().toLowerCase();
  1644. typeSelect.value = '';
  1645. let found = false;
  1646. for (let option of typeSelect.options) {
  1647. if (option.value && String(option.value) === String(row.type)) {
  1648. typeSelect.value = option.value;
  1649. found = true;
  1650. break;
  1651. }
  1652. }
  1653. if (!found && rowType) {
  1654. const legacyCategory = getCategoryCode(rowType);
  1655. for (let option of typeSelect.options) {
  1656. if (option.value) {
  1657. const typeObj = activeTypes.find(t => String(t.id) === String(option.value));
  1658. if (typeObj) {
  1659. const optCategory = getCategoryCode(typeObj.type_name);
  1660. if (optCategory === legacyCategory) {
  1661. typeSelect.value = option.value;
  1662. found = true;
  1663. break;
  1664. }
  1665. }
  1666. }
  1667. }
  1668. }
  1669. document.getElementById('form-subject').value = row.subject || '';
  1670. document.getElementById('form-amount').value = row.amount || '';
  1671. document.getElementById('form-invoice-no').value = row.invoice_no || '';
  1672. document.getElementById('form-receiptno').value = row.receiptno || '';
  1673. document.getElementById('form-invoice-date').value = row.invoice_date || '';
  1674. document.getElementById('form-physical-file-no').value = row.physical_file_no || '';
  1675. const tatEl = document.getElementById('form-tat');
  1676. if (tatEl) tatEl.value = row.tat || '';
  1677. const statusSelect = document.getElementById('form-status');
  1678. const rowStatus = String(row.status || '').trim();
  1679. statusSelect.value = 'Pending';
  1680. for (let option of statusSelect.options) {
  1681. if (option.value.toLowerCase() === rowStatus.toLowerCase()) {
  1682. statusSelect.value = option.value;
  1683. break;
  1684. }
  1685. }
  1686. const followupEl = document.getElementById('form-followup');
  1687. if (followupEl) followupEl.value = row.followup || '';
  1688. const myfollowupEl = document.getElementById('form-myfollowup');
  1689. if (myfollowupEl) myfollowupEl.value = row.myfollowup || '';
  1690. const orderbyEl = document.getElementById('form-orderby');
  1691. if (orderbyEl) orderbyEl.value = row.order_by || '';
  1692. document.getElementById('form-remarks').value = row.remarks || '';
  1693. // Set vendor select (using ID or legacy fallback/custom)
  1694. const select = document.getElementById('form-vendor-select');
  1695. const vendorVal = String(row.vendor_name || '').trim();
  1696. select.value = '';
  1697. document.getElementById('form-vendor-custom').style.display = 'none';
  1698. document.getElementById('form-vendor-custom').required = false;
  1699. document.getElementById('form-vendor-custom').value = '';
  1700. if (vendorVal) {
  1701. let optionFound = false;
  1702. for (let option of select.options) {
  1703. if (option.value && option.value !== '__custom__' && String(option.value) === vendorVal) {
  1704. select.value = option.value;
  1705. optionFound = true;
  1706. break;
  1707. }
  1708. }
  1709. if (!optionFound) {
  1710. const matchedVendor = activeVendors.find(v => String(v.vendor_name).toLowerCase().trim() === vendorVal.toLowerCase());
  1711. if (matchedVendor) {
  1712. select.value = String(matchedVendor.id);
  1713. optionFound = true;
  1714. }
  1715. }
  1716. if (!optionFound) {
  1717. select.value = '__custom__';
  1718. const customInput = document.getElementById('form-vendor-custom');
  1719. customInput.style.display = 'block';
  1720. customInput.required = true;
  1721. customInput.value = row.vendor_name;
  1722. }
  1723. }
  1724. // Populate/Reset PDF elements
  1725. document.getElementById('form-pdf').value = '';
  1726. const existingPdfContainer = document.getElementById('form-existing-pdf-container');
  1727. const existingPdfLink = document.getElementById('form-existing-pdf-link');
  1728. if (row.file_path) {
  1729. existingPdfContainer.style.display = 'block';
  1730. let pdfUrl = row.file_path;
  1731. if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
  1732. pdfUrl = contextPath + pdfUrl.substring(1);
  1733. } else {
  1734. pdfUrl = contextPath + pdfUrl;
  1735. }
  1736. existingPdfLink.href = pdfUrl;
  1737. } else {
  1738. existingPdfContainer.style.display = 'none';
  1739. }
  1740. recordModalObj.show();
  1741. }
  1742. // SAVE Record API call
  1743. async function saveRecord(event) {
  1744. event.preventDefault();
  1745. showLoader(true, 'Saving record details...');
  1746. const id = document.getElementById('form-id').value;
  1747. const received_date = document.getElementById('form-received-date').value;
  1748. const type = document.getElementById('form-type').value;
  1749. const subject = document.getElementById('form-subject').value;
  1750. const amount = document.getElementById('form-amount').value;
  1751. const invoice_no = document.getElementById('form-invoice-no').value;
  1752. const receiptno = document.getElementById('form-receiptno').value;
  1753. const invoice_date = document.getElementById('form-invoice-date').value;
  1754. const physical_file_no = document.getElementById('form-physical-file-no').value;
  1755. const tatEl = document.getElementById('form-tat');
  1756. const tat = tatEl ? tatEl.value : '';
  1757. const status = document.getElementById('form-status').value;
  1758. // Preserve original values if they were loaded in edit mode
  1759. let originalRecord = null;
  1760. if (id) {
  1761. originalRecord = allRecords.find(r => String(r.id) === String(id));
  1762. }
  1763. const followupEl = document.getElementById('form-followup');
  1764. const followup = followupEl ? followupEl.value : (originalRecord ? originalRecord.followup : '');
  1765. const myfollowupEl = document.getElementById('form-myfollowup');
  1766. const myfollowup = myfollowupEl ? myfollowupEl.value : (originalRecord ? originalRecord.myfollowup : '');
  1767. const orderbyEl = document.getElementById('form-orderby');
  1768. const order_by = orderbyEl ? orderbyEl.value : (originalRecord ? originalRecord.order_by : '');
  1769. const remarks = document.getElementById('form-remarks').value;
  1770. // Resolve vendor
  1771. const vendorSelect = document.getElementById('form-vendor-select').value;
  1772. const vendorName = vendorSelect === '__custom__' ? document.getElementById('form-vendor-custom').value : vendorSelect;
  1773. if (!vendorName) {
  1774. alert('Please select or specify a vendor.');
  1775. showLoader(false);
  1776. return;
  1777. }
  1778. // Upload PDF if selected
  1779. let filePath = originalRecord ? originalRecord.file_path : null;
  1780. const pdfInput = document.getElementById('form-pdf');
  1781. if (pdfInput && pdfInput.files && pdfInput.files.length > 0) {
  1782. showLoader(true, 'Uploading PDF document...');
  1783. const formData = new FormData();
  1784. formData.append('file', pdfInput.files[0]);
  1785. const csrfHeaders = getCsrfHeaders();
  1786. try {
  1787. const uploadResponse = await fetch(contextPath + 'gccservicepaymenttracker/api/upload-pdf', {
  1788. method: 'POST',
  1789. headers: {
  1790. ...csrfHeaders
  1791. },
  1792. body: formData
  1793. });
  1794. const uploadResult = await uploadResponse.json();
  1795. if (uploadResult.success) {
  1796. filePath = uploadResult.filePath;
  1797. } else {
  1798. alert(uploadResult.error || 'Failed to upload PDF file.');
  1799. showLoader(false);
  1800. return;
  1801. }
  1802. } catch (err) {
  1803. console.error("Upload error:", err);
  1804. alert("Failed to upload PDF due to connection error.");
  1805. showLoader(false);
  1806. return;
  1807. }
  1808. }
  1809. const payload = {
  1810. id, received_date, type, vendor_name: vendorName, subject, amount, invoice_no,
  1811. receiptno, invoice_date, physical_file_no, tat, status, followup,
  1812. myfollowup, order_by, remarks, file_path: filePath
  1813. };
  1814. const csrfHeaders = getCsrfHeaders();
  1815. const headers = {
  1816. 'Content-Type': 'application/json',
  1817. ...csrfHeaders
  1818. };
  1819. try {
  1820. showLoader(true, 'Saving record details...');
  1821. const response = await fetch(contextPath + 'gccservicepaymenttracker/api/save', {
  1822. method: 'POST',
  1823. headers: headers,
  1824. body: JSON.stringify(payload)
  1825. });
  1826. const data = await response.json();
  1827. if (data.success) {
  1828. recordModalObj.hide();
  1829. loadCounts();
  1830. loadList();
  1831. } else {
  1832. alert(data.error || 'Failed to save record.');
  1833. showLoader(false);
  1834. }
  1835. } catch (err) {
  1836. console.error("Save Error:", err);
  1837. alert("Failed to save record due to connection error.");
  1838. showLoader(false);
  1839. }
  1840. }
  1841. // Delete record API call
  1842. function deleteRecord(id) {
  1843. if (!confirm('Are you sure you want to delete this payment tracking record?')) {
  1844. return;
  1845. }
  1846. showLoader(true, 'Deleting record...');
  1847. const csrfHeaders = getCsrfHeaders();
  1848. const headers = {
  1849. 'Content-Type': 'application/x-www-form-urlencoded',
  1850. ...csrfHeaders
  1851. };
  1852. const body = new URLSearchParams();
  1853. body.append('id', id);
  1854. fetch(contextPath + 'gccservicepaymenttracker/api/delete', {
  1855. method: 'POST',
  1856. headers: headers,
  1857. body: body
  1858. })
  1859. .then(response => response.json())
  1860. .then(data => {
  1861. if (data.success) {
  1862. loadCounts();
  1863. loadList();
  1864. } else {
  1865. alert(data.error || 'Failed to delete record.');
  1866. showLoader(false);
  1867. }
  1868. })
  1869. .catch(err => {
  1870. console.error("Delete Error:", err);
  1871. alert("Failed to delete record due to connection error.");
  1872. showLoader(false);
  1873. });
  1874. }
  1875. </script>
  1876. </body>
  1877. </html>