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.

1011 lines
29 KiB

4 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
4 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  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 IT Cell Outsourced Manpower Details</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. <link rel="stylesheet" type="text/css" th:href="@{/assets/css/body-form-styles.css}">
  11. <link rel="stylesheet" type="text/css" th:href="@{/assets/css/home-styles.css}">
  12. <th:block th:replace="~{fragments/header :: header}"></th:block>
  13. <style>
  14. .spreadsheet-container {
  15. background: #ffffff;
  16. border-radius: 12px;
  17. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  18. padding: 25px;
  19. margin: 20px auto;
  20. max-width: 100%;
  21. }
  22. .sheet-header {
  23. display: flex;
  24. justify-content: space-between;
  25. align-items: center;
  26. margin-bottom: 20px;
  27. border-bottom: 2px solid #eef2f5;
  28. padding-bottom: 15px;
  29. }
  30. .sheet-title {
  31. color: #0f5c78;
  32. font-weight: 800;
  33. font-size: 1.8rem;
  34. margin: 0;
  35. display: flex;
  36. align-items: center;
  37. gap: 12px;
  38. }
  39. .sheet-date-badge {
  40. background: #eef7fa;
  41. color: #0f5c78;
  42. font-weight: 700;
  43. padding: 6px 14px;
  44. border-radius: 20px;
  45. font-size: 0.9rem;
  46. border: 1px solid #d4e7ee;
  47. }
  48. .table-responsive {
  49. overflow-x: auto;
  50. border-radius: 8px;
  51. border: 1px solid #dcdfe3;
  52. }
  53. .sheet-table {
  54. width: 100%;
  55. border-collapse: collapse;
  56. font-size: 0.92rem;
  57. text-align: left;
  58. background: #fff;
  59. }
  60. .sheet-table th {
  61. background-color: #0f5c78;
  62. color: #ffffff;
  63. font-weight: 700;
  64. padding: 12px 10px;
  65. border: 1px solid #0b4960;
  66. text-align: center;
  67. vertical-align: middle;
  68. text-transform: uppercase;
  69. font-size: 0.85rem;
  70. letter-spacing: 0.5px;
  71. }
  72. .sheet-table td {
  73. padding: 8px 10px;
  74. border: 1px solid #e1e4e8;
  75. vertical-align: middle;
  76. color: #2c3e50;
  77. }
  78. .sheet-table tbody tr:nth-child(even) {
  79. background-color: #f9fbfb;
  80. }
  81. .sheet-table tbody tr:hover {
  82. background-color: #f1f6f8;
  83. }
  84. .col-center {
  85. text-align: center;
  86. }
  87. .col-right {
  88. text-align: right;
  89. }
  90. /* Inputs */
  91. .sheet-input-number {
  92. width: 75px;
  93. padding: 5px 8px;
  94. border: 1px solid #ccc;
  95. border-radius: 4px;
  96. text-align: center;
  97. font-weight: 600;
  98. color: #0f5c78;
  99. transition: border-color 0.2s, box-shadow 0.2s;
  100. }
  101. .sheet-input-number:focus {
  102. border-color: #1e82a6;
  103. box-shadow: 0 0 0 3px rgba(30, 130, 166, 0.15);
  104. outline: none;
  105. }
  106. .sheet-input-text {
  107. width: 90%;
  108. min-width: 150px;
  109. padding: 5px 10px;
  110. border: 1px solid #ccc;
  111. border-radius: 4px;
  112. font-weight: 500;
  113. transition: border-color 0.2s, box-shadow 0.2s;
  114. }
  115. .sheet-input-text:focus {
  116. border-color: #1e82a6;
  117. box-shadow: 0 0 0 3px rgba(30, 130, 166, 0.15);
  118. outline: none;
  119. }
  120. /* Totals Row */
  121. .totals-row {
  122. background-color: #e8eff2 !important;
  123. font-weight: 800;
  124. border-top: 2px solid #0f5c78;
  125. }
  126. .totals-row td {
  127. color: #0f5c78 !important;
  128. font-size: 0.98rem;
  129. border-top: 2px solid #0f5c78;
  130. }
  131. /* Action Buttons */
  132. .action-bar {
  133. display: flex;
  134. justify-content: flex-end;
  135. margin-top: 20px;
  136. gap: 15px;
  137. }
  138. .save-btn {
  139. background-color: #10b981;
  140. color: white;
  141. border: none;
  142. padding: 12px 28px;
  143. font-size: 1rem;
  144. font-weight: 700;
  145. border-radius: 6px;
  146. cursor: pointer;
  147. display: flex;
  148. align-items: center;
  149. gap: 8px;
  150. box-shadow: 0 4px 6px rgba(16, 185, 129, 0.2);
  151. transition: background-color 0.2s, transform 0.1s;
  152. }
  153. .save-btn:hover {
  154. background-color: #059669;
  155. }
  156. .save-btn:active {
  157. transform: scale(0.98);
  158. }
  159. /* Remarks styling */
  160. .vacant-highlight {
  161. color: #e74c3c;
  162. font-weight: 700;
  163. }
  164. .no-vacant {
  165. color: #27ae60;
  166. font-weight: 700;
  167. }
  168. /* Custom Dashboard Tabs */
  169. .dashboard-tabs {
  170. display: flex;
  171. gap: 15px;
  172. margin-bottom: 25px;
  173. border-bottom: 2px solid #eef2f5;
  174. padding-bottom: 10px;
  175. }
  176. .dashboard-tab {
  177. padding: 10px 22px;
  178. font-size: 1.05rem;
  179. font-weight: 700;
  180. color: #7f8c8d;
  181. background: none;
  182. border: none;
  183. border-radius: 8px 8px 0 0;
  184. cursor: pointer;
  185. position: relative;
  186. transition: all 0.3s ease;
  187. }
  188. .dashboard-tab:hover {
  189. color: #0f5c78;
  190. background-color: rgba(15, 92, 120, 0.05);
  191. }
  192. .dashboard-tab.active {
  193. color: #0f5c78;
  194. }
  195. .dashboard-tab.active::after {
  196. content: '';
  197. position: absolute;
  198. bottom: -12px;
  199. left: 0;
  200. width: 100%;
  201. height: 4px;
  202. background-color: #0f5c78;
  203. border-radius: 2px;
  204. }
  205. /* Vendor Directory Styles */
  206. .vendor-grid {
  207. display: grid;
  208. grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  209. gap: 20px;
  210. margin-bottom: 30px;
  211. }
  212. .vendor-card {
  213. background: #ffffff;
  214. border: 1px solid #eef2f6;
  215. border-radius: 12px;
  216. padding: 22px;
  217. box-shadow: 0 4px 15px rgba(0, 0, 0, 0.02);
  218. transition: all 0.3s ease;
  219. display: flex;
  220. flex-direction: column;
  221. justify-content: space-between;
  222. position: relative;
  223. overflow: hidden;
  224. }
  225. .vendor-card::before {
  226. content: '';
  227. position: absolute;
  228. top: 0;
  229. left: 0;
  230. width: 4px;
  231. height: 100%;
  232. background-color: #0f5c78;
  233. opacity: 0.8;
  234. }
  235. .vendor-card:hover {
  236. transform: translateY(-5px);
  237. box-shadow: 0 8px 25px rgba(15, 92, 120, 0.08);
  238. border-color: #d4e7ee;
  239. }
  240. .vendor-card-header {
  241. display: flex;
  242. justify-content: space-between;
  243. align-items: start;
  244. margin-bottom: 12px;
  245. }
  246. .vendor-card-title {
  247. font-size: 1.15rem;
  248. font-weight: 800;
  249. color: #2c3e50;
  250. margin: 0;
  251. text-transform: capitalize;
  252. line-height: 1.3;
  253. }
  254. .vendor-card-desc {
  255. font-size: 0.9rem;
  256. color: #7f8c8d;
  257. line-height: 1.4;
  258. margin-bottom: 20px;
  259. flex-grow: 1;
  260. }
  261. .vendor-card-footer {
  262. display: flex;
  263. justify-content: space-between;
  264. align-items: center;
  265. padding-top: 15px;
  266. border-top: 1px dashed #eef2f6;
  267. }
  268. .vendor-badge {
  269. font-size: 0.75rem;
  270. font-weight: 700;
  271. text-transform: uppercase;
  272. padding: 4px 10px;
  273. border-radius: 12px;
  274. background-color: #eef7fa;
  275. color: #0f5c78;
  276. }
  277. /* Employee Count Interactive Badge */
  278. .emp-count-badge {
  279. background: linear-gradient(135deg, #0f5c78, #1e82a6);
  280. color: #ffffff;
  281. font-weight: 800;
  282. font-size: 1.05rem;
  283. padding: 8px 18px;
  284. border-radius: 30px;
  285. cursor: pointer;
  286. border: none;
  287. box-shadow: 0 4px 10px rgba(30, 130, 166, 0.25);
  288. transition: all 0.2s ease;
  289. display: inline-flex;
  290. align-items: center;
  291. gap: 8px;
  292. }
  293. .emp-count-badge:hover {
  294. transform: scale(1.08);
  295. box-shadow: 0 6px 15px rgba(30, 130, 166, 0.4);
  296. background: linear-gradient(135deg, #1e82a6, #249cc8);
  297. color: #ffffff;
  298. }
  299. .emp-count-badge:active {
  300. transform: scale(0.96);
  301. }
  302. /* Modal styling overrides */
  303. .modal-custom-header {
  304. background-color: #0f5c78;
  305. color: white;
  306. border-top-left-radius: 14px;
  307. border-top-right-radius: 14px;
  308. padding: 20px;
  309. display: flex;
  310. justify-content: space-between;
  311. align-items: center;
  312. }
  313. .modal-custom-title {
  314. font-weight: 800;
  315. font-size: 1.35rem;
  316. display: flex;
  317. align-items: center;
  318. gap: 10px;
  319. margin: 0;
  320. }
  321. .modal-search-box {
  322. position: relative;
  323. margin-bottom: 20px;
  324. flex-grow: 1;
  325. }
  326. .modal-search-box input {
  327. padding: 10px 15px 10px 40px;
  328. border: 1px solid #dcdfe3;
  329. border-radius: 8px;
  330. width: 100%;
  331. font-size: 0.95rem;
  332. transition: all 0.3s ease;
  333. }
  334. .modal-search-box input:focus {
  335. border-color: #0f5c78;
  336. box-shadow: 0 0 0 3px rgba(15, 92, 120, 0.15);
  337. outline: none;
  338. }
  339. .modal-search-box i {
  340. position: absolute;
  341. left: 15px;
  342. top: 50%;
  343. transform: translateY(-50%);
  344. color: #7f8c8d;
  345. }
  346. .modal-table {
  347. font-size: 0.9rem;
  348. }
  349. .modal-table th {
  350. background-color: #eef7fa !important;
  351. color: #0f5c78 !important;
  352. font-weight: 700;
  353. text-transform: uppercase;
  354. font-size: 0.8rem;
  355. letter-spacing: 0.5px;
  356. border-bottom: 2px solid #0f5c78 !important;
  357. }
  358. .modal-table td {
  359. vertical-align: middle;
  360. }
  361. .modal-body {
  362. padding: 25px;
  363. }
  364. .export-btn {
  365. background-color: #27ae60;
  366. color: white;
  367. border: none;
  368. padding: 8px 16px;
  369. font-size: 0.9rem;
  370. font-weight: 700;
  371. border-radius: 6px;
  372. cursor: pointer;
  373. display: flex;
  374. align-items: center;
  375. gap: 8px;
  376. transition: all 0.2s ease;
  377. }
  378. .export-btn:hover {
  379. background-color: #219653;
  380. color: white;
  381. }
  382. </style>
  383. </head>
  384. <body>
  385. <th:block th:replace="~{fragments/navbar :: navbar(curPage=${curPage}, isAdmin=${isAdmin})}"></th:block>
  386. <div class="container" style="max-width: 95%;">
  387. <div class="spreadsheet-container">
  388. <!-- Dashboard Tabs -->
  389. <div class="dashboard-tabs">
  390. <button class="dashboard-tab active" id="btn-vendor-tab" onclick="switchTab('vendor-tab')">
  391. <i class="fa-solid fa-users-gear"></i> Vendor Directory & Counts
  392. </button>
  393. <button class="dashboard-tab" id="btn-manpower-tab" onclick="switchTab('manpower-tab')">
  394. <i class="fa-solid fa-file-spreadsheet"></i> Manpower Statistics
  395. </button>
  396. </div>
  397. <!-- Tab 1: Vendor Directory & Counts -->
  398. <div id="vendor-tab-content" class="tab-content">
  399. <div class="sheet-header">
  400. <h2 class="sheet-title">
  401. <i class="fa-solid fa-cubes text-primary"></i> Vendor Directory & Employees
  402. </h2>
  403. <span class="sheet-date-badge">Overall Count Wise Summary</span>
  404. </div>
  405. <div class="vendor-grid">
  406. <div class="vendor-card" th:each="vendor : ${vendorDetails}">
  407. <div class="vendor-card-header">
  408. <h3 class="vendor-card-title" th:text="${vendor.vendor_name}">Vendor Name</h3>
  409. <span class="vendor-badge" th:text="'Order: ' + ${vendor.orderby}">Order</span>
  410. </div>
  411. <p class="vendor-card-desc"
  412. th:text="${vendor.description != null ? vendor.description : 'No description provided.'}">
  413. Description</p>
  414. <div class="vendor-card-footer">
  415. <span style="font-weight: 600; color: #7f8c8d; font-size: 0.9rem;">
  416. Employees Count:
  417. </span>
  418. <button class="emp-count-badge"
  419. th:attr="onclick=|fetchAndShowEmployees('${vendor.vendor_name}')|">
  420. <i class="fa-solid fa-users"></i>
  421. <span th:text="${vendor.actual_employee_count}">0</span>
  422. </button>
  423. </div>
  424. </div>
  425. </div>
  426. </div>
  427. <!-- Tab 2: Manpower Statistics (Existing Spreadsheet) -->
  428. <div id="manpower-tab-content" class="tab-content" style="display: none;">
  429. <div class="sheet-header">
  430. <h2 class="sheet-title">
  431. <i class="fa-solid fa-file-spreadsheet"></i> Manpower Statistics Report
  432. </h2>
  433. <span class="sheet-date-badge" id="current-date-badge">As on 21.05.2026</span>
  434. </div>
  435. <div class="table-responsive">
  436. <table class="sheet-table" id="manpower-sheet-table">
  437. <thead>
  438. <tr>
  439. <th style="width: 50px;">S.No</th>
  440. <th>Description (Designation)</th>
  441. <th>Vendor Name</th>
  442. <th style="width: 120px;">Manpower Count<br>as per Tender</th>
  443. <th style="width: 120px;">Available<br>Manpower Count</th>
  444. <th>Working Location</th>
  445. <th style="width: 110px;">Registered<br>in App</th>
  446. <th>Vacant / Remarks</th>
  447. </tr>
  448. </thead>
  449. <tbody>
  450. <tr th:each="entry, stat : ${manpowerEntries}">
  451. <td class="col-center" th:text="${stat.count}">1</td>
  452. <td th:text="${entry.description}" style="font-weight: 600;">Description</td>
  453. <td th:text="${entry.vendor_name}">Vendor Name</td>
  454. <!-- Read-only Tender Count -->
  455. <td class="col-center tender-cell" th:text="${entry.tender_count}"
  456. style="font-weight: 700;">0</td>
  457. <!-- Editable Available Count -->
  458. <td class="col-center">
  459. <input type="number" min="0" class="sheet-input-number available-input"
  460. th:value="${entry.available_count}" th:data-id="${entry.id}"
  461. oninput="recalculateTotals()">
  462. </td>
  463. <td th:text="${entry.working_location}">Location</td>
  464. <td class="col-center" th:text="${entry.registered_count}">0</td>
  465. <!-- Editable Remarks -->
  466. <td>
  467. <input type="text" class="sheet-input-text remarks-input"
  468. th:value="${entry.vacant_remarks}" th:data-id="${entry.id}"
  469. oninput="recalculateTotals()">
  470. </td>
  471. </tr>
  472. </tbody>
  473. <tfoot>
  474. <tr class="totals-row">
  475. <td colspan="3" style="text-align: right; padding-right: 20px;">Total No</td>
  476. <td class="col-center" id="total-tender">319</td>
  477. <td class="col-center" id="total-available">292</td>
  478. <td></td>
  479. <td></td>
  480. <td id="total-vacant" style="font-weight: 800; padding-left: 20px;">30</td>
  481. </tr>
  482. </tfoot>
  483. </table>
  484. </div>
  485. <div class="action-bar">
  486. <button class="save-btn" onclick="saveManpowerData()">
  487. <i class="fa-solid fa-floppy-disk"></i> Save Changes
  488. </button>
  489. </div>
  490. </div>
  491. </div>
  492. </div>
  493. <!-- Employee Details Modal -->
  494. <div class="modal fade" id="employeeDetailsModal" tabindex="-1" aria-labelledby="employeeDetailsModalLabel"
  495. aria-hidden="true">
  496. <div class="modal-dialog modal-xl modal-dialog-centered">
  497. <div class="modal-content"
  498. style="border-radius: 14px; border: none; box-shadow: 0 10px 40px rgba(0,0,0,0.2);">
  499. <div class="modal-custom-header">
  500. <h5 class="modal-custom-title" id="employeeDetailsModalLabel">
  501. <i class="fa-solid fa-users-viewfinder"></i> Employee Details - <span id="modal-vendor-name"
  502. style="text-decoration: underline;">Vendor</span>
  503. </h5>
  504. <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
  505. aria-label="Close"></button>
  506. </div>
  507. <div class="modal-body">
  508. <!-- Search and Export Toolbar -->
  509. <div class="d-flex flex-column flex-md-row justify-content-between align-items-center gap-3 mb-4">
  510. <div class="modal-search-box flex-grow-1 m-0">
  511. <i class="fa-solid fa-magnifying-glass"></i>
  512. <input type="text" id="employee-search-input"
  513. placeholder="Search employees by name, designation, location, code..."
  514. oninput="filterEmployees()">
  515. </div>
  516. <div class="d-flex align-items-center gap-2 w-100 w-md-auto justify-content-md-end">
  517. <!-- Work Order PDF Link -->
  518. <a id="modal-workorder-file-link" href="#" target="_blank" class="btn btn-danger text-nowrap align-items-center justify-content-center gap-2" style="height: 44px; border-radius: 8px; font-weight: 600; display: none; padding: 0 15px; background-color: #c0392b; border-color: #c0392b; color: white;">
  519. <i class="fa-solid fa-file-pdf"></i> Work Order File
  520. </a>
  521. <select id="modal-designation-filter" class="form-select form-select-sm"
  522. style="height: 44px; border-radius: 8px; border: 1px solid #cbd5e1; padding: 0 35px 0 15px; font-weight: 600; min-width: 200px;"
  523. onchange="filterEmployees()">
  524. <option value="">- All Designations -</option>
  525. </select>
  526. <button class="export-btn text-nowrap" onclick="exportEmployeesToCSV()">
  527. <i class="fa-solid fa-file-csv"></i> Export to CSV
  528. </button>
  529. </div>
  530. </div>
  531. <!-- Table of Employees -->
  532. <div class="table-responsive" style="max-height: 450px; border: 1px solid #eef2f6;">
  533. <table class="table table-hover modal-table" id="modal-employee-table">
  534. <thead>
  535. <tr>
  536. <th style="width: 60px;" class="text-center">S.No</th>
  537. <th>Emp Code</th>
  538. <th>Name</th>
  539. <th>Email</th>
  540. <th>Mobile</th>
  541. <th>User Type</th>
  542. <th>Designation</th>
  543. <th class="text-center" style="width: 150px;">Last Updated On</th>
  544. <th>Location</th>
  545. <th>Vendor Name</th>
  546. </tr>
  547. </thead>
  548. <tbody id="modal-employee-tbody">
  549. <!-- Dynamically populated -->
  550. </tbody>
  551. </table>
  552. <div id="no-employees-found" class="text-center text-muted py-5" style="display: none;">
  553. <i class="fa-solid fa-user-slash fs-1 mb-3 text-secondary"></i>
  554. <p class="fw-bold mb-0">No matching employees found for this vendor.</p>
  555. </div>
  556. </div>
  557. </div>
  558. </div>
  559. </div>
  560. </div>
  561. <th:block th:replace="~{fragments/footer :: footer}"></th:block>
  562. <!-- Inline Spreadsheet Script -->
  563. <script th:inline="javascript">
  564. const contextPath = /*[[@{/}]]*/ '';
  565. const vendorDetailsData = /*[[${vendorDetails}]]*/[];
  566. const allDesignations = /*[[${designations}]]*/[];
  567. // Globals to store current modal state
  568. let currentVendorName = "";
  569. let currentVendorEmployees = [];
  570. let filteredVendorEmployees = [];
  571. // Handle switching between dynamic dashboard and spreadsheet
  572. function switchTab(tabId) {
  573. const tabs = document.querySelectorAll(".dashboard-tab");
  574. const tabContents = document.querySelectorAll(".tab-content");
  575. tabs.forEach(t => t.classList.remove("active"));
  576. tabContents.forEach(c => c.style.display = "none");
  577. if (tabId === 'vendor-tab') {
  578. document.getElementById("btn-vendor-tab").classList.add("active");
  579. document.getElementById("vendor-tab-content").style.display = "block";
  580. } else if (tabId === 'manpower-tab') {
  581. document.getElementById("btn-manpower-tab").classList.add("active");
  582. document.getElementById("manpower-tab-content").style.display = "block";
  583. }
  584. }
  585. // Fetch employee details from API dynamically and render in Modal
  586. function fetchAndShowEmployees(vendorName, designationFilter = null) {
  587. currentVendorName = vendorName;
  588. let displayTitle = vendorName;
  589. document.getElementById("modal-vendor-name").textContent = displayTitle;
  590. // Populate modal designation dropdown based on selected vendor ID
  591. const modalDesigFilter = document.getElementById("modal-designation-filter");
  592. modalDesigFilter.innerHTML = '<option value="">- All Designations -</option>';
  593. const vendor = vendorDetailsData.find(v => v.vendor_name === vendorName);
  594. if (vendor && vendor.designationDetails) {
  595. vendor.designationDetails.forEach(des => {
  596. const opt = document.createElement("option");
  597. opt.value = des.designation;
  598. let displayText = des.designation;
  599. const fileNo = des.File_no || des.file_no;
  600. if (fileNo && fileNo !== '-' && fileNo !== '') {
  601. displayText += " - " + fileNo;
  602. }
  603. opt.textContent = displayText;
  604. modalDesigFilter.appendChild(opt);
  605. });
  606. }
  607. if (designationFilter) {
  608. modalDesigFilter.value = designationFilter;
  609. } else {
  610. modalDesigFilter.value = "";
  611. }
  612. // Show Loading state inside tbody
  613. const tbody = document.getElementById("modal-employee-tbody");
  614. tbody.innerHTML = `
  615. <tr>
  616. <td colspan="10" class="text-center py-5">
  617. <div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem;">
  618. <span class="visually-hidden">Loading...</span>
  619. </div>
  620. <p class="mt-3 text-secondary fw-semibold">Fetching employee records from database...</p>
  621. </td>
  622. </tr>
  623. `;
  624. document.getElementById("no-employees-found").style.display = "none";
  625. document.getElementById("employee-search-input").value = "";
  626. // Show Modal using Bootstrap
  627. const modalElement = document.getElementById("employeeDetailsModal");
  628. const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
  629. modal.show();
  630. // Fetch from REST API
  631. fetch(contextPath + 'api/vendor-employees?vendorName=' + encodeURIComponent(vendorName))
  632. .then(res => res.json())
  633. .then(data => {
  634. currentVendorEmployees = data;
  635. filterEmployees();
  636. })
  637. .catch(err => {
  638. console.error("Error fetching employees:", err);
  639. tbody.innerHTML = `
  640. <tr>
  641. <td colspan="10" class="text-center py-5 text-danger fw-bold">
  642. <i class="fa-solid fa-circle-exclamation fs-1 mb-2"></i>
  643. <p>Failed to connect to server and fetch database records. Please try again.</p>
  644. </td>
  645. </tr>
  646. `;
  647. });
  648. }
  649. // Render the employee list inside the modal table
  650. function renderEmployeeTable(employees) {
  651. const tbody = document.getElementById("modal-employee-tbody");
  652. const noEmployeesEl = document.getElementById("no-employees-found");
  653. tbody.innerHTML = "";
  654. if (employees.length === 0) {
  655. noEmployeesEl.style.display = "block";
  656. return;
  657. }
  658. noEmployeesEl.style.display = "none";
  659. employees.forEach((emp, index) => {
  660. const tr = document.createElement("tr");
  661. tr.innerHTML = `
  662. <td class="text-center fw-bold text-secondary">${index + 1}</td>
  663. <td class="fw-semibold text-primary">${escapeHtml(emp.emp_code || '-')}</td>
  664. <td class="fw-bold text-dark">${escapeHtml(emp.name || '-')}</td>
  665. <td>${escapeHtml(emp.email || '-')}</td>
  666. <td>${escapeHtml(emp.mobile || '-')}</td>
  667. <td><span class="badge bg-secondary">${escapeHtml(emp.user_type || '-')}</span></td>
  668. <td class="fw-semibold">${escapeHtml(emp.designation || '-')}</td>
  669. <td class="text-center fw-semibold text-primary">${escapeHtml(formatDateTime(emp.updated_at || emp.cdate))}</td>
  670. <td><span class="badge bg-light text-dark border">${escapeHtml(emp.location || '-')}</span></td>
  671. <td class="fw-semibold text-secondary">${escapeHtml(emp.vendor_name || '-')}</td>
  672. `;
  673. tbody.appendChild(tr);
  674. });
  675. }
  676. // Instant fuzzy client-side search inside the modal
  677. function filterEmployees() {
  678. const query = document.getElementById("employee-search-input").value.toLowerCase().trim();
  679. const selectedDesignation = document.getElementById("modal-designation-filter").value.toLowerCase().trim();
  680. let list = currentVendorEmployees;
  681. // Apply designation filter
  682. if (selectedDesignation !== "") {
  683. list = list.filter(emp => emp.designation && emp.designation.trim().toLowerCase() === selectedDesignation);
  684. }
  685. if (query === "") {
  686. filteredVendorEmployees = list;
  687. } else {
  688. filteredVendorEmployees = list.filter(emp => {
  689. return (emp.name && emp.name.toLowerCase().includes(query)) ||
  690. (emp.emp_code && emp.emp_code.toLowerCase().includes(query)) ||
  691. (emp.designation && emp.designation.toLowerCase().includes(query)) ||
  692. (emp.location && emp.location.toLowerCase().includes(query)) ||
  693. (emp.email && emp.email.toLowerCase().includes(query)) ||
  694. (emp.mobile && emp.mobile.toLowerCase().includes(query)) ||
  695. (emp.vendor_name && emp.vendor_name.toLowerCase().includes(query)) ||
  696. (emp.user_type && emp.user_type.toLowerCase().includes(query));
  697. });
  698. }
  699. renderEmployeeTable(filteredVendorEmployees);
  700. updateWorkOrderFileLink();
  701. }
  702. function updateWorkOrderFileLink() {
  703. const selectEl = document.getElementById("modal-designation-filter");
  704. const fileLink = document.getElementById("modal-workorder-file-link");
  705. if (!selectEl || !fileLink) return;
  706. const selectedVal = selectEl.value.trim().toLowerCase();
  707. if (selectedVal === "") {
  708. fileLink.classList.add("d-none");
  709. fileLink.href = "#";
  710. return;
  711. }
  712. let filePath = null;
  713. if (currentVendorName === "All Active Employees") {
  714. // Find matching designation in allDesignations
  715. const matchedDes = allDesignations.find(d => d.designation && d.designation.trim().toLowerCase() === selectedVal);
  716. filePath = matchedDes ? matchedDes.file_path : null;
  717. } else {
  718. // Find matching designation details for current vendor
  719. const vendor = vendorDetailsData.find(v => v.vendor_name === currentVendorName);
  720. if (vendor && vendor.designationDetails) {
  721. const matchedDes = vendor.designationDetails.find(d => d.designation && d.designation.trim().toLowerCase() === selectedVal);
  722. filePath = matchedDes ? matchedDes.file_path : null;
  723. }
  724. }
  725. if (filePath) {
  726. filePath = filePath.trim();
  727. }
  728. if (filePath && filePath !== '-' && filePath !== '') {
  729. if (!filePath.toLowerCase().endsWith('.pdf')) {
  730. filePath += '.pdf';
  731. }
  732. const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
  733. fileLink.href = contextPath + normalizedPath;
  734. fileLink.classList.remove("d-none");
  735. fileLink.classList.add("d-inline-flex");
  736. return;
  737. }
  738. fileLink.classList.add("d-none");
  739. fileLink.href = "#";
  740. }
  741. // Premium CSV Exporter
  742. function exportEmployeesToCSV() {
  743. if (filteredVendorEmployees.length === 0) {
  744. alert("No employee records to export!");
  745. return;
  746. }
  747. let csvContent = "data:text/csv;charset=utf-8,";
  748. csvContent += "S.No,Employee Code,Name,Email,Mobile,User Type,Designation,Location,Vendor Name\n";
  749. filteredVendorEmployees.forEach((emp, index) => {
  750. const row = [
  751. index + 1,
  752. `"${(emp.emp_code || '-').replace(/"/g, '""')}"`,
  753. `"${(emp.name || '-').replace(/"/g, '""')}"`,
  754. `"${(emp.email || '-').replace(/"/g, '""')}"`,
  755. `"${(emp.mobile || '-').replace(/"/g, '""')}"`,
  756. `"${(emp.user_type || '-').replace(/"/g, '""')}"`,
  757. `"${(emp.designation || '-').replace(/"/g, '""')}"`,
  758. `"${(emp.location || '-').replace(/"/g, '""')}"`,
  759. `"${(emp.vendor_name || '-').replace(/"/g, '""')}"`
  760. ];
  761. csvContent += row.join(",") + "\n";
  762. });
  763. const encodedUri = encodeURI(csvContent);
  764. const link = document.createElement("a");
  765. link.setAttribute("href", encodedUri);
  766. // Filename format: [vendor_name]_employees.csv
  767. const sanitizedVendorName = currentVendorName.toLowerCase().replace(/[^a-z0-9]/g, "_");
  768. link.setAttribute("download", `${sanitizedVendorName}_employees.csv`);
  769. document.body.appendChild(link);
  770. link.click();
  771. document.body.removeChild(link);
  772. }
  773. // Helper function to safely escape HTML to prevent XSS
  774. function escapeHtml(str) {
  775. if (!str) return '';
  776. return String(str)
  777. .replace(/&/g, "&amp;")
  778. .replace(/</g, "&lt;")
  779. .replace(/>/g, "&gt;")
  780. .replace(/"/g, "&quot;")
  781. .replace(/'/g, "&#039;");
  782. }
  783. // Helper function to format timestamp to dd/mm/yyyy HH:MM:ss
  784. function formatDateTime(dateTimeStr) {
  785. if (!dateTimeStr || dateTimeStr === '-' || dateTimeStr.trim() === '') return '-';
  786. dateTimeStr = dateTimeStr.trim();
  787. let dateObj = new Date(dateTimeStr.replace('T', ' '));
  788. if (isNaN(dateObj.getTime())) {
  789. dateObj = new Date(dateTimeStr);
  790. }
  791. if (isNaN(dateObj.getTime())) return dateTimeStr;
  792. const day = String(dateObj.getDate()).padStart(2, '0');
  793. const month = String(dateObj.getMonth() + 1).padStart(2, '0');
  794. const year = dateObj.getFullYear();
  795. const hours = String(dateObj.getHours()).padStart(2, '0');
  796. const minutes = String(dateObj.getMinutes()).padStart(2, '0');
  797. const seconds = String(dateObj.getSeconds()).padStart(2, '0');
  798. return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`;
  799. }
  800. // Dynamic calculation when page loads
  801. document.addEventListener("DOMContentLoaded", () => {
  802. // Update the date badge dynamically to current date format
  803. const d = new Date();
  804. const day = String(d.getDate()).padStart(2, '0');
  805. const month = String(d.getMonth() + 1).padStart(2, '0');
  806. const year = d.getFullYear();
  807. document.getElementById("current-date-badge").textContent = `As on ${day}.${month}.${year}`;
  808. recalculateTotals();
  809. });
  810. // Recalculates Tender, Available, and Vacant counts dynamically in the footer
  811. function recalculateTotals() {
  812. const table = document.getElementById("manpower-sheet-table");
  813. const rows = table.querySelectorAll("tbody tr");
  814. let tenderSum = 0;
  815. let availableSum = 0;
  816. let vacantSum = 0;
  817. rows.forEach(row => {
  818. // Tender sum
  819. const tenderVal = parseInt(row.querySelector(".tender-cell").textContent) || 0;
  820. tenderSum += tenderVal;
  821. // Available sum
  822. const availableInput = row.querySelector(".available-input");
  823. const availableVal = parseInt(availableInput.value) || 0;
  824. availableSum += availableVal;
  825. // Vacant Sum parsing: Extract leading digit or number from vacant input
  826. const remarksInput = row.querySelector(".remarks-input");
  827. const remarksVal = remarksInput.value.trim();
  828. // Extract the first number found in the remarks input
  829. const numMatch = remarksVal.match(/^-?\d+/);
  830. if (numMatch) {
  831. vacantSum += parseInt(numMatch[0]);
  832. }
  833. });
  834. document.getElementById("total-tender").textContent = tenderSum;
  835. document.getElementById("total-available").textContent = availableSum;
  836. document.getElementById("total-vacant").textContent = vacantSum;
  837. }
  838. // Save dynamic spreadsheet updates back to database
  839. function saveManpowerData() {
  840. const table = document.getElementById("manpower-sheet-table");
  841. const rows = table.querySelectorAll("tbody tr");
  842. const payload = [];
  843. rows.forEach(row => {
  844. const availableInput = row.querySelector(".available-input");
  845. const remarksInput = row.querySelector(".remarks-input");
  846. payload.push({
  847. id: parseInt(availableInput.dataset.id),
  848. availableCount: parseInt(availableInput.value) || 0,
  849. vacantRemarks: remarksInput.value
  850. });
  851. });
  852. // CSRF tokens
  853. const csrfToken = document.querySelector("meta[name='_csrf']")?.getAttribute("content");
  854. const csrfHeader = document.querySelector("meta[name='_csrf_header']")?.getAttribute("content");
  855. const headers = {
  856. 'Content-Type': 'application/json'
  857. };
  858. if (csrfToken && csrfHeader) {
  859. headers[csrfHeader] = csrfToken;
  860. }
  861. // Save to API
  862. fetch(contextPath + 'api/save-manpower-stats', {
  863. method: 'POST',
  864. headers: headers,
  865. body: JSON.stringify(payload)
  866. })
  867. .then(res => res.json())
  868. .then(data => {
  869. if (data.success) {
  870. alert("Changes saved successfully to MySQL database!");
  871. window.location.reload();
  872. } else {
  873. alert("Failed to save changes: " + (data.error || "Unknown error"));
  874. }
  875. })
  876. .catch(err => {
  877. console.error("Error saving data:", err);
  878. alert("Something went wrong. Failed to communicate with server.");
  879. });
  880. }
  881. </script>
  882. </body>
  883. </html>