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.

4486 lines
198 KiB

​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  1. <!DOCTYPE html>
  2. <html xmlns:th="http://www.thymeleaf.org" lang="en">
  3. <head>
  4. <script th:inline="javascript">
  5. (function () {
  6. const currentUser = /*[[${session.pay_usrName}]]*/ null;
  7. if (currentUser && sessionStorage.getItem("pay_usrName") !== currentUser) {
  8. window.location.replace(/*[[@{/gccservicepaymenttracker/login}]]*/ '/gccservicepaymenttracker/login');
  9. }
  10. })();
  11. </script>
  12. <meta charset="UTF-8">
  13. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  14. <title>GCC Payment Tracker - Dashboard</title>
  15. <!-- CSRF Tokens -->
  16. <meta th:if="${_csrf != null}" name="_csrf" th:content="${_csrf.token}" />
  17. <meta th:if="${_csrf != null}" name="_csrf_header" th:content="${_csrf.headerName}" />
  18. <!-- Google Fonts -->
  19. <link
  20. href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Poppins:wght@300;400;500;600;700&display=swap"
  21. rel="stylesheet">
  22. <!-- Bootstrap & FontAwesome -->
  23. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
  24. <link rel="stylesheet" th:href="@{/assets/font-awesome-6.5.2/css/all.min.css}" type="text/css">
  25. <!-- Flatpickr Datepicker -->
  26. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
  27. <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
  28. <!-- Chart.js -->
  29. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  30. <!-- SheetJS for Export to Excel -->
  31. <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
  32. <!-- Custom Style Sheet mapping to theme -->
  33. <style>
  34. .text-info-dark {
  35. color: #075985 !important;
  36. font-weight: 600;
  37. }
  38. .text-warning-dark {
  39. color: #92400e !important;
  40. font-weight: 600;
  41. }
  42. body {
  43. background-color: #f4f7f6;
  44. font-family: 'Poppins', sans-serif;
  45. color: #333;
  46. }
  47. .header {
  48. background: linear-gradient(135deg, #09303d, #0f5c78);
  49. color: #fff;
  50. padding: 15px 30px;
  51. display: flex;
  52. justify-content: space-between;
  53. align-items: center;
  54. box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
  55. }
  56. .logo-left {
  57. display: flex;
  58. align-items: center;
  59. gap: 15px;
  60. }
  61. .logo-left img {
  62. width: 60px;
  63. height: 60px;
  64. }
  65. .logo-text h2 {
  66. font-family: 'Outfit', sans-serif;
  67. font-size: 1.1rem;
  68. font-weight: 700;
  69. margin: 0;
  70. line-height: 1.2;
  71. letter-spacing: 0.5px;
  72. }
  73. .logo-text p {
  74. font-size: 0.7rem;
  75. margin: 0;
  76. color: #aed6f1;
  77. }
  78. .header-title h2 {
  79. font-family: 'Outfit', sans-serif;
  80. font-size: 1.5rem;
  81. font-weight: 700;
  82. margin: 0;
  83. }
  84. .user-menu {
  85. display: flex;
  86. align-items: center;
  87. gap: 15px;
  88. }
  89. .user-badge {
  90. background: rgba(255, 255, 255, 0.15);
  91. padding: 8px 16px;
  92. border-radius: 20px;
  93. font-size: 0.85rem;
  94. font-weight: 600;
  95. display: flex;
  96. align-items: center;
  97. gap: 8px;
  98. }
  99. .btn-logout {
  100. background: #e74c3c;
  101. color: #fff;
  102. border: none;
  103. padding: 8px 16px;
  104. border-radius: 20px;
  105. font-size: 0.85rem;
  106. font-weight: 600;
  107. cursor: pointer;
  108. transition: all 0.3s;
  109. text-decoration: none;
  110. display: inline-flex;
  111. align-items: center;
  112. gap: 6px;
  113. }
  114. .btn-logout:hover {
  115. background: #c0392b;
  116. color: #fff;
  117. transform: translateY(-1.5px);
  118. }
  119. .main-container {
  120. max-width: 95%;
  121. margin: 35px auto;
  122. padding: 0 15px;
  123. }
  124. /* 4 Category Cards Dashboard */
  125. .category-grid {
  126. display: grid;
  127. grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  128. gap: 20px;
  129. margin-bottom: 35px;
  130. }
  131. .category-card {
  132. background: #ffffff;
  133. border: 1px solid #eef2f6;
  134. border-radius: 16px;
  135. padding: 24px;
  136. box-shadow: 0 5px 20px rgba(0, 0, 0, 0.02);
  137. transition: all 0.3s ease;
  138. position: relative;
  139. overflow: hidden;
  140. cursor: pointer;
  141. display: flex;
  142. align-items: center;
  143. justify-content: space-between;
  144. }
  145. .category-card::before {
  146. content: '';
  147. position: absolute;
  148. top: 0;
  149. left: 0;
  150. width: 5px;
  151. height: 100%;
  152. }
  153. /* SD Card Accent */
  154. .category-card.card-sd::before {
  155. background-color: #0f5c78;
  156. }
  157. .category-card.card-sd.active {
  158. border-color: #0f5c78;
  159. box-shadow: 0 10px 25px rgba(15, 92, 120, 0.15);
  160. background-color: #f0f7f9;
  161. }
  162. /* Invoice Card Accent */
  163. .category-card.card-invoice::before {
  164. background-color: #27ae60;
  165. }
  166. .category-card.card-invoice.active {
  167. border-color: #27ae60;
  168. box-shadow: 0 10px 25px rgba(39, 174, 96, 0.15);
  169. background-color: #f1fbf4;
  170. }
  171. /* EMD Card Accent */
  172. .category-card.card-emd::before {
  173. background-color: #8e44ad;
  174. }
  175. .category-card.card-emd.active {
  176. border-color: #8e44ad;
  177. box-shadow: 0 10px 25px rgba(142, 68, 173, 0.15);
  178. background-color: #f9f2fc;
  179. }
  180. /* BG Card Accent */
  181. .category-card.card-bg::before {
  182. background-color: #1e40af;
  183. }
  184. .category-card.card-bg.active {
  185. border-color: #1e40af;
  186. box-shadow: 0 10px 25px rgba(30, 64, 175, 0.15);
  187. background-color: #eff6ff;
  188. }
  189. /* DC Card Accent */
  190. .category-card.card-dc::before {
  191. background-color: #b91c1c;
  192. }
  193. .category-card.card-dc.active {
  194. border-color: #b91c1c;
  195. box-shadow: 0 10px 25px rgba(185, 28, 28, 0.15);
  196. background-color: #fef2f2;
  197. }
  198. /* ASD Card Accent */
  199. .category-card.card-asd::before {
  200. background-color: #d35400;
  201. }
  202. .category-card.card-asd.active {
  203. border-color: #d35400;
  204. box-shadow: 0 10px 25px rgba(211, 84, 0, 0.15);
  205. background-color: #fdf5e6;
  206. }
  207. /* Agreement Card Accent */
  208. .category-card.card-agreement::before {
  209. background-color: #db2777;
  210. }
  211. .category-card.card-agreement.active {
  212. border-color: #db2777;
  213. box-shadow: 0 10px 25px rgba(219, 39, 119, 0.15);
  214. background-color: #fdf2f8;
  215. }
  216. /* Total No of Files Card Accent */
  217. .category-card.card-total::before {
  218. background-color: #2563eb;
  219. }
  220. .category-card.card-total.active {
  221. border-color: #2563eb;
  222. box-shadow: 0 10px 25px rgba(37, 99, 235, 0.15);
  223. background-color: #eff6ff;
  224. }
  225. .card-total .card-icon {
  226. color: #2563eb;
  227. }
  228. /* Type Not Updated Files Card Accent */
  229. .category-card.card-status-not-updated::before {
  230. background-color: #475569;
  231. }
  232. .category-card.card-status-not-updated.active {
  233. border-color: #475569;
  234. box-shadow: 0 10px 25px rgba(71, 85, 105, 0.15);
  235. background-color: #f8fafc;
  236. }
  237. .card-status-not-updated .card-icon {
  238. color: #475569;
  239. }
  240. .category-card:hover {
  241. transform: translateY(-4px);
  242. box-shadow: 0 8px 25px rgba(0, 0, 0, 0.06);
  243. }
  244. .card-left {
  245. display: flex;
  246. flex-direction: column;
  247. gap: 5px;
  248. }
  249. .card-icon {
  250. font-size: 2.2rem;
  251. opacity: 0.85;
  252. transition: all 0.3s;
  253. }
  254. .card-sd .card-icon {
  255. color: #0f5c78;
  256. }
  257. .card-invoice .card-icon {
  258. color: #27ae60;
  259. }
  260. .card-emd .card-icon {
  261. color: #8e44ad;
  262. }
  263. .card-bg .card-icon {
  264. color: #1e40af;
  265. }
  266. .card-dc .card-icon {
  267. color: #b91c1c;
  268. }
  269. .card-asd .card-icon {
  270. color: #d35400;
  271. }
  272. .card-agreement .card-icon {
  273. color: #db2777;
  274. }
  275. .category-card:hover .card-icon {
  276. transform: scale(1.1);
  277. }
  278. .card-title {
  279. font-family: 'Outfit', sans-serif;
  280. font-size: 0.95rem;
  281. font-weight: 700;
  282. color: #64748b;
  283. text-transform: uppercase;
  284. letter-spacing: 0.5px;
  285. margin: 0;
  286. }
  287. .card-value {
  288. font-family: 'Outfit', sans-serif;
  289. font-size: 1.85rem;
  290. font-weight: 800;
  291. color: #1e293b;
  292. margin: 0;
  293. }
  294. /* Filter Section */
  295. .filter-card {
  296. background: #ffffff;
  297. border-radius: 16px;
  298. border: 1px solid #eef2f6;
  299. box-shadow: 0 8px 30px rgba(0, 0, 0, 0.02);
  300. padding: 24px;
  301. margin-bottom: 30px;
  302. }
  303. .filter-grid {
  304. display: grid;
  305. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  306. gap: 20px;
  307. align-items: end;
  308. }
  309. .filter-group {
  310. display: flex;
  311. flex-direction: column;
  312. gap: 8px;
  313. }
  314. .filter-label {
  315. font-size: 0.8rem;
  316. font-weight: 700;
  317. color: #64748b;
  318. text-transform: uppercase;
  319. letter-spacing: 0.5px;
  320. }
  321. .filter-input {
  322. height: 44px;
  323. border-radius: 10px;
  324. border: 1.5px solid #cbd5e1;
  325. padding: 10px 16px;
  326. font-size: 0.9rem;
  327. font-weight: 500;
  328. color: #334155;
  329. background-color: #f8fafc;
  330. transition: all 0.3s ease;
  331. }
  332. .filter-input:focus {
  333. outline: none;
  334. border-color: #0f5c78;
  335. background-color: #ffffff;
  336. box-shadow: 0 0 0 3px rgba(15, 92, 120, 0.15);
  337. }
  338. .btn-filter-search {
  339. height: 44px;
  340. border-radius: 10px;
  341. border: none;
  342. background: linear-gradient(135deg, #0f5c78, #1781a6);
  343. color: #ffffff;
  344. font-weight: 600;
  345. font-size: 0.9rem;
  346. transition: all 0.3s;
  347. display: flex;
  348. align-items: center;
  349. justify-content: center;
  350. gap: 8px;
  351. cursor: pointer;
  352. box-shadow: 0 4px 10px rgba(15, 92, 120, 0.25);
  353. }
  354. .btn-filter-search:hover {
  355. background: linear-gradient(135deg, #1781a6, #0f5c78);
  356. transform: translateY(-1.5px);
  357. }
  358. .btn-filter-reset {
  359. height: 44px;
  360. border-radius: 10px;
  361. border: 1.5px solid #cbd5e1;
  362. background-color: #ffffff;
  363. color: #475569;
  364. font-weight: 600;
  365. font-size: 0.9rem;
  366. transition: all 0.3s;
  367. display: flex;
  368. align-items: center;
  369. justify-content: center;
  370. gap: 8px;
  371. cursor: pointer;
  372. }
  373. .btn-filter-reset:hover {
  374. background-color: #f1f5f9;
  375. color: #0f5c78;
  376. border-color: #0f5c78;
  377. }
  378. /* Table Card Section */
  379. .table-card {
  380. background: #ffffff;
  381. border-radius: 16px;
  382. border: 1px solid #eef2f6;
  383. box-shadow: 0 8px 30px rgba(0, 0, 0, 0.02);
  384. padding: 24px;
  385. }
  386. .table-card-header {
  387. display: flex;
  388. justify-content: space-between;
  389. align-items: center;
  390. margin-bottom: 20px;
  391. flex-wrap: wrap;
  392. gap: 15px;
  393. }
  394. .table-card-title {
  395. font-family: 'Outfit', sans-serif;
  396. font-size: 1.25rem;
  397. font-weight: 700;
  398. color: #1e293b;
  399. margin: 0;
  400. display: flex;
  401. align-items: center;
  402. gap: 10px;
  403. }
  404. .active-filter-badge {
  405. background-color: #e2e8f0;
  406. color: #475569;
  407. font-size: 0.78rem;
  408. font-weight: 700;
  409. padding: 5px 12px;
  410. border-radius: 20px;
  411. text-transform: uppercase;
  412. }
  413. .btn-add-record {
  414. background: linear-gradient(135deg, #0f5c78, #1781a6);
  415. color: #ffffff;
  416. border: none;
  417. padding: 10px 20px;
  418. border-radius: 10px;
  419. font-size: 0.9rem;
  420. font-weight: 600;
  421. cursor: pointer;
  422. transition: all 0.3s;
  423. display: inline-flex;
  424. align-items: center;
  425. gap: 8px;
  426. box-shadow: 0 4px 10px rgba(15, 92, 120, 0.2);
  427. text-decoration: none;
  428. }
  429. .btn-add-record:hover {
  430. background: linear-gradient(135deg, #1781a6, #0f5c78);
  431. transform: translateY(-2px);
  432. color: #ffffff;
  433. }
  434. /* Custom Data Grid Table */
  435. .grid-table {
  436. font-size: 0.85rem;
  437. }
  438. .grid-table th {
  439. background-color: #f1f5f9 !important;
  440. color: #475569 !important;
  441. font-weight: 700;
  442. text-transform: uppercase;
  443. font-size: 0.75rem;
  444. letter-spacing: 0.5px;
  445. padding: 14px 10px;
  446. border-bottom: 2px solid #cbd5e1 !important;
  447. vertical-align: middle;
  448. }
  449. .grid-table td {
  450. padding: 12px 10px;
  451. vertical-align: middle;
  452. border-bottom: 1px solid #e2e8f0;
  453. }
  454. .grid-table tbody tr:hover {
  455. background-color: #f8fafc;
  456. }
  457. /* Badges for status */
  458. .status-badge {
  459. font-size: 0.72rem;
  460. font-weight: 700;
  461. padding: 4px 10px;
  462. border-radius: 12px;
  463. text-transform: uppercase;
  464. display: inline-block;
  465. }
  466. .status-pending {
  467. background-color: #fef9c3;
  468. color: #854d0e;
  469. }
  470. .status-awaiting-details {
  471. background-color: #ffedd5;
  472. color: #c2410c;
  473. }
  474. .status-pending-with-dept {
  475. background-color: #e0f2fe;
  476. color: #0369a1;
  477. }
  478. .status-completed {
  479. background-color: #d1fae5;
  480. color: #065f46;
  481. }
  482. .status-no-action {
  483. background-color: #f1f5f9;
  484. color: #475569;
  485. }
  486. .status-processed {
  487. background-color: #dbeafe;
  488. color: #1e40af;
  489. }
  490. .status-approved {
  491. background-color: #d1fae5;
  492. color: #065f46;
  493. }
  494. .status-paid {
  495. background-color: #dcfce7;
  496. color: #166534;
  497. }
  498. .status-rejected {
  499. background-color: #fee2e2;
  500. color: #991b1b;
  501. }
  502. .status-default {
  503. background-color: #f1f5f9;
  504. color: #475569;
  505. }
  506. /* Type Badges */
  507. .type-badge {
  508. font-size: 0.72rem;
  509. font-weight: 700;
  510. padding: 4px 10px;
  511. border-radius: 12px;
  512. text-transform: uppercase;
  513. display: inline-block;
  514. }
  515. .type-sd {
  516. background-color: #f0f7f9;
  517. color: #0f5c78;
  518. border: 1px solid rgba(15, 92, 120, 0.25);
  519. }
  520. .type-tax-invoice {
  521. background-color: #f1fbf4;
  522. color: #27ae60;
  523. border: 1px solid rgba(39, 174, 96, 0.25);
  524. }
  525. .type-emd {
  526. background-color: #f9f2fc;
  527. color: #8e44ad;
  528. border: 1px solid rgba(142, 68, 173, 0.25);
  529. }
  530. .type-bank-guarantee {
  531. background-color: #eff6ff;
  532. color: #1e40af;
  533. border: 1px solid rgba(30, 64, 175, 0.25);
  534. }
  535. .type-delivery-chellan {
  536. background-color: #fef2f2;
  537. color: #b91c1c;
  538. border: 1px solid rgba(185, 28, 28, 0.25);
  539. }
  540. .type-asd {
  541. background-color: #fdf5e6;
  542. color: #d35400;
  543. border: 1px solid rgba(211, 84, 0, 0.25);
  544. }
  545. .type-agreement {
  546. background-color: #fdf2f8;
  547. color: #db2777;
  548. border: 1px solid rgba(219, 39, 119, 0.25);
  549. }
  550. .btn-grid-action {
  551. width: 32px;
  552. height: 32px;
  553. border-radius: 8px;
  554. border: none;
  555. display: inline-flex;
  556. align-items: center;
  557. justify-content: center;
  558. font-size: 0.85rem;
  559. cursor: pointer;
  560. transition: all 0.2s;
  561. }
  562. .btn-grid-edit {
  563. background-color: #e0f2fe;
  564. color: #0369a1;
  565. }
  566. .btn-grid-edit:hover {
  567. background-color: #0369a1;
  568. color: #ffffff;
  569. transform: scale(1.08);
  570. }
  571. .btn-grid-delete {
  572. background-color: #fee2e2;
  573. color: #b91c1c;
  574. }
  575. .btn-grid-delete:hover {
  576. background-color: #b91c1c;
  577. color: #ffffff;
  578. transform: scale(1.08);
  579. }
  580. .btn-grid-pdf {
  581. background-color: #fef2f2;
  582. color: #b91c1c;
  583. border: 1px solid rgba(185, 28, 28, 0.15) !important;
  584. }
  585. .btn-grid-pdf:hover {
  586. background-color: #b91c1c;
  587. color: #ffffff;
  588. transform: scale(1.08);
  589. }
  590. /* Loading Overlay styling */
  591. #loading-overlay {
  592. position: fixed;
  593. top: 0;
  594. left: 0;
  595. width: 100vw;
  596. height: 100vh;
  597. background: rgba(255, 255, 255, 0.75);
  598. backdrop-filter: blur(8px);
  599. -webkit-backdrop-filter: blur(8px);
  600. z-index: 99999;
  601. display: none;
  602. flex-direction: column;
  603. align-items: center;
  604. justify-content: center;
  605. opacity: 0;
  606. pointer-events: none;
  607. transition: opacity 0.3s ease;
  608. }
  609. #loading-overlay.active {
  610. opacity: 1;
  611. pointer-events: all;
  612. display: flex;
  613. }
  614. .loader-spinner-container {
  615. background: white;
  616. padding: 2.5rem 3.5rem;
  617. border-radius: 16px;
  618. box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08);
  619. border: 1px solid rgba(15, 92, 120, 0.1);
  620. display: flex;
  621. flex-direction: column;
  622. align-items: center;
  623. gap: 1.25rem;
  624. }
  625. .loader-spinner {
  626. font-size: 3rem;
  627. color: #0f5c78;
  628. }
  629. .loader-text {
  630. font-family: 'Outfit', 'Inter', sans-serif;
  631. font-size: 1.1rem;
  632. font-weight: 600;
  633. color: #0f5c78;
  634. letter-spacing: 0.5px;
  635. }
  636. .empty-state {
  637. text-align: center;
  638. padding: 60px 20px;
  639. color: #64748b;
  640. }
  641. .empty-icon {
  642. font-size: 3.5rem;
  643. color: #cbd5e1;
  644. margin-bottom: 15px;
  645. }
  646. /* Custom Modal Header Styling */
  647. .modal-custom-header {
  648. background-color: #0f5c78;
  649. color: white;
  650. border-top-left-radius: 14px;
  651. border-top-right-radius: 14px;
  652. padding: 18px 24px;
  653. display: flex;
  654. justify-content: space-between;
  655. align-items: center;
  656. }
  657. .modal-custom-title {
  658. font-weight: 700;
  659. font-size: 1.25rem;
  660. display: flex;
  661. align-items: center;
  662. gap: 10px;
  663. margin: 0;
  664. font-family: 'Outfit', sans-serif;
  665. }
  666. .modal-content {
  667. border-radius: 14px;
  668. border: none;
  669. box-shadow: 0 15px 50px rgba(0, 0, 0, 0.25);
  670. }
  671. /* Pagination and Table Controls Styling */
  672. .pagination .page-item .page-link {
  673. color: #0f5c78;
  674. border-color: #cbd5e1;
  675. font-weight: 500;
  676. padding: 6px 12px;
  677. transition: all 0.2s;
  678. }
  679. .pagination .page-item.active .page-link {
  680. background-color: #0f5c78;
  681. border-color: #0f5c78;
  682. color: #ffffff;
  683. }
  684. .pagination .page-item .page-link:hover {
  685. background-color: #e2e8f0;
  686. border-color: #0f5c78;
  687. color: #0f5c78;
  688. }
  689. .pagination .page-item.disabled .page-link {
  690. color: #94a3b8;
  691. border-color: #cbd5e1;
  692. background-color: #f1f5f9;
  693. }
  694. .table-controls-row {
  695. display: flex;
  696. justify-content: space-between;
  697. align-items: center;
  698. margin-bottom: 20px;
  699. flex-wrap: wrap;
  700. gap: 15px;
  701. }
  702. .table-controls-left,
  703. .table-controls-right {
  704. display: flex;
  705. align-items: center;
  706. gap: 8px;
  707. }
  708. /* Interactive Summary Cell Styles */
  709. .cursor-pointer {
  710. cursor: pointer;
  711. transition: all 0.2s ease;
  712. }
  713. .cursor-pointer:hover {
  714. background-color: #cbd5e1 !important;
  715. transform: scale(1.08);
  716. box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
  717. z-index: 5;
  718. position: relative;
  719. }
  720. /* Customized premium scrollbar for responsive tables */
  721. .table-responsive::-webkit-scrollbar {
  722. height: 6px;
  723. width: 6px;
  724. }
  725. .table-responsive::-webkit-scrollbar-thumb {
  726. background: #cbd5e1;
  727. border-radius: 4px;
  728. }
  729. .table-responsive::-webkit-scrollbar-track {
  730. background: #f8fafc;
  731. }
  732. /* Force table cells to wrap cleanly and prevent word clipping */
  733. .grid-table th,
  734. .grid-table td {
  735. white-space: normal;
  736. }
  737. /* Keep straight horizontal scrolling layout only on mobile phone viewports */
  738. @media (max-width: 767.98px) {
  739. .grid-table th,
  740. .grid-table td {
  741. white-space: nowrap;
  742. }
  743. }
  744. /* Media Queries for all devices */
  745. @media (max-width: 991.98px) {
  746. .header {
  747. flex-direction: column;
  748. gap: 15px;
  749. text-align: center;
  750. padding: 20px;
  751. }
  752. .header-title h2 {
  753. font-size: 1.35rem;
  754. }
  755. .user-menu {
  756. width: 100%;
  757. justify-content: center;
  758. }
  759. .main-container {
  760. max-width: 100%;
  761. margin: 15px auto;
  762. }
  763. .category-grid {
  764. grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  765. gap: 15px;
  766. margin-bottom: 25px;
  767. }
  768. .card-value {
  769. font-size: 1.5rem;
  770. }
  771. .card-icon {
  772. font-size: 1.85rem;
  773. }
  774. }
  775. @media (max-width: 575.98px) {
  776. .header-title h2 {
  777. font-size: 1.15rem;
  778. }
  779. .category-grid {
  780. grid-template-columns: repeat(2, 1fr);
  781. gap: 10px;
  782. }
  783. .category-card {
  784. padding: 16px;
  785. border-radius: 12px;
  786. }
  787. .card-value {
  788. font-size: 1.25rem;
  789. }
  790. .card-icon {
  791. font-size: 1.5rem;
  792. }
  793. .filter-card,
  794. .table-card {
  795. padding: 16px;
  796. border-radius: 12px;
  797. }
  798. .table-controls-row {
  799. flex-direction: column;
  800. align-items: stretch;
  801. gap: 10px;
  802. }
  803. .table-controls-right input {
  804. width: 100% !important;
  805. }
  806. .btn-add-record {
  807. width: 100%;
  808. justify-content: center;
  809. }
  810. }
  811. /* Make modal-xl wider to fit multi-column data tables comfortably without squishing text */
  812. @media (min-width: 1200px) {
  813. .modal-xl {
  814. max-width: 94% !important;
  815. }
  816. }
  817. @media (min-width: 1600px) {
  818. .modal-xl {
  819. max-width: 1550px !important;
  820. }
  821. }
  822. </style>
  823. </head>
  824. <body>
  825. <!-- Fullscreen Loading Overlay -->
  826. <div id="loading-overlay">
  827. <div class="loader-spinner-container">
  828. <i class="fa-solid fa-spinner fa-spin loader-spinner"></i>
  829. <span class="loader-text" id="loader-overlay-text">Processing...</span>
  830. </div>
  831. </div>
  832. <!-- Header Bar -->
  833. <div class="header">
  834. <div class="logo-left">
  835. <img th:src="@{/assets/header-images/gcc1.png}" alt="Greater Chennai Corporation Logo">
  836. <div class="logo-text">
  837. <h2>GREATER CHENNAI CORPORATION</h2>
  838. <p>பெருநகர சென்னை மாநகராட்சி</p>
  839. </div>
  840. </div>
  841. <div class="header-title">
  842. <h2>Payment Processing Tracker</h2>
  843. </div>
  844. <div class="user-menu" style="display: flex; align-items: center; gap: 15px;">
  845. <!-- Latest Update Badge -->
  846. <div id="latest-update-badge" class="d-flex align-items-center gap-2 p-2 px-3 rounded text-white"
  847. style="display: none !important; background-color: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); font-size: 0.8rem; font-family: 'Outfit', sans-serif; cursor: pointer; transition: all 0.3s;"
  848. onclick="openLatestUpdatesModal()">
  849. <i class="fa-solid fa-bell text-warning animate-bounce"></i>
  850. <div>
  851. Latest Update: <span class="fw-bold text-warning" id="latest-update-count">0</span> File(s) on
  852. <span class="fw-semibold" id="latest-update-time">-</span>
  853. </div>
  854. </div>
  855. <span class="user-badge">
  856. <i class="fa-solid fa-user-shield"></i>
  857. <span th:text="${usrName}">Guest</span>
  858. </span>
  859. <a th:href="@{/gccservicepaymenttracker/logout}" class="btn-logout">
  860. <i class="fa-solid fa-right-from-bracket"></i> Logout
  861. </a>
  862. </div>
  863. </div>
  864. <div class="main-container">
  865. <!-- Category Grid (7 Cards) -->
  866. <div class="category-grid">
  867. <!-- Total No of Files -->
  868. <div class="category-card card-total" onclick="openModalForCount('total', null, 'Total No of Files')">
  869. <div class="card-left">
  870. <h3 class="card-title">Total No of Files</h3>
  871. <p class="card-value" id="count-total">0</p>
  872. </div>
  873. <i class="fa-solid fa-folder-open card-icon"></i>
  874. </div>
  875. <!-- Type Not Updated Files Files -->
  876. <div class="category-card card-status-not-updated"
  877. onclick="openModalForCount('category', 'status_not_updated', 'Type Not Updated Files Files')">
  878. <div class="card-left">
  879. <h3 class="card-title">Type Not Updated Files</h3>
  880. <p class="card-value" id="count-status-not-updated">0</p>
  881. </div>
  882. <i class="fa-solid fa-circle-question card-icon"></i>
  883. </div>
  884. <!-- Security Deposit -->
  885. <div class="category-card card-sd" onclick="openModalForCount('category', 'sd', 'Security Deposit')">
  886. <div class="card-left">
  887. <h3 class="card-title">Security Deposit</h3>
  888. <p class="card-value" id="count-sd">0</p>
  889. </div>
  890. <i class="fa-solid fa-wallet card-icon"></i>
  891. </div>
  892. <!-- Tax Invoice -->
  893. <div class="category-card card-invoice"
  894. onclick="openModalForCount('category', 'tax invoice', 'Tax Invoice')">
  895. <div class="card-left">
  896. <h3 class="card-title">Tax Invoice</h3>
  897. <p class="card-value" id="count-invoice">0</p>
  898. </div>
  899. <i class="fa-solid fa-file-invoice-dollar card-icon"></i>
  900. </div>
  901. <!-- EMD -->
  902. <div class="category-card card-emd" onclick="openModalForCount('category', 'emd', 'EMD')">
  903. <div class="card-left">
  904. <h3 class="card-title">EMD</h3>
  905. <p class="card-value" id="count-emd">0</p>
  906. </div>
  907. <i class="fa-solid fa-file-signature card-icon"></i>
  908. </div>
  909. <!-- Bank Guarantee commented out
  910. <div class="category-card card-bg" onclick="openModalForCount('category', 'bank guarantee', 'Bank Guarantee')">
  911. <div class="card-left">
  912. <h3 class="card-title">Bank Guarantee</h3>
  913. <p class="card-value" id="count-bg">0</p>
  914. </div>
  915. <i class="fa-solid fa-shield-halved card-icon"></i>
  916. </div>
  917. -->
  918. <!-- Delivery Chellan commented out
  919. <div class="category-card card-dc" onclick="openModalForCount('category', 'delivery chellan', 'Delivery Chellan')">
  920. <div class="card-left">
  921. <h3 class="card-title">Delivery Chellan</h3>
  922. <p class="card-value" id="count-dc">0</p>
  923. </div>
  924. <i class="fa-solid fa-truck-ramp-box card-icon"></i>
  925. </div>
  926. -->
  927. <!-- ASD -->
  928. <div class="category-card card-asd" onclick="openModalForCount('category', 'asd', 'ASD')">
  929. <div class="card-left">
  930. <h3 class="card-title">ASD</h3>
  931. <p class="card-value" id="count-asd">0</p>
  932. </div>
  933. <i class="fa-solid fa-vault card-icon"></i>
  934. </div>
  935. <!-- Agreement -->
  936. <div class="category-card card-agreement" onclick="openModalForCount('category', 'agreement', 'Agreement')">
  937. <div class="card-left">
  938. <h3 class="card-title">Agreement</h3>
  939. <p class="card-value" id="count-agreement">0</p>
  940. </div>
  941. <i class="fa-solid fa-file-contract card-icon"></i>
  942. </div>
  943. </div>
  944. <!-- Executive Summary & Charts Section -->
  945. <div class="row g-4 mb-4">
  946. <!-- Executive Summary Table Card -->
  947. <div class="col-lg-8">
  948. <div class="card h-100 border-0 shadow-sm" style="border-radius: 16px; overflow: hidden;">
  949. <div class="card-header bg-white py-3 border-0">
  950. <h4 class="card-title mb-0 fw-bold"
  951. style="font-family: 'Outfit', sans-serif; color: #09303d; font-size: 1.15rem;">
  952. <i class="fa-solid fa-chart-line me-2 text-primary"></i>Total Files in No of days wise count
  953. </h4>
  954. </div>
  955. <div class="card-body p-3">
  956. <div class="table-responsive">
  957. <table class="table table-bordered text-center align-middle mb-0"
  958. style="font-size: 0.82rem; border-color: #e2e8f0;">
  959. <thead class="table-light text-secondary fw-bold" style="font-size: 0.75rem;">
  960. <tr id="summary-thead-tr">
  961. <th style="background-color: #f8fafc; color: #475569;">Total No of files</th>
  962. <th style="background-color: #f0fdf4; color: #166534;">Completed Files</th>
  963. <th style="background-color: #fef2f2; color: #991b1b;">Pending Files</th>
  964. <th style="background-color: #fafaf9; color: #44403c;">1 to 15 Days</th>
  965. <th style="background-color: #fafaf9; color: #44403c;">16 to 30 Days</th>
  966. <th style="background-color: #fafaf9; color: #44403c;">31 to 60 Days</th>
  967. <th style="background-color: #fafaf9; color: #44403c;">61 to 90 Days</th>
  968. <th style="background-color: #fafaf9; color: #44403c;">91 to 120 Days</th>
  969. <th style="background-color: #fafaf9; color: #44403c;">Over 120 Days</th>
  970. </tr>
  971. </thead>
  972. <tbody>
  973. <tr class="fw-bold" style="font-size: 1rem; color: #1e293b;" id="summary-tbody-tr">
  974. <td id="summary-total" class="cursor-pointer text-primary"
  975. style="background-color: #f8fafc;"
  976. onclick="openModalForCount('total', null, 'Total No of files')">0</td>
  977. <td id="summary-completed" class="cursor-pointer text-success"
  978. onclick="openModalForCount('completed_custom', null, 'Completed Files')">0
  979. </td>
  980. <td id="summary-pending" class="cursor-pointer text-danger"
  981. style="color: #991b1b; font-weight: bold;"
  982. onclick="openModalForCount('pending_custom', null, 'Pending Files')">0</td>
  983. <td id="summary-1-15" class="cursor-pointer"
  984. onclick="openModalForCount('age', [1, 15], 'Age: 1 to 15 Days')">0</td>
  985. <td id="summary-16-30" class="cursor-pointer"
  986. onclick="openModalForCount('age', [16, 30], 'Age: 16 to 30 Days')">0</td>
  987. <td id="summary-31-60" class="cursor-pointer"
  988. onclick="openModalForCount('age', [31, 60], 'Age: 31 to 60 Days')">0</td>
  989. <td id="summary-61-90" class="cursor-pointer"
  990. onclick="openModalForCount('age', [61, 90], 'Age: 61 to 90 Days')">0</td>
  991. <td id="summary-91-120" class="cursor-pointer"
  992. onclick="openModalForCount('age', [91, 120], 'Age: 91 to 120 Days')">0</td>
  993. <td id="summary-over-120" class="cursor-pointer text-danger"
  994. onclick="openModalForCount('age', [121, null], 'Age: Over 120 Days')">0</td>
  995. </tr>
  996. </tbody>
  997. </table>
  998. </div>
  999. <p class="text-muted mt-2 mb-0" style="font-size: 0.72rem; font-style: italic;">
  1000. <i class="fa-solid fa-circle-info me-1"></i>Tip: Click on any count cell to filter the
  1001. detailed payment tracker list below.
  1002. </p>
  1003. </div>
  1004. </div>
  1005. </div>
  1006. <!-- Pie Chart Card -->
  1007. <div class="col-lg-4">
  1008. <div class="card h-100 border-0 shadow-sm" style="border-radius: 16px;">
  1009. <div class="card-header bg-white py-3 border-0">
  1010. <h4 class="card-title mb-0 fw-bold"
  1011. style="font-family: 'Outfit', sans-serif; color: #09303d; font-size: 1.15rem;">
  1012. <i class="fa-solid fa-chart-pie me-2 text-primary"></i>Invoices Status Distribution
  1013. </h4>
  1014. </div>
  1015. <div class="card-body d-flex align-items-center justify-content-center p-2"
  1016. style="position: relative; height: 180px;">
  1017. <canvas id="statusPieChart" style="max-height: 100%; max-width: 100%;"></canvas>
  1018. </div>
  1019. </div>
  1020. </div>
  1021. </div>
  1022. <!-- Vendor Breakdown Details Card -->
  1023. <div class="card border-0 shadow-sm mb-4" style="border-radius: 16px; overflow: hidden;">
  1024. <div
  1025. class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center flex-wrap gap-2">
  1026. <h4 class="card-title mb-0 fw-bold"
  1027. style="font-family: 'Outfit', sans-serif; color: #09303d; font-size: 1.15rem;">
  1028. <i class="fa-solid fa-users-viewfinder me-2 text-primary"></i>Vendor Breakdown Details (Invoices)
  1029. </h4>
  1030. <div style="font-size: 0.8rem; color: #64748b;">
  1031. <input type="text" id="vendor-breakdown-search" class="form-control form-control-sm"
  1032. style="width: 200px; border-radius: 8px;" placeholder="Search vendor breakdown..."
  1033. oninput="filterVendorBreakdown(this.value)">
  1034. </div>
  1035. </div>
  1036. <div class="card-body p-3">
  1037. <div class="table-responsive" style="max-height: 280px; overflow-y: auto;">
  1038. <table class="table table-striped table-bordered text-center align-middle mb-0"
  1039. style="font-size: 0.8rem; border-color: #e2e8f0;">
  1040. <thead class="table-light sticky-top text-secondary fw-bold"
  1041. style="font-size: 0.72rem; z-index: 10;">
  1042. <tr id="vendor-breakdown-thead-tr">
  1043. <th style="text-align: left; background-color: #f8fafc;">Vendor Name</th>
  1044. <th style="background-color: #f8fafc;">Total No of files</th>
  1045. <th style="background-color: #f0fdf4;">Completed Files</th>
  1046. <th style="background-color: #fffbeb;">Awaiting Details</th>
  1047. <th style="background-color: #f0f9ff;">Pending with Dept</th>
  1048. <th style="background-color: #fef2f2;">Pending</th>
  1049. <th style="background-color: #fafaf9;">1 to 15 Days</th>
  1050. <th style="background-color: #fafaf9;">16 to 30 Days</th>
  1051. <th style="background-color: #fafaf9;">31 to 60 Days</th>
  1052. <th style="background-color: #fafaf9;">61 to 90 Days</th>
  1053. <th style="background-color: #fafaf9;">91 to 120 Days</th>
  1054. <th style="background-color: #fafaf9;">Over 120 Days</th>
  1055. </tr>
  1056. </thead>
  1057. <tbody id="vendor-breakdown-tbody">
  1058. <!-- Populated dynamically via JS -->
  1059. </tbody>
  1060. </table>
  1061. </div>
  1062. </div>
  1063. </div>
  1064. <!-- Filter Card -->
  1065. <div class="filter-card">
  1066. <div class="filter-grid">
  1067. <!-- Date range picker
  1068. <div class="filter-group">
  1069. <label class="filter-label" for="filter-from-date">From Date</label>
  1070. <input type="text" id="filter-from-date" class="filter-input" placeholder="dd/mm/yyyy" readonly>
  1071. </div>
  1072. <div class="filter-group">
  1073. <label class="filter-label" for="filter-to-date">To Date</label>
  1074. <input type="text" id="filter-to-date" class="filter-input" placeholder="dd/mm/yyyy" readonly>
  1075. </div>
  1076. -->
  1077. <!-- Type Filter -->
  1078. <div class="filter-group">
  1079. <label class="filter-label" for="filter-type">Type</label>
  1080. <select id="filter-type" class="filter-input" onchange="handleTypeFilterChange(this.value)">
  1081. <option value="all">- All Types -</option>
  1082. <!-- Commented out static options to load from database
  1083. <option value="sd">Security Deposit</option>
  1084. <option value="tax invoice">Tax Invoice</option>
  1085. <option value="emd">EMD</option>
  1086. <option value="asd">ASD</option>
  1087. <option value="agreement">Agreement</option>
  1088. -->
  1089. </select>
  1090. </div>
  1091. <!-- Status Filter -->
  1092. <div class="filter-group">
  1093. <label class="filter-label" for="filter-status">Status</label>
  1094. <select id="filter-status" class="filter-input" onchange="loadList()">
  1095. <option value="">- All Statuses -</option>
  1096. </select>
  1097. </div>
  1098. <!-- Text Search -->
  1099. <div class="filter-group">
  1100. <label class="filter-label" for="search-text">Search Keyword</label>
  1101. <input type="text" id="search-text" class="filter-input" placeholder="Vendor, subject, invoice #..."
  1102. oninput="handleSearchInput(this.value)">
  1103. </div>
  1104. <!-- Action buttons -->
  1105. <button class="btn-filter-search" onclick="loadList()">
  1106. <i class="fa-solid fa-magnifying-glass"></i> Filter
  1107. </button>
  1108. <button class="btn-filter-reset" onclick="resetFilters()">
  1109. <i class="fa-solid fa-rotate-right"></i> Reset
  1110. </button>
  1111. </div>
  1112. </div>
  1113. <!-- Table Card Section -->
  1114. <div class="table-card">
  1115. <div class="table-card-header">
  1116. <h3 class="table-card-title">
  1117. <i class="fa-solid fa-table-list text-primary"></i>
  1118. <span>Payment Tracking Details</span>
  1119. <span class="active-filter-badge" id="active-category-badge">Type: All Types</span>
  1120. <span class="active-filter-badge ms-2" id="click-filter-badge"
  1121. style="display: none; background-color: #fee2e2; color: #b91c1c;"></span>
  1122. </h3>
  1123. <div class="d-flex align-items-center gap-2">
  1124. <button class="btn btn-sm btn-success fw-bold px-3 d-flex align-items-center gap-2"
  1125. style="border-radius: 8px; height: 36px; font-size: 0.85rem;"
  1126. onclick="exportMainTableToExcel()">
  1127. <i class="fa-solid fa-file-excel"></i> Export to Excel
  1128. </button>
  1129. <button th:if="${isAdmin}"
  1130. class="btn btn-sm btn-danger fw-bold px-3 d-flex align-items-center gap-2"
  1131. style="border-radius: 8px; height: 36px; font-size: 0.85rem; margin: 0;"
  1132. onclick="openDeletedRecordsModal()">
  1133. <i class="fa-solid fa-trash-can"></i> Deleted Records
  1134. </button>
  1135. <button th:if="${isAdmin}" class="btn-add-record" onclick="openAddModal()"
  1136. style="margin: 0; height: 36px;">
  1137. <i class="fa-solid fa-circle-plus"></i> Add Payment Tracking Record
  1138. </button>
  1139. </div>
  1140. </div>
  1141. <!-- Table Controls: Page size & Real-time Search -->
  1142. <div class="table-controls-row">
  1143. <div class="table-controls-left">
  1144. <span class="text-secondary fw-semibold" style="font-size: 0.85rem;">Show</span>
  1145. <select id="table-page-size" class="form-select form-select-sm" style="width: 80px;"
  1146. onchange="changePageSize(this.value)">
  1147. <option value="10">10</option>
  1148. <option value="20" selected>20</option>
  1149. <option value="30">30</option>
  1150. <option value="40">40</option>
  1151. <option value="50">50</option>
  1152. </select>
  1153. <span class="text-secondary fw-semibold" style="font-size: 0.85rem;">entries</span>
  1154. </div>
  1155. <div class="table-controls-right">
  1156. <span class="text-secondary fw-semibold" style="font-size: 0.85rem;">Search:</span>
  1157. <input type="text" id="table-search" class="form-control form-control-sm" style="width: 250px;"
  1158. placeholder="Search in this table..." oninput="handleTableSearch(this.value)">
  1159. </div>
  1160. </div>
  1161. <!-- Responsive Table Grid -->
  1162. <div class="table-responsive">
  1163. <table class="table grid-table table-striped table-hover align-middle" id="tracking-table">
  1164. <!-- Commented out old thead code
  1165. <thead>
  1166. <tr>
  1167. <th class="text-center" style="width: 50px;">S.No</th>
  1168. <th>Received Date</th>
  1169. <th>Type</th>
  1170. <th>Vendor Name</th>
  1171. <th>Subject</th>
  1172. <th>Amount</th>
  1173. <th>Invoice No</th>
  1174. <th>Receipt No</th>
  1175. <th>Invoice Date</th>
  1176. <th>Physical File No</th>
  1177. <th>Remarks</th>
  1178. <th>TAT</th>
  1179. <th>Status</th>
  1180. <th>Followup</th>
  1181. <th>My Followup</th>
  1182. <th class="text-center" style="width: 100px;">Actions</th>
  1183. </tr>
  1184. </thead>
  1185. -->
  1186. <thead>
  1187. <tr>
  1188. <th class="text-center" style="width: 50px;">Sr. No.</th>
  1189. <th>Received Date</th>
  1190. <th>Invoice Date</th>
  1191. <th>Physical File No.</th>
  1192. <th>eFile No./ Receipt No.</th>
  1193. <th>Subject</th>
  1194. <th>Type of File</th>
  1195. <th>Vendor Name</th>
  1196. <th>Amount</th>
  1197. <th>Age of Bill (Days)</th>
  1198. <th>Status (Pending / Completed)</th>
  1199. <th>Remarks</th>
  1200. <th>Last Updated At</th>
  1201. <th>Last Updated By</th>
  1202. <th class="text-center" style="width: 120px;">Actions</th>
  1203. </tr>
  1204. </thead>
  1205. <tbody id="tracking-tbody">
  1206. <!-- Populated dynamically via JS -->
  1207. </tbody>
  1208. </table>
  1209. <div id="no-records-found" class="empty-state" style="display: none;">
  1210. <i class="fa-solid fa-folder-open empty-icon"></i>
  1211. <h4>No Records Found</h4>
  1212. <p>No payment tracking records match the active criteria.</p>
  1213. </div>
  1214. </div>
  1215. <!-- Pagination Controls -->
  1216. <div class="d-flex justify-content-between align-items-center mt-3 flex-wrap gap-2">
  1217. <div class="text-secondary fw-semibold" style="font-size: 0.85rem;" id="pagination-info">
  1218. Showing 0 to 0 of 0 entries
  1219. </div>
  1220. <nav aria-label="Table navigation">
  1221. <ul class="pagination pagination-sm mb-0" id="pagination-controls">
  1222. <!-- Populated dynamically via JS -->
  1223. </ul>
  1224. </nav>
  1225. </div>
  1226. </div>
  1227. </div>
  1228. <!-- Add/Edit Modal Form -->
  1229. <div class="modal fade" id="recordModal" tabindex="-1" aria-labelledby="recordModalLabel" aria-hidden="true">
  1230. <div class="modal-dialog modal-lg modal-dialog-centered">
  1231. <div class="modal-content">
  1232. <div class="modal-custom-header">
  1233. <h5 class="modal-custom-title" id="recordModalLabel">
  1234. <i class="fa-solid fa-file-invoice-dollar"></i> Add/Edit Record
  1235. </h5>
  1236. <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
  1237. aria-label="Close"></button>
  1238. </div>
  1239. <div class="modal-body">
  1240. <form id="recordForm" onsubmit="saveRecord(event)">
  1241. <input type="hidden" id="form-id" name="id">
  1242. <div class="row g-3">
  1243. <!-- Received Date -->
  1244. <div class="col-md-6">
  1245. <label for="form-received-date" class="form-label fw-semibold text-secondary">Received
  1246. Date <span class="text-danger">*</span></label>
  1247. <input type="date" class="form-control" id="form-received-date" required
  1248. onclick="this.showPicker()">
  1249. </div>
  1250. <!-- Type -->
  1251. <div class="col-md-6">
  1252. <label for="form-type" class="form-label fw-semibold text-secondary">Type <span
  1253. class="text-danger">*</span></label>
  1254. <select class="form-select" id="form-type" required>
  1255. <!-- Commented out static options to load from database
  1256. <option value="sd">Security Deposit</option>
  1257. <option value="tax invoice">Tax Invoice</option>
  1258. <option value="emd">EMD</option>
  1259. <option value="asd">ASD</option>
  1260. <option value="agreement">Agreement</option>
  1261. -->
  1262. </select>
  1263. </div>
  1264. <!-- Vendor Name (Dynamic dropdown + Write-in) -->
  1265. <div class="col-md-6">
  1266. <label for="form-vendor-select" class="form-label fw-semibold text-secondary">Vendor
  1267. Name <span class="text-danger">*</span></label>
  1268. <select class="form-select mb-2" id="form-vendor-select"
  1269. onchange="toggleCustomVendor()">
  1270. <option value="">-- Select Vendor --</option>
  1271. <!-- Commented out model-populated options
  1272. <option th:each="vendor : ${vendorDetails}" th:value="${vendor.vendor_name}"
  1273. th:text="${vendor.vendor_name}"></option>
  1274. -->
  1275. <option value="__custom__">-- Write-in custom vendor --</option>
  1276. </select>
  1277. <input type="text" class="form-control" id="form-vendor-custom"
  1278. placeholder="Enter Custom Vendor Name" style="display: none;">
  1279. </div>
  1280. <!-- Subject -->
  1281. <div class="col-md-6">
  1282. <label for="form-subject" class="form-label fw-semibold text-secondary">Subject <span
  1283. class="text-danger">*</span></label>
  1284. <input type="text" class="form-control" id="form-subject"
  1285. placeholder="Enter subject/matter" required>
  1286. </div>
  1287. <!-- Amount -->
  1288. <div class="col-md-6">
  1289. <label for="form-amount" class="form-label fw-semibold text-secondary">Amount <span
  1290. class="text-danger">*</span></label>
  1291. <input type="text" class="form-control" id="form-amount"
  1292. placeholder="Enter amount (e.g. 50,000)" required>
  1293. </div>
  1294. <!-- Invoice No -->
  1295. <div class="col-md-6">
  1296. <label for="form-invoice-no" class="form-label fw-semibold text-secondary">Invoice
  1297. No</label>
  1298. <input type="text" class="form-control" id="form-invoice-no"
  1299. placeholder="Enter invoice number">
  1300. </div>
  1301. <!-- Receipt No -->
  1302. <div class="col-md-6">
  1303. <label for="form-receiptno" class="form-label fw-semibold text-secondary">Receipt
  1304. No</label>
  1305. <input type="text" class="form-control" id="form-receiptno"
  1306. placeholder="Enter Receipt No">
  1307. </div>
  1308. <!-- Invoice Date -->
  1309. <div class="col-md-6">
  1310. <label for="form-invoice-date" class="form-label fw-semibold text-secondary">Invoice
  1311. Date</label>
  1312. <input type="date" class="form-control" id="form-invoice-date"
  1313. onclick="this.showPicker()">
  1314. </div>
  1315. <!-- Physical File No -->
  1316. <div class="col-md-6">
  1317. <label for="form-physical-file-no"
  1318. class="form-label fw-semibold text-secondary">Physical File No</label>
  1319. <input type="text" class="form-control" id="form-physical-file-no"
  1320. placeholder="Enter physical file reference number">
  1321. </div>
  1322. <!-- TAT -->
  1323. <!-- <div class="col-md-6">
  1324. <label for="form-tat" class="form-label fw-semibold text-secondary">Turnaround Time
  1325. (TAT)</label>
  1326. <input type="text" class="form-control" id="form-tat" placeholder="e.g. 5 days">
  1327. </div> -->
  1328. <!-- Status -->
  1329. <div class="col-md-6">
  1330. <label for="form-status" class="form-label fw-semibold text-secondary">Status <span
  1331. class="text-danger">*</span></label>
  1332. <select class="form-select" id="form-status" required>
  1333. </select>
  1334. </div>
  1335. <!-- Followup commented out
  1336. <div class="col-md-6">
  1337. <label for="form-followup"
  1338. class="form-label fw-semibold text-secondary">Follow-up</label>
  1339. <input type="text" class="form-control" id="form-followup" placeholder="Follow-up note">
  1340. </div>
  1341. -->
  1342. <!-- My Followup commented out
  1343. <div class="col-md-6">
  1344. <label for="form-myfollowup" class="form-label fw-semibold text-secondary">My
  1345. Follow-up</label>
  1346. <input type="text" class="form-control" id="form-myfollowup"
  1347. placeholder="Internal follow-up note">
  1348. </div>
  1349. -->
  1350. <!-- Order By commented out
  1351. <div class="col-md-6">
  1352. <label for="form-orderby" class="form-label fw-semibold text-secondary">Order By</label>
  1353. <input type="text" class="form-control" id="form-orderby"
  1354. placeholder="Reference order description">
  1355. </div>
  1356. -->
  1357. <!-- PDF Attachment -->
  1358. <div class="col-md-12">
  1359. <label for="form-pdf" class="form-label fw-semibold text-secondary">PDF Attachment
  1360. (Optional)</label>
  1361. <input type="file" class="form-control" id="form-pdf" accept="application/pdf">
  1362. <div id="form-existing-pdf-container" class="mt-2" style="display: none;">
  1363. <span class="text-secondary fw-semibold">Current Document:</span>
  1364. <a id="form-existing-pdf-link" href="#" target="_blank"
  1365. class="ms-1 text-primary fw-bold text-decoration-none">
  1366. <i class="fa-solid fa-file-pdf"></i> View PDF
  1367. </a>
  1368. </div>
  1369. </div>
  1370. <!-- Remarks -->
  1371. <div class="col-12">
  1372. <label for="form-remarks" class="form-label fw-semibold text-secondary">Remarks</label>
  1373. <textarea class="form-control" id="form-remarks" rows="2"
  1374. placeholder="Additional remarks..."></textarea>
  1375. </div>
  1376. </div>
  1377. <div class="d-flex justify-content-end gap-2 mt-4">
  1378. <button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">Cancel</button>
  1379. <button type="submit" class="btn btn-primary px-4"
  1380. style="background-color: #0f5c78; border-color: #0f5c78;">
  1381. <i class="fa-solid fa-save"></i> Save Record
  1382. </button>
  1383. </div>
  1384. </form>
  1385. </div>
  1386. </div>
  1387. </div>
  1388. </div>
  1389. <!-- History Modal -->
  1390. <div class="modal fade" id="historyModal" tabindex="-1" aria-labelledby="historyModalLabel" aria-hidden="true">
  1391. <div class="modal-dialog modal-xl modal-dialog-centered">
  1392. <div class="modal-content">
  1393. <div class="modal-custom-header" style="background-color: #2c3e50;">
  1394. <h5 class="modal-custom-title" id="historyModalLabel">
  1395. <i class="fa-solid fa-clock-rotate-left"></i> Payment Record Update History
  1396. </h5>
  1397. <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
  1398. aria-label="Close"></button>
  1399. </div>
  1400. <div class="modal-body">
  1401. <div class="table-responsive" style="max-height: 400px; border: 1px solid #eef2f6;">
  1402. <table class="table table-hover modal-table grid-table" id="history-table">
  1403. <thead>
  1404. <tr>
  1405. <th style="width: 50px;" class="text-center">S.No</th>
  1406. <th>Received Date</th>
  1407. <th>Invoice Date</th>
  1408. <th>Subject</th>
  1409. <th>Type</th>
  1410. <th>Vendor Name</th>
  1411. <th>Amount</th>
  1412. <th>Status</th>
  1413. <th>Remarks</th>
  1414. <th>Last Updated At</th>
  1415. <th>Last Updated By</th>
  1416. <th class="text-center" style="width: 80px;">Document</th>
  1417. </tr>
  1418. </thead>
  1419. <tbody id="history-tbody">
  1420. <!-- Populated dynamically -->
  1421. </tbody>
  1422. </table>
  1423. <div id="no-history-found" class="text-center text-muted py-5" style="display: none;">
  1424. <i class="fa-solid fa-history fs-1 mb-3 text-secondary" style="opacity: 0.5;"></i>
  1425. <p class="fw-bold mb-0">No update history found for this record.</p>
  1426. </div>
  1427. </div>
  1428. </div>
  1429. </div>
  1430. </div>
  1431. </div>
  1432. <!-- Delete Confirmation Modal with Remarks -->
  1433. <div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmModalLabel"
  1434. aria-hidden="true" style="z-index: 1070;">
  1435. <div class="modal-dialog modal-dialog-centered">
  1436. <div class="modal-content">
  1437. <div class="modal-custom-header" style="background-color: #dc3545; color: white;">
  1438. <h5 class="modal-custom-title" id="deleteConfirmModalLabel">
  1439. <i class="fa-solid fa-triangle-exclamation"></i> Confirm Delete
  1440. </h5>
  1441. <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
  1442. aria-label="Close"></button>
  1443. </div>
  1444. <div class="modal-body">
  1445. <p class="text-dark fw-semibold">Are you sure you want to delete this payment tracking record?</p>
  1446. <div class="mb-3">
  1447. <label for="delete-remarks" class="form-label fw-bold text-secondary text-uppercase"
  1448. style="font-size: 0.75rem;">Remarks / Reason for Deletion <span
  1449. class="text-danger">*</span></label>
  1450. <textarea id="delete-remarks" class="form-control" rows="3"
  1451. placeholder="Enter remarks for deleting this record..." required></textarea>
  1452. </div>
  1453. </div>
  1454. <div class="modal-footer bg-light p-2">
  1455. <button type="button" class="btn btn-sm btn-secondary px-3" data-bs-dismiss="modal">Cancel</button>
  1456. <button type="button" class="btn btn-sm btn-danger px-3" onclick="submitDeleteRecord()">Delete
  1457. Record</button>
  1458. </div>
  1459. </div>
  1460. </div>
  1461. </div>
  1462. <!-- Count Details Modal -->
  1463. <div class="modal fade" id="countDetailsModal" tabindex="-1" aria-labelledby="countDetailsModalLabel"
  1464. aria-hidden="true">
  1465. <div class="modal-dialog modal-xl modal-dialog-centered">
  1466. <div class="modal-content">
  1467. <div class="modal-custom-header" style="background-color: #0f5c78;">
  1468. <h5 class="modal-custom-title" id="countDetailsModalLabel">
  1469. <i class="fa-solid fa-list-check"></i> Count Details
  1470. </h5>
  1471. <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
  1472. aria-label="Close"></button>
  1473. </div>
  1474. <div class="modal-body" style="padding: 1.5rem;">
  1475. <!-- Filters Row Inside Modal -->
  1476. <div class="row g-3 mb-4 p-3 bg-light rounded border align-items-end">
  1477. <div class="col-md-3">
  1478. <label for="modal-filter-type" class="form-label fw-bold text-secondary text-uppercase"
  1479. style="font-size: 0.75rem; letter-spacing: 0.5px;">Type Filter</label>
  1480. <select id="modal-filter-type" class="form-select form-select-sm"
  1481. onchange="applyModalFilters()">
  1482. <option value="all">- All Types -</option>
  1483. </select>
  1484. </div>
  1485. <div class="col-md-3">
  1486. <label for="modal-filter-status" class="form-label fw-bold text-secondary text-uppercase"
  1487. style="font-size: 0.75rem; letter-spacing: 0.5px;">Status Filter</label>
  1488. <select id="modal-filter-status" class="form-select form-select-sm"
  1489. onchange="applyModalFilters()">
  1490. <option value="all">- All Statuses -</option>
  1491. </select>
  1492. </div>
  1493. <div class="col-md-3">
  1494. <label for="modal-filter-vendor" class="form-label fw-bold text-secondary text-uppercase"
  1495. style="font-size: 0.75rem; letter-spacing: 0.5px;">Vendor Filter</label>
  1496. <select id="modal-filter-vendor" class="form-select form-select-sm"
  1497. onchange="applyModalFilters()">
  1498. <option value="all">- All Vendors -</option>
  1499. </select>
  1500. </div>
  1501. <div class="col-md-3">
  1502. <label class="form-label fw-bold text-secondary text-uppercase"
  1503. style="font-size: 0.75rem; letter-spacing: 0.5px;">Search:</label>
  1504. <input type="text" id="modal-table-search" class="form-control form-control-sm"
  1505. placeholder="Search in this table..." oninput="applyModalFilters()">
  1506. </div>
  1507. <div class="col-md-3">
  1508. <button
  1509. class="btn btn-sm btn-success fw-bold px-3 d-inline-flex align-items-center gap-2 w-100 justify-content-center"
  1510. style="border-radius: 8px; height: 31px;" onclick="exportModalTableToExcel()">
  1511. <i class="fa-solid fa-file-excel"></i> Export to Excel
  1512. </button>
  1513. </div>
  1514. </div>
  1515. <!-- Responsive Table Inside Modal -->
  1516. <div class="table-responsive"
  1517. style="max-height: 400px; overflow-y: auto; border: 1px solid #eef2f6;">
  1518. <table class="table grid-table table-striped table-hover align-middle mb-0"
  1519. id="modal-tracking-table">
  1520. <thead class="table-light">
  1521. <tr>
  1522. <th class="text-center" style="width: 50px;">Sr. No.</th>
  1523. <th>Received Date</th>
  1524. <th>Invoice Date</th>
  1525. <th>Physical File No.</th>
  1526. <th>eFile No./ Receipt No.</th>
  1527. <th>Subject</th>
  1528. <th>Type of File</th>
  1529. <th>Vendor Name</th>
  1530. <th>Amount</th>
  1531. <th>Age of Bill (Days)</th>
  1532. <th>Status (Pending / Completed)</th>
  1533. <th>Remarks</th>
  1534. <th>Last Updated Date</th>
  1535. <th>Last Updated By</th>
  1536. <th class="text-center" style="width: 120px;">Actions</th>
  1537. </tr>
  1538. </thead>
  1539. <tbody id="modal-tracking-tbody">
  1540. <!-- Populated dynamically via JS -->
  1541. </tbody>
  1542. </table>
  1543. <div id="modal-no-records-found" class="empty-state text-center py-5" style="display: none;">
  1544. <i class="fa-solid fa-folder-open empty-icon"
  1545. style="font-size: 2.5rem; color: #cbd5e1; margin-bottom: 10px;"></i>
  1546. <h5>No Records Found</h5>
  1547. <p>No payment tracking records match the active criteria.</p>
  1548. </div>
  1549. </div>
  1550. </div>
  1551. </div>
  1552. </div>
  1553. </div>
  1554. <!-- Deleted Records Modal -->
  1555. <div class="modal fade" id="deletedRecordsModal" tabindex="-1" aria-labelledby="deletedRecordsModalLabel"
  1556. aria-hidden="true">
  1557. <div class="modal-dialog modal-xl modal-dialog-centered">
  1558. <div class="modal-content">
  1559. <div class="modal-custom-header" style="background-color: #dc3545;">
  1560. <h5 class="modal-custom-title" id="deletedRecordsModalLabel">
  1561. <i class="fa-solid fa-trash-can"></i> Deleted Payment Records
  1562. </h5>
  1563. <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
  1564. aria-label="Close"></button>
  1565. </div>
  1566. <div class="modal-body" style="padding: 1.5rem;">
  1567. <!-- Responsive Table Inside Modal -->
  1568. <div class="table-responsive"
  1569. style="max-height: 450px; overflow-y: auto; border: 1px solid #eef2f6;">
  1570. <table class="table grid-table table-striped table-hover align-middle mb-0"
  1571. id="deleted-tracking-table">
  1572. <thead class="table-light">
  1573. <tr>
  1574. <th class="text-center" style="width: 50px;">Sr. No.</th>
  1575. <th>Received Date</th>
  1576. <th>Invoice Date</th>
  1577. <th>Physical File No.</th>
  1578. <th>eFile No./ Receipt No.</th>
  1579. <th>Subject</th>
  1580. <th>Type of File</th>
  1581. <th>Vendor Name</th>
  1582. <th>Amount</th>
  1583. <th>Age of Bill (Days)</th>
  1584. <th>Status</th>
  1585. <th>Deletion Remarks</th>
  1586. <th>Deleted Date</th>
  1587. <th>Deleted By</th>
  1588. <th class="text-center" style="width: 100px;">Actions</th>
  1589. </tr>
  1590. </thead>
  1591. <tbody id="deleted-tracking-tbody">
  1592. <!-- Populated dynamically via JS -->
  1593. </tbody>
  1594. </table>
  1595. <div id="deleted-no-records-found" class="empty-state text-center py-5" style="display: none;">
  1596. <i class="fa-solid fa-trash-arrow-up empty-icon"
  1597. style="font-size: 2.5rem; color: #cbd5e1; margin-bottom: 10px;"></i>
  1598. <h5>No Deleted Records</h5>
  1599. <p>No deleted payment tracking records found in the database.</p>
  1600. </div>
  1601. </div>
  1602. </div>
  1603. </div>
  1604. </div>
  1605. </div>
  1606. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
  1607. <!-- Custom Page Scripting -->
  1608. <script th:inline="javascript">
  1609. const contextPath = /*[[@{/}]]*/ '';
  1610. const isAdmin = /*[[${isAdmin}]]*/ false;
  1611. let activeCategory = 'all';
  1612. let allRecords = [];
  1613. let currentFilteredRecords = [];
  1614. let recordModalObj = null;
  1615. let historyModalObj = null;
  1616. let deletedRecordsModalObj = null;
  1617. let currentPage = 1;
  1618. let pageSize = 20;
  1619. // Custom dashboard filter & chart instances
  1620. let activeClickFilter = null;
  1621. let statusChartInstance = null;
  1622. let vendorBreakdownData = [];
  1623. // Dynamic lists for types, vendors, and statuses
  1624. let activeTypes = [];
  1625. let activeVendors = [];
  1626. let activeStatus = [];
  1627. // Date Pickers
  1628. // let fromDatePicker, toDatePicker;
  1629. document.addEventListener('DOMContentLoaded', async () => {
  1630. recordModalObj = new bootstrap.Modal(document.getElementById('recordModal'));
  1631. historyModalObj = new bootstrap.Modal(document.getElementById('historyModal'));
  1632. deletedRecordsModalObj = new bootstrap.Modal(document.getElementById('deletedRecordsModal'));
  1633. // initDatePickers();
  1634. await loadTypesAndVendors();
  1635. loadCounts();
  1636. loadList();
  1637. });
  1638. // Helper to map type_name to a category code
  1639. function getCategoryCode(typeName) {
  1640. const type = String(typeName).toLowerCase().trim();
  1641. if (type === 'asd' || type.includes('asd') || type.includes('additional security')) {
  1642. return 'asd';
  1643. } else if (type.includes('agreement')) {
  1644. return 'agreement';
  1645. } else if (type === 'sd' || (type.includes('security') && !type.includes('additional') && !type.includes('asd'))) {
  1646. return 'sd';
  1647. } else if (type.includes('invoice') || type.includes('bill')) {
  1648. return 'tax invoice';
  1649. } else if (type === 'emd' || type.includes('earnest')) {
  1650. return 'emd';
  1651. } else if (type.includes('guarantee') || type === 'bg') {
  1652. return 'bank guarantee';
  1653. } else if (type.includes('chellan') || type.includes('challan') || type === 'dc') {
  1654. return 'delivery chellan';
  1655. }
  1656. return type;
  1657. }
  1658. // Fetch types, vendors, and statuses from DB endpoints
  1659. async function loadTypesAndVendors() {
  1660. try {
  1661. const [typesRes, vendorsRes, statusRes] = await Promise.all([
  1662. fetch(contextPath + 'gccservicepaymenttracker/api/types?_=' + new Date().getTime()),
  1663. fetch(contextPath + 'gccservicepaymenttracker/api/vendors?_=' + new Date().getTime()),
  1664. fetch(contextPath + 'gccservicepaymenttracker/api/statuslist?_=' + new Date().getTime())
  1665. ]);
  1666. activeTypes = await typesRes.json();
  1667. activeVendors = await vendorsRes.json();
  1668. const fetchedStatus = await statusRes.json();
  1669. // Sort activeStatus to have completed first, then pending, then others
  1670. const completedStatuses = fetchedStatus.filter(s => getStatusKey(s.status_name) === 'completed');
  1671. const pendingStatuses = fetchedStatus.filter(s => getStatusKey(s.status_name) === 'pending');
  1672. const otherStatuses = fetchedStatus.filter(s =>
  1673. getStatusKey(s.status_name) !== 'completed' &&
  1674. getStatusKey(s.status_name) !== 'pending'
  1675. );
  1676. activeStatus = [...completedStatuses, ...pendingStatuses, ...otherStatuses];
  1677. populateDropdowns();
  1678. } catch (err) {
  1679. console.error("Error loading types, vendors or statuses:", err);
  1680. }
  1681. }
  1682. function getStatusKey(statusName) {
  1683. if (!statusName) return 'default';
  1684. const name = String(statusName).toLowerCase().trim();
  1685. if (name.includes('completed') || name.includes('approved') || name.includes('paid')) {
  1686. return 'completed';
  1687. }
  1688. if (name.includes('awaiting') || name.includes('details')) {
  1689. return 'awaiting details';
  1690. }
  1691. if (name.includes('dept') || name.includes('uac') || name.includes('office') || name.includes('fm unit')) {
  1692. return 'pending with dept';
  1693. }
  1694. if (name.includes('pending') || name.includes('initiated')) {
  1695. return 'pending';
  1696. }
  1697. if (name.includes('no action') || name.includes('action required') || name.includes('duplicate')) {
  1698. return 'no action';
  1699. }
  1700. return 'default';
  1701. }
  1702. const statusColors = {
  1703. 'completed': { bg: '#f0fdf4', text: '#166534', badgeClass: 'text-success' },
  1704. 'awaiting details': { bg: '#fffbeb', text: '#92400e', badgeClass: 'text-warning-dark' },
  1705. 'pending with dept': { bg: '#f0f9ff', text: '#075985', badgeClass: 'text-info-dark' },
  1706. 'pending': { bg: '#fef2f2', text: '#991b1b', badgeClass: 'text-danger' },
  1707. 'no action': { bg: '#fafaf9', text: '#44403c', badgeClass: 'text-secondary' },
  1708. 'processed': { bg: '#dbeafe', text: '#1e40af', badgeClass: 'text-primary' },
  1709. 'approved': { bg: '#d1fae5', text: '#065f46', badgeClass: 'text-success' },
  1710. 'paid': { bg: '#dcfce7', text: '#166534', badgeClass: 'text-success' },
  1711. 'rejected': { bg: '#fee2e2', text: '#991b1b', badgeClass: 'text-danger' }
  1712. };
  1713. function getStatusStyle(statusName) {
  1714. const key = getStatusKey(statusName);
  1715. return statusColors[key] || { bg: '#f1f5f9', text: '#475569', badgeClass: 'text-secondary' };
  1716. }
  1717. // Populate selects with dynamic options
  1718. function populateDropdowns() {
  1719. // Populate filter-type
  1720. const filterTypeSelect = document.getElementById('filter-type');
  1721. if (filterTypeSelect) {
  1722. filterTypeSelect.innerHTML = '<option value="all">- All Types -</option>';
  1723. const optNotUpdated = document.createElement('option');
  1724. optNotUpdated.value = 'status_not_updated';
  1725. optNotUpdated.textContent = 'Status Not Updated';
  1726. filterTypeSelect.appendChild(optNotUpdated);
  1727. activeTypes.forEach(t => {
  1728. const categoryCode = getCategoryCode(t.type_name);
  1729. const opt = document.createElement('option');
  1730. opt.value = categoryCode;
  1731. opt.textContent = t.type_name;
  1732. filterTypeSelect.appendChild(opt);
  1733. });
  1734. }
  1735. // Populate form-type
  1736. const formTypeSelect = document.getElementById('form-type');
  1737. if (formTypeSelect) {
  1738. formTypeSelect.innerHTML = '<option value="">-- Select Type --</option>';
  1739. activeTypes.forEach(t => {
  1740. const opt = document.createElement('option');
  1741. opt.value = t.id;
  1742. opt.textContent = t.type_name;
  1743. formTypeSelect.appendChild(opt);
  1744. });
  1745. }
  1746. // Populate filter-status
  1747. const filterStatusSelect = document.getElementById('filter-status');
  1748. if (filterStatusSelect) {
  1749. filterStatusSelect.innerHTML = '<option value="">- All Statuses -</option>';
  1750. activeStatus.forEach(s => {
  1751. const opt = document.createElement('option');
  1752. opt.value = s.id;
  1753. opt.textContent = s.status_name;
  1754. filterStatusSelect.appendChild(opt);
  1755. });
  1756. }
  1757. // Populate form-status
  1758. const formStatusSelect = document.getElementById('form-status');
  1759. if (formStatusSelect) {
  1760. formStatusSelect.innerHTML = '<option value="">-- Select Status --</option>';
  1761. activeStatus.forEach(s => {
  1762. const opt = document.createElement('option');
  1763. opt.value = s.id;
  1764. opt.textContent = s.status_name;
  1765. formStatusSelect.appendChild(opt);
  1766. });
  1767. }
  1768. // Populate form-vendor-select
  1769. const formVendorSelect = document.getElementById('form-vendor-select');
  1770. if (formVendorSelect) {
  1771. formVendorSelect.innerHTML = '<option value="">-- Select Vendor --</option>';
  1772. activeVendors.forEach(v => {
  1773. const opt = document.createElement('option');
  1774. opt.value = v.id;
  1775. opt.textContent = v.vendor_name;
  1776. formVendorSelect.appendChild(opt);
  1777. });
  1778. const customOpt = document.createElement('option');
  1779. customOpt.value = '__custom__';
  1780. customOpt.textContent = '-- Write-in custom vendor --';
  1781. formVendorSelect.appendChild(customOpt);
  1782. }
  1783. // Dynamically populate summary table headers
  1784. const summaryTheadTr = document.getElementById('summary-thead-tr');
  1785. if (summaryTheadTr) {
  1786. let summaryHeadHtml = `
  1787. <th style="background-color: #f8fafc; color: #475569;">Total No of files</th>
  1788. <th style="background-color: #f0fdf4; color: #166534;">Completed Files</th>
  1789. <th style="background-color: #fef2f2; color: #991b1b;">Pending Files</th>
  1790. `;
  1791. summaryHeadHtml += `
  1792. <th style="background-color: #fafaf9; color: #44403c;">1 to 15 Days</th>
  1793. <th style="background-color: #fafaf9; color: #44403c;">16 to 30 Days</th>
  1794. <th style="background-color: #fafaf9; color: #44403c;">31 to 60 Days</th>
  1795. <th style="background-color: #fafaf9; color: #44403c;">61 to 90 Days</th>
  1796. <th style="background-color: #fafaf9; color: #44403c;">91 to 120 Days</th>
  1797. <th style="background-color: #fafaf9; color: #44403c;">Over 120 Days</th>
  1798. `;
  1799. summaryTheadTr.innerHTML = summaryHeadHtml;
  1800. }
  1801. // Dynamically populate vendor breakdown table headers
  1802. const vendorBreakdownTheadTr = document.getElementById('vendor-breakdown-thead-tr');
  1803. if (vendorBreakdownTheadTr) {
  1804. const completedStatuses = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed');
  1805. const nonCompletedStatuses = activeStatus.filter(s => getStatusKey(s.status_name) !== 'completed');
  1806. let vendorHeadHtml = `
  1807. <th style="text-align: left; background-color: #f8fafc;">Vendor Name</th>
  1808. <th style="background-color: #f8fafc;">Total No of files</th>
  1809. `;
  1810. completedStatuses.forEach(s => {
  1811. const style = getStatusStyle(s.status_name);
  1812. vendorHeadHtml += `<th style="background-color: ${style.bg}; color: ${style.text};">${escapeHtml(s.status_name)}</th>`;
  1813. });
  1814. // Custom Pending column header
  1815. vendorHeadHtml += `<th style="background-color: #fef2f2; color: #991b1b;">Pending files</th>`;
  1816. nonCompletedStatuses.forEach(s => {
  1817. const style = getStatusStyle(s.status_name);
  1818. vendorHeadHtml += `<th style="background-color: ${style.bg}; color: ${style.text};">${escapeHtml(s.status_name)}</th>`;
  1819. });
  1820. vendorHeadHtml += `
  1821. <th style="background-color: #fafaf9;">1 to 15 Days</th>
  1822. <th style="background-color: #fafaf9;">16 to 30 Days</th>
  1823. <th style="background-color: #fafaf9;">31 to 60 Days</th>
  1824. <th style="background-color: #fafaf9;">61 to 90 Days</th>
  1825. <th style="background-color: #fafaf9;">91 to 120 Days</th>
  1826. <th style="background-color: #fafaf9;">Over 120 Days</th>
  1827. `;
  1828. vendorBreakdownTheadTr.innerHTML = vendorHeadHtml;
  1829. }
  1830. }
  1831. // Helper to get status name by ID or fallback to status value
  1832. function getStatusName(statusVal) {
  1833. if (statusVal === null || statusVal === undefined || String(statusVal).trim() === '') {
  1834. return '-';
  1835. }
  1836. const cleanVal = String(statusVal).trim();
  1837. const statusObj = activeStatus.find(s => String(s.id) === cleanVal);
  1838. if (statusObj) {
  1839. return statusObj.status_name;
  1840. }
  1841. return statusVal;
  1842. }
  1843. // Show/hide loader overlay
  1844. function showLoader(show, text = 'Processing...') {
  1845. const overlay = document.getElementById('loading-overlay');
  1846. const overlayText = document.getElementById('loader-overlay-text');
  1847. overlayText.textContent = text;
  1848. if (show) {
  1849. overlay.classList.add('active');
  1850. } else {
  1851. overlay.classList.remove('active');
  1852. }
  1853. }
  1854. // Get CSRF details
  1855. function getCsrfHeaders() {
  1856. const csrfToken = document.querySelector("meta[name='_csrf']")?.getAttribute("content");
  1857. const csrfHeader = document.querySelector("meta[name='_csrf_header']")?.getAttribute("content");
  1858. const headers = {};
  1859. if (csrfToken && csrfHeader) {
  1860. headers[csrfHeader] = csrfToken;
  1861. }
  1862. return headers;
  1863. }
  1864. // Category count loaders
  1865. function loadCounts() {
  1866. fetch(contextPath + 'gccservicepaymenttracker/api/counts?_=' + new Date().getTime())
  1867. .then(response => response.json())
  1868. .then(data => {
  1869. document.getElementById('count-sd').textContent = data.sd || 0;
  1870. document.getElementById('count-invoice').textContent = data['tax invoice'] || 0;
  1871. document.getElementById('count-emd').textContent = data.emd || 0;
  1872. const bgEl = document.getElementById('count-bg');
  1873. if (bgEl) bgEl.textContent = data['bank guarantee'] || 0;
  1874. const dcEl = document.getElementById('count-dc');
  1875. if (dcEl) dcEl.textContent = data['delivery chellan'] || 0;
  1876. document.getElementById('count-asd').textContent = data.asd || 0;
  1877. document.getElementById('count-agreement').textContent = data.agreement || 0;
  1878. // Set total and Type Not Updated Files counts
  1879. const totalEl = document.getElementById('count-total');
  1880. if (totalEl) totalEl.textContent = data.total || 0;
  1881. const missingTypeEl = document.getElementById('count-status-not-updated');
  1882. if (missingTypeEl) missingTypeEl.textContent = data.status_not_updated || 0;
  1883. })
  1884. .catch(err => console.error("Error loading category counts:", err));
  1885. }
  1886. // Handles select change of card
  1887. function selectCategory(category, cardElement) {
  1888. const isActive = cardElement.classList.contains('active');
  1889. // Remove active class from all
  1890. document.querySelectorAll('.category-card').forEach(el => el.classList.remove('active'));
  1891. const filterTypeSelect = document.getElementById('filter-type');
  1892. if (isActive) {
  1893. activeCategory = 'all';
  1894. document.getElementById('active-category-badge').textContent = 'Type: All Types';
  1895. if (filterTypeSelect) {
  1896. filterTypeSelect.value = 'all';
  1897. }
  1898. } else {
  1899. cardElement.classList.add('active');
  1900. activeCategory = category;
  1901. if (filterTypeSelect) {
  1902. filterTypeSelect.value = category;
  1903. }
  1904. // Update badge text
  1905. const labels = {
  1906. 'sd': 'Security Deposit',
  1907. 'tax invoice': 'Tax Invoice',
  1908. 'emd': 'EMD',
  1909. 'asd': 'ASD',
  1910. 'agreement': 'Agreement'
  1911. };
  1912. document.getElementById('active-category-badge').textContent = 'Type: ' + (labels[category] || category);
  1913. }
  1914. // Refresh list
  1915. loadList();
  1916. // Smoothly scroll down to the table card
  1917. document.querySelector('.table-card')?.scrollIntoView({ behavior: 'smooth' });
  1918. }
  1919. // Loading and Rendering list data
  1920. function loadList() {
  1921. showLoader(true, 'Loading payment tracking details...');
  1922. const statusVal = document.getElementById('filter-status').value;
  1923. const searchVal = document.getElementById('search-text').value;
  1924. // Sync search-text to table-search when hitting main filter search
  1925. const tblSearch = document.getElementById('table-search');
  1926. if (tblSearch) {
  1927. tblSearch.value = searchVal;
  1928. }
  1929. // Build query params
  1930. const params = new URLSearchParams();
  1931. if (activeCategory && activeCategory !== 'all') {
  1932. params.append('category', activeCategory);
  1933. }
  1934. if (statusVal) params.append('status', statusVal);
  1935. // Add cache buster
  1936. params.append('_', new Date().getTime());
  1937. fetch(contextPath + 'gccservicepaymenttracker/api/list?' + params.toString())
  1938. .then(response => response.json())
  1939. .then(data => {
  1940. allRecords = data;
  1941. currentPage = 1;
  1942. clearClickFilter(null);
  1943. calculateSummaries();
  1944. renderTable();
  1945. updateLatestUpdateBadge();
  1946. if (document.getElementById('countDetailsModal')?.classList.contains('show')) {
  1947. applyModalFilters();
  1948. }
  1949. })
  1950. .catch(err => {
  1951. console.error("Error loading details:", err);
  1952. showLoader(false);
  1953. });
  1954. }
  1955. function renderTable() {
  1956. const tbody = document.getElementById('tracking-tbody');
  1957. const noRecords = document.getElementById('no-records-found');
  1958. tbody.innerHTML = '';
  1959. const searchVal = document.getElementById('table-search')?.value || '';
  1960. let query = searchVal.toLowerCase().trim();
  1961. let filtered = allRecords.filter(row => {
  1962. // Apply activeClickFilter if set
  1963. if (activeClickFilter) {
  1964. const age = parseInt(calculateAgeOfBill(row.received_date), 10);
  1965. const status = String(row.resolved_status || getStatusName(row.status) || '').trim();
  1966. const vendor = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
  1967. if (activeClickFilter.type === 'status') {
  1968. const filterVal = String(activeClickFilter.value);
  1969. const matched = activeStatus.find(s => String(s.id) === filterVal);
  1970. if (matched) {
  1971. const rowStatusStr = String(row.status || '').trim();
  1972. const matches = rowStatusStr === String(matched.id) ||
  1973. rowStatusStr.toLowerCase() === matched.status_name.toLowerCase().trim();
  1974. if (!matches) return false;
  1975. } else {
  1976. if (status.toLowerCase() !== filterVal.toLowerCase()) return false;
  1977. }
  1978. } else if (activeClickFilter.type === 'age') {
  1979. const [min, max] = activeClickFilter.value;
  1980. if (max !== null) {
  1981. if (age < min || age > max) return false;
  1982. } else {
  1983. if (age < min) return false;
  1984. }
  1985. } else if (activeClickFilter.type === 'vendor') {
  1986. if (vendor !== activeClickFilter.vendorName) return false;
  1987. if (activeClickFilter.col) {
  1988. if (activeClickFilter.col.match(/^[0-9]+$/)) {
  1989. const matched = activeStatus.find(s => String(s.id) === activeClickFilter.col);
  1990. if (matched) {
  1991. const rowStatusStr = String(row.status || '').trim();
  1992. const rowMatches = rowStatusStr === String(matched.id) ||
  1993. rowStatusStr.toLowerCase() === matched.status_name.toLowerCase().trim();
  1994. if (!rowMatches) return false;
  1995. }
  1996. } else {
  1997. if (activeClickFilter.col === 'completed' && status.toLowerCase() !== 'completed') return false;
  1998. if (activeClickFilter.col === 'awaiting' && status.toLowerCase() !== 'awaiting details') return false;
  1999. if (activeClickFilter.col === 'pending_dept' && status.toLowerCase() !== 'pending with dept') return false;
  2000. if (activeClickFilter.col === 'age_1_15' && (age < 1 || age > 15)) return false;
  2001. if (activeClickFilter.col === 'age_16_30' && (age < 16 || age > 30)) return false;
  2002. if (activeClickFilter.col === 'age_31_60' && (age < 31 || age > 60)) return false;
  2003. if (activeClickFilter.col === 'age_61_90' && (age < 61 || age > 90)) return false;
  2004. if (activeClickFilter.col === 'age_91_120' && (age < 91 || age > 120)) return false;
  2005. if (activeClickFilter.col === 'age_over_120' && age <= 120) return false;
  2006. }
  2007. }
  2008. }
  2009. }
  2010. if (!query) return true;
  2011. const vName = String(row.resolved_vendor || row.vendor_name || '').toLowerCase();
  2012. const tName = String(row.resolved_type || row.type || '').toLowerCase();
  2013. const statusName = String(row.resolved_status || getStatusName(row.status) || '').toLowerCase();
  2014. return (
  2015. vName.includes(query) ||
  2016. tName.includes(query) ||
  2017. (row.subject && String(row.subject).toLowerCase().includes(query)) ||
  2018. (row.amount && String(row.amount).toLowerCase().includes(query)) ||
  2019. (row.invoice_no && String(row.invoice_no).toLowerCase().includes(query)) ||
  2020. (row.receiptno && String(row.receiptno).toLowerCase().includes(query)) ||
  2021. (row.physical_file_no && String(row.physical_file_no).toLowerCase().includes(query)) ||
  2022. (row.remarks && String(row.remarks).toLowerCase().includes(query)) ||
  2023. (row.tat && String(row.tat).toLowerCase().includes(query)) ||
  2024. statusName.includes(query)
  2025. );
  2026. });
  2027. currentFilteredRecords = filtered;
  2028. showLoader(false);
  2029. const totalRecords = filtered.length;
  2030. if (totalRecords === 0) {
  2031. noRecords.style.display = 'block';
  2032. document.getElementById('pagination-info').textContent = 'Showing 0 to 0 of 0 entries';
  2033. document.getElementById('pagination-controls').innerHTML = '';
  2034. return;
  2035. }
  2036. noRecords.style.display = 'none';
  2037. // Paginate
  2038. const totalPages = Math.ceil(totalRecords / pageSize);
  2039. if (currentPage > totalPages) {
  2040. currentPage = totalPages;
  2041. }
  2042. if (currentPage < 1) {
  2043. currentPage = 1;
  2044. }
  2045. const startIndex = (currentPage - 1) * pageSize;
  2046. const endIndex = Math.min(startIndex + pageSize, totalRecords);
  2047. const paginatedData = filtered.slice(startIndex, endIndex);
  2048. // Update Pagination Info
  2049. document.getElementById('pagination-info').textContent =
  2050. `Showing ${startIndex + 1} to ${endIndex} of ${totalRecords} entries`;
  2051. paginatedData.forEach((row, pageIndex) => {
  2052. const globalIndex = startIndex + pageIndex;
  2053. const tr = document.createElement('tr');
  2054. // S.No
  2055. const tdIndex = document.createElement('td');
  2056. tdIndex.className = 'text-center fw-semibold text-secondary';
  2057. tdIndex.textContent = globalIndex + 1;
  2058. tr.appendChild(tdIndex);
  2059. // Helper for blank values
  2060. const text = val => (val === null || val === undefined || String(val).trim() === '') ? '-' : val;
  2061. // Format Dates (yyyy-MM-dd to dd/MM/yyyy)
  2062. const formatDate = dateStr => {
  2063. if (!dateStr) return '-';
  2064. const parts = dateStr.split('-');
  2065. if (parts.length === 3) {
  2066. return `${parts[2]}/${parts[1]}/${parts[0]}`;
  2067. }
  2068. return dateStr;
  2069. };
  2070. // Format Date-Times (yyyy-MM-ddTHH:mm:ss... to dd/MM/yyyy HH:mm:ss)
  2071. const formatDateTime = dateTimeStr => {
  2072. if (!dateTimeStr) return '-';
  2073. try {
  2074. const d = new Date(dateTimeStr);
  2075. if (!isNaN(d.getTime())) {
  2076. const dd = String(d.getDate()).padStart(2, '0');
  2077. const MM = String(d.getMonth() + 1).padStart(2, '0');
  2078. const yyyy = d.getFullYear();
  2079. const HH = String(d.getHours()).padStart(2, '0');
  2080. const mm = String(d.getMinutes()).padStart(2, '0');
  2081. const ss = String(d.getSeconds()).padStart(2, '0');
  2082. return `${dd}/${MM}/${yyyy} ${HH}:${mm}:${ss}`;
  2083. }
  2084. } catch (e) {
  2085. console.error(e);
  2086. }
  2087. return dateTimeStr;
  2088. };
  2089. const getTypeBadge = (typeVal, filePath) => {
  2090. const cleanType = String(typeVal || '').trim().toLowerCase();
  2091. let label = cleanType;
  2092. let badgeClass = 'type-sd';
  2093. if (cleanType === 'asd' || cleanType.includes('asd') || cleanType.includes('additional security')) {
  2094. label = 'ASD';
  2095. badgeClass = 'type-asd';
  2096. } else if (cleanType.includes('agreement')) {
  2097. label = 'Agreement';
  2098. badgeClass = 'type-agreement';
  2099. } else if (cleanType === 'sd' || (cleanType.includes('security') && !cleanType.includes('additional') && !cleanType.includes('asd'))) {
  2100. label = 'Security Deposit';
  2101. badgeClass = 'type-sd';
  2102. } else if (cleanType.includes('invoice') || cleanType.includes('bill')) {
  2103. label = 'Tax Invoice';
  2104. badgeClass = 'type-tax-invoice';
  2105. } else if (cleanType === 'emd' || cleanType.includes('earnest')) {
  2106. label = 'EMD';
  2107. badgeClass = 'type-emd';
  2108. } else if (cleanType.includes('guarantee') || cleanType === 'bg') {
  2109. label = 'Bank Guarantee';
  2110. badgeClass = 'type-bank-guarantee';
  2111. } else if (cleanType.includes('chellan') || cleanType.includes('challan') || cleanType === 'dc') {
  2112. label = 'Delivery Chellan';
  2113. badgeClass = 'type-delivery-chellan';
  2114. } else {
  2115. // Fallback: check if cleanType is a numeric ID from activeTypes
  2116. const typeObj = activeTypes.find(t => String(t.id) === cleanType);
  2117. if (typeObj) {
  2118. return getTypeBadge(typeObj.type_name, filePath);
  2119. }
  2120. label = typeVal || '-';
  2121. badgeClass = 'status-default';
  2122. }
  2123. let onClickAttr = '';
  2124. if (filePath) {
  2125. let pdfUrl = filePath;
  2126. if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
  2127. pdfUrl = contextPath + pdfUrl.substring(1);
  2128. } else {
  2129. pdfUrl = contextPath + pdfUrl;
  2130. }
  2131. onClickAttr = `onclick="window.open('${escapeHtml(pdfUrl)}', '_blank')"`;
  2132. } else {
  2133. onClickAttr = `onclick="alert('pdf file not uploaded')"`;
  2134. }
  2135. return `<span class="type-badge ${badgeClass} cursor-pointer" ${onClickAttr} style="cursor: pointer;" title="${filePath ? 'Click to view PDF' : 'PDF not uploaded'}">${label}</span>`;
  2136. };
  2137. const dispStatus = String(row.resolved_status || getStatusName(row.status) || '').trim();
  2138. tr.innerHTML += `
  2139. <td>${formatDate(row.received_date)}</td>
  2140. <td>${formatDate(row.invoice_date)}</td>
  2141. <td>${text(row.physical_file_no)}</td>
  2142. <td>${text(row.receiptno)}</td>
  2143. <td>${text(row.subject)}</td>
  2144. <td>${getTypeBadge(row.resolved_type || row.type, row.file_path)}</td>
  2145. <td class="fw-semibold text-secondary">${text(row.resolved_vendor || row.vendor_name)}</td>
  2146. <td class="fw-semibold text-dark">&#8377; ${text(row.amount)}</td>
  2147. <td>${calculateAgeOfBill(row.received_date)}</td>
  2148. <td><span class="status-badge status-${dispStatus.toLowerCase().replace(/\s+/g, '-')}">${text(dispStatus)}</span></td>
  2149. <td><small class="text-muted">${text(row.remarks)}</small></td>
  2150. <td>${formatDateTime(row.updated_date)}</td>
  2151. <td class="fw-semibold text-secondary">${text(row.updated_by_name || '-')}</td>
  2152. `;
  2153. // Actions cell
  2154. const tdActions = document.createElement('td');
  2155. tdActions.className = 'text-center';
  2156. if (row.file_path) {
  2157. const btnViewPdf = document.createElement('button');
  2158. btnViewPdf.className = 'btn-grid-action btn-grid-pdf me-1';
  2159. btnViewPdf.innerHTML = '<i class="fa-solid fa-file-pdf"></i>';
  2160. btnViewPdf.title = 'View PDF Document';
  2161. btnViewPdf.onclick = () => {
  2162. let pdfUrl = row.file_path;
  2163. if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
  2164. pdfUrl = contextPath + pdfUrl.substring(1);
  2165. } else {
  2166. pdfUrl = contextPath + pdfUrl;
  2167. }
  2168. window.open(pdfUrl, '_blank');
  2169. };
  2170. tdActions.appendChild(btnViewPdf);
  2171. }
  2172. // History Button (Available to all logged in users)
  2173. const btnHistory = document.createElement('button');
  2174. btnHistory.className = 'btn-grid-action btn-outline-secondary me-1';
  2175. btnHistory.innerHTML = '<i class="fa-solid fa-clock-rotate-left"></i>';
  2176. btnHistory.style.border = '1px solid #cbd5e1';
  2177. btnHistory.title = 'View Update History';
  2178. btnHistory.onclick = () => showHistoryModal(row.id);
  2179. tdActions.appendChild(btnHistory);
  2180. if (isAdmin) {
  2181. const btnEdit = document.createElement('button');
  2182. btnEdit.className = 'btn-grid-action btn-grid-edit me-1';
  2183. btnEdit.innerHTML = '<i class="fa-solid fa-pen-to-square"></i>';
  2184. btnEdit.title = 'Edit Record';
  2185. btnEdit.onclick = () => openEditModal(row);
  2186. const btnDelete = document.createElement('button');
  2187. btnDelete.className = 'btn-grid-action btn-grid-delete';
  2188. btnDelete.innerHTML = '<i class="fa-solid fa-trash-can"></i>';
  2189. btnDelete.title = 'Delete Record';
  2190. btnDelete.onclick = () => deleteRecord(row.id);
  2191. tdActions.appendChild(btnEdit);
  2192. tdActions.appendChild(btnDelete);
  2193. }
  2194. tr.appendChild(tdActions);
  2195. tbody.appendChild(tr);
  2196. });
  2197. // Rebuild Pagination Controls
  2198. buildPaginationControls(totalPages);
  2199. }
  2200. function buildPaginationControls(totalPages) {
  2201. const container = document.getElementById('pagination-controls');
  2202. container.innerHTML = '';
  2203. if (totalPages <= 1) {
  2204. return;
  2205. }
  2206. // Previous Button
  2207. const prevLi = document.createElement('li');
  2208. prevLi.className = 'page-item' + (currentPage === 1 ? ' disabled' : '');
  2209. prevLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${currentPage - 1})">Previous</a>`;
  2210. container.appendChild(prevLi);
  2211. // Page numbers
  2212. const maxVisible = 5;
  2213. let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2));
  2214. let endPage = Math.min(totalPages, startPage + maxVisible - 1);
  2215. if (endPage - startPage + 1 < maxVisible) {
  2216. startPage = Math.max(1, endPage - maxVisible + 1);
  2217. }
  2218. if (startPage > 1) {
  2219. const firstLi = document.createElement('li');
  2220. firstLi.className = 'page-item';
  2221. firstLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(1)">1</a>`;
  2222. container.appendChild(firstLi);
  2223. if (startPage > 2) {
  2224. const dotsLi = document.createElement('li');
  2225. dotsLi.className = 'page-item disabled';
  2226. dotsLi.innerHTML = `<span class="page-link">...</span>`;
  2227. container.appendChild(dotsLi);
  2228. }
  2229. }
  2230. for (let i = startPage; i <= endPage; i++) {
  2231. const pageLi = document.createElement('li');
  2232. pageLi.className = 'page-item' + (i === currentPage ? ' active' : '');
  2233. pageLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${i})">${i}</a>`;
  2234. container.appendChild(pageLi);
  2235. }
  2236. if (endPage < totalPages) {
  2237. if (endPage < totalPages - 1) {
  2238. const dotsLi = document.createElement('li');
  2239. dotsLi.className = 'page-item disabled';
  2240. dotsLi.innerHTML = `<span class="page-link">...</span>`;
  2241. container.appendChild(dotsLi);
  2242. }
  2243. const lastLi = document.createElement('li');
  2244. lastLi.className = 'page-item';
  2245. lastLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${totalPages})">${totalPages}</a>`;
  2246. container.appendChild(lastLi);
  2247. }
  2248. // Next Button
  2249. const nextLi = document.createElement('li');
  2250. nextLi.className = 'page-item' + (currentPage === totalPages ? ' disabled' : '');
  2251. nextLi.innerHTML = `<a class="page-link" href="javascript:void(0)" onclick="goToPage(${currentPage + 1})">Next</a>`;
  2252. container.appendChild(nextLi);
  2253. }
  2254. function goToPage(page) {
  2255. currentPage = page;
  2256. renderTable();
  2257. }
  2258. function changePageSize(size) {
  2259. pageSize = parseInt(size, 10);
  2260. currentPage = 1;
  2261. renderTable();
  2262. }
  2263. // Handles search in local table search input
  2264. function handleTableSearch(keyword) {
  2265. const filterSearch = document.getElementById('search-text');
  2266. if (filterSearch) {
  2267. filterSearch.value = keyword;
  2268. }
  2269. currentPage = 1;
  2270. renderTable();
  2271. }
  2272. function handleSearchInput(keyword) {
  2273. const tblSearch = document.getElementById('table-search');
  2274. if (tblSearch) {
  2275. tblSearch.value = keyword;
  2276. }
  2277. currentPage = 1;
  2278. renderTable();
  2279. }
  2280. function handleTypeFilterChange(category) {
  2281. activeCategory = category;
  2282. // Remove active class from all top cards
  2283. document.querySelectorAll('.category-card').forEach(el => el.classList.remove('active'));
  2284. if (category !== 'all') {
  2285. const classMap = {
  2286. 'sd': 'card-sd',
  2287. 'tax invoice': 'card-invoice',
  2288. 'emd': 'card-emd',
  2289. 'asd': 'card-asd',
  2290. 'agreement': 'card-agreement'
  2291. };
  2292. const cardClass = classMap[category];
  2293. if (cardClass) {
  2294. const cardElement = document.querySelector('.' + cardClass);
  2295. if (cardElement) {
  2296. cardElement.classList.add('active');
  2297. }
  2298. }
  2299. const labels = {
  2300. 'sd': 'Security Deposit',
  2301. 'tax invoice': 'Tax Invoice',
  2302. 'emd': 'EMD',
  2303. 'asd': 'ASD',
  2304. 'agreement': 'Agreement'
  2305. };
  2306. document.getElementById('active-category-badge').textContent = 'Type: ' + (labels[category] || category);
  2307. } else {
  2308. document.getElementById('active-category-badge').textContent = 'Type: All Types';
  2309. }
  2310. loadList();
  2311. }
  2312. function resetFilters() {
  2313. document.getElementById('filter-status').value = '';
  2314. document.getElementById('search-text').value = '';
  2315. const filterType = document.getElementById('filter-type');
  2316. if (filterType) {
  2317. filterType.value = 'all';
  2318. }
  2319. activeCategory = 'all';
  2320. document.querySelectorAll('.category-card').forEach(el => el.classList.remove('active'));
  2321. document.getElementById('active-category-badge').textContent = 'Type: All Types';
  2322. const tblSearch = document.getElementById('table-search');
  2323. if (tblSearch) {
  2324. tblSearch.value = '';
  2325. }
  2326. currentPage = 1;
  2327. loadList();
  2328. }
  2329. // Toggle text field for dynamic vendor select dropdown
  2330. function toggleCustomVendor() {
  2331. const select = document.getElementById('form-vendor-select');
  2332. const customInput = document.getElementById('form-vendor-custom');
  2333. if (select.value === '__custom__') {
  2334. customInput.style.display = 'block';
  2335. customInput.required = true;
  2336. } else {
  2337. customInput.style.display = 'none';
  2338. customInput.required = false;
  2339. customInput.value = '';
  2340. }
  2341. }
  2342. // Edit/Add Modals
  2343. function openAddModal() {
  2344. document.getElementById('recordModalLabel').innerHTML = '<i class="fa-solid fa-circle-plus"></i> Add Payment Tracking Record';
  2345. document.getElementById('recordForm').reset();
  2346. document.getElementById('form-id').value = '';
  2347. // Set type to active category by default
  2348. const formTypeSelect = document.getElementById('form-type');
  2349. formTypeSelect.value = '';
  2350. if (activeCategory && activeCategory !== 'all') {
  2351. for (let option of formTypeSelect.options) {
  2352. if (option.value) {
  2353. const typeObj = activeTypes.find(t => String(t.id) === String(option.value));
  2354. if (typeObj && getCategoryCode(typeObj.type_name) === activeCategory) {
  2355. formTypeSelect.value = option.value;
  2356. break;
  2357. }
  2358. }
  2359. }
  2360. }
  2361. const pendingObj = activeStatus.find(s => s.status_name.toLowerCase().trim() === 'pending');
  2362. document.getElementById('form-status').value = pendingObj ? String(pendingObj.id) : '';
  2363. document.getElementById('form-vendor-custom').style.display = 'none';
  2364. document.getElementById('form-vendor-custom').required = false;
  2365. // Reset PDF upload elements
  2366. document.getElementById('form-pdf').value = '';
  2367. document.getElementById('form-existing-pdf-container').style.display = 'none';
  2368. recordModalObj.show();
  2369. }
  2370. function openEditModal(row) {
  2371. document.getElementById('recordModalLabel').innerHTML = '<i class="fa-solid fa-file-signature"></i> Edit Payment Tracking Record';
  2372. document.getElementById('recordForm').reset();
  2373. document.getElementById('form-id').value = row.id;
  2374. document.getElementById('form-received-date').value = row.received_date || '';
  2375. // Set type select
  2376. const typeSelect = document.getElementById('form-type');
  2377. const rowType = String(row.type || '').trim().toLowerCase();
  2378. typeSelect.value = '';
  2379. let found = false;
  2380. for (let option of typeSelect.options) {
  2381. if (option.value && String(option.value) === String(row.type)) {
  2382. typeSelect.value = option.value;
  2383. found = true;
  2384. break;
  2385. }
  2386. }
  2387. if (!found && rowType) {
  2388. const legacyCategory = getCategoryCode(rowType);
  2389. for (let option of typeSelect.options) {
  2390. if (option.value) {
  2391. const typeObj = activeTypes.find(t => String(t.id) === String(option.value));
  2392. if (typeObj) {
  2393. const optCategory = getCategoryCode(typeObj.type_name);
  2394. if (optCategory === legacyCategory) {
  2395. typeSelect.value = option.value;
  2396. found = true;
  2397. break;
  2398. }
  2399. }
  2400. }
  2401. }
  2402. }
  2403. document.getElementById('form-subject').value = row.subject || '';
  2404. document.getElementById('form-amount').value = row.amount || '';
  2405. document.getElementById('form-invoice-no').value = row.invoice_no || '';
  2406. document.getElementById('form-receiptno').value = row.receiptno || '';
  2407. document.getElementById('form-invoice-date').value = row.invoice_date || '';
  2408. document.getElementById('form-physical-file-no').value = row.physical_file_no || '';
  2409. const tatEl = document.getElementById('form-tat');
  2410. if (tatEl) tatEl.value = row.tat || '';
  2411. const statusSelect = document.getElementById('form-status');
  2412. const rowStatus = String(row.status || '').trim();
  2413. statusSelect.value = '';
  2414. let statusFound = false;
  2415. // 1. Try matching by option value (which can be ID or exact string match)
  2416. for (let option of statusSelect.options) {
  2417. if (option.value && String(option.value).toLowerCase() === rowStatus.toLowerCase()) {
  2418. statusSelect.value = option.value;
  2419. statusFound = true;
  2420. break;
  2421. }
  2422. }
  2423. // 2. Try matching by option text content (which is the status name)
  2424. if (!statusFound && rowStatus) {
  2425. for (let option of statusSelect.options) {
  2426. if (option.text && option.text.toLowerCase().trim() === rowStatus.toLowerCase().trim()) {
  2427. statusSelect.value = option.value;
  2428. statusFound = true;
  2429. break;
  2430. }
  2431. }
  2432. }
  2433. // Fallback default
  2434. if (!statusFound) {
  2435. const pendingObj = activeStatus.find(s => s.status_name.toLowerCase().trim() === 'pending');
  2436. statusSelect.value = pendingObj ? String(pendingObj.id) : '';
  2437. }
  2438. const followupEl = document.getElementById('form-followup');
  2439. if (followupEl) followupEl.value = row.followup || '';
  2440. const myfollowupEl = document.getElementById('form-myfollowup');
  2441. if (myfollowupEl) myfollowupEl.value = row.myfollowup || '';
  2442. const orderbyEl = document.getElementById('form-orderby');
  2443. if (orderbyEl) orderbyEl.value = row.order_by || '';
  2444. document.getElementById('form-remarks').value = row.remarks || '';
  2445. // Set vendor select (using ID or legacy fallback/custom)
  2446. const select = document.getElementById('form-vendor-select');
  2447. const vendorVal = String(row.vendor_name || '').trim();
  2448. select.value = '';
  2449. document.getElementById('form-vendor-custom').style.display = 'none';
  2450. document.getElementById('form-vendor-custom').required = false;
  2451. document.getElementById('form-vendor-custom').value = '';
  2452. if (vendorVal) {
  2453. let optionFound = false;
  2454. for (let option of select.options) {
  2455. if (option.value && option.value !== '__custom__' && String(option.value) === vendorVal) {
  2456. select.value = option.value;
  2457. optionFound = true;
  2458. break;
  2459. }
  2460. }
  2461. if (!optionFound) {
  2462. const matchedVendor = activeVendors.find(v => String(v.vendor_name).toLowerCase().trim() === vendorVal.toLowerCase());
  2463. if (matchedVendor) {
  2464. select.value = String(matchedVendor.id);
  2465. optionFound = true;
  2466. }
  2467. }
  2468. if (!optionFound) {
  2469. select.value = '__custom__';
  2470. const customInput = document.getElementById('form-vendor-custom');
  2471. customInput.style.display = 'block';
  2472. customInput.required = true;
  2473. customInput.value = row.vendor_name;
  2474. }
  2475. }
  2476. // Populate/Reset PDF elements
  2477. document.getElementById('form-pdf').value = '';
  2478. const existingPdfContainer = document.getElementById('form-existing-pdf-container');
  2479. const existingPdfLink = document.getElementById('form-existing-pdf-link');
  2480. if (row.file_path) {
  2481. existingPdfContainer.style.display = 'block';
  2482. let pdfUrl = row.file_path;
  2483. if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
  2484. pdfUrl = contextPath + pdfUrl.substring(1);
  2485. } else {
  2486. pdfUrl = contextPath + pdfUrl;
  2487. }
  2488. existingPdfLink.href = pdfUrl;
  2489. } else {
  2490. existingPdfContainer.style.display = 'none';
  2491. }
  2492. recordModalObj.show();
  2493. }
  2494. // SAVE Record API call
  2495. async function saveRecord(event) {
  2496. event.preventDefault();
  2497. showLoader(true, 'Saving record details...');
  2498. const id = document.getElementById('form-id').value;
  2499. const received_date = document.getElementById('form-received-date').value;
  2500. const type = document.getElementById('form-type').value;
  2501. const subject = document.getElementById('form-subject').value;
  2502. const amount = document.getElementById('form-amount').value;
  2503. const invoice_no = document.getElementById('form-invoice-no').value;
  2504. const receiptno = document.getElementById('form-receiptno').value;
  2505. const invoice_date = document.getElementById('form-invoice-date').value;
  2506. const physical_file_no = document.getElementById('form-physical-file-no').value;
  2507. const tatEl = document.getElementById('form-tat');
  2508. const tat = tatEl ? tatEl.value : '';
  2509. const status = document.getElementById('form-status').value;
  2510. // Preserve original values if they were loaded in edit mode
  2511. let originalRecord = null;
  2512. if (id) {
  2513. originalRecord = allRecords.find(r => String(r.id) === String(id));
  2514. }
  2515. const followupEl = document.getElementById('form-followup');
  2516. const followup = followupEl ? followupEl.value : (originalRecord ? originalRecord.followup : '');
  2517. const myfollowupEl = document.getElementById('form-myfollowup');
  2518. const myfollowup = myfollowupEl ? myfollowupEl.value : (originalRecord ? originalRecord.myfollowup : '');
  2519. const orderbyEl = document.getElementById('form-orderby');
  2520. const order_by = orderbyEl ? orderbyEl.value : (originalRecord ? originalRecord.order_by : '');
  2521. const remarks = document.getElementById('form-remarks').value;
  2522. // Resolve vendor
  2523. const vendorSelect = document.getElementById('form-vendor-select').value;
  2524. const vendorName = vendorSelect === '__custom__' ? document.getElementById('form-vendor-custom').value : vendorSelect;
  2525. if (!vendorName) {
  2526. alert('Please select or specify a vendor.');
  2527. showLoader(false);
  2528. return;
  2529. }
  2530. // Upload PDF if selected
  2531. let filePath = originalRecord ? originalRecord.file_path : null;
  2532. const pdfInput = document.getElementById('form-pdf');
  2533. if (pdfInput && pdfInput.files && pdfInput.files.length > 0) {
  2534. showLoader(true, 'Uploading PDF document...');
  2535. const formData = new FormData();
  2536. formData.append('file', pdfInput.files[0]);
  2537. const csrfHeaders = getCsrfHeaders();
  2538. try {
  2539. const uploadResponse = await fetch(contextPath + 'gccservicepaymenttracker/api/upload-pdf', {
  2540. method: 'POST',
  2541. headers: {
  2542. ...csrfHeaders
  2543. },
  2544. body: formData
  2545. });
  2546. const uploadResult = await uploadResponse.json();
  2547. if (uploadResult.success) {
  2548. filePath = uploadResult.filePath;
  2549. } else {
  2550. alert(uploadResult.error || 'Failed to upload PDF file.');
  2551. showLoader(false);
  2552. return;
  2553. }
  2554. } catch (err) {
  2555. console.error("Upload error:", err);
  2556. alert("Failed to upload PDF due to connection error.");
  2557. showLoader(false);
  2558. return;
  2559. }
  2560. }
  2561. const payload = {
  2562. id, received_date, type, vendor_name: vendorName, subject, amount, invoice_no,
  2563. receiptno, invoice_date, physical_file_no, tat, status, followup,
  2564. myfollowup, order_by, remarks, file_path: filePath
  2565. };
  2566. const csrfHeaders = getCsrfHeaders();
  2567. const headers = {
  2568. 'Content-Type': 'application/json',
  2569. ...csrfHeaders
  2570. };
  2571. try {
  2572. showLoader(true, 'Saving record details...');
  2573. const response = await fetch(contextPath + 'gccservicepaymenttracker/api/save', {
  2574. method: 'POST',
  2575. headers: headers,
  2576. body: JSON.stringify(payload)
  2577. });
  2578. const data = await response.json();
  2579. if (data.success) {
  2580. recordModalObj.hide();
  2581. loadCounts();
  2582. loadList();
  2583. } else {
  2584. alert(data.error || 'Failed to save record.');
  2585. showLoader(false);
  2586. }
  2587. } catch (err) {
  2588. console.error("Save Error:", err);
  2589. alert("Failed to save record due to connection error.");
  2590. showLoader(false);
  2591. }
  2592. }
  2593. // Calculate age of bill (days) dynamically from received date
  2594. function calculateAgeOfBill(receivedDateStr) {
  2595. if (!receivedDateStr) return '0';
  2596. try {
  2597. const recDate = new Date(receivedDateStr);
  2598. if (isNaN(recDate.getTime())) return '0';
  2599. recDate.setHours(0, 0, 0, 0);
  2600. const today = new Date();
  2601. today.setHours(0, 0, 0, 0);
  2602. const graceDate = new Date(recDate);
  2603. graceDate.setDate(graceDate.getDate());
  2604. if (today <= graceDate) {
  2605. return '0';
  2606. } else {
  2607. const diffTime = today.getTime() - graceDate.getTime();
  2608. const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
  2609. return String(diffDays);
  2610. }
  2611. } catch (e) {
  2612. console.error("Error calculating age of bill:", e);
  2613. return '0';
  2614. }
  2615. }
  2616. // --- NEW METRICS & INTERACTIVE CLICK FILTER LOGIC ---
  2617. function calculateSummaries() {
  2618. let total = allRecords.length;
  2619. // Dynamic Status Counts Map
  2620. const statusCounts = {};
  2621. activeStatus.forEach(s => {
  2622. statusCounts[s.id] = 0;
  2623. });
  2624. let age_1_15 = 0;
  2625. let age_16_30 = 0;
  2626. let age_31_60 = 0;
  2627. let age_61_90 = 0;
  2628. let age_91_120 = 0;
  2629. let age_over_120 = 0;
  2630. const vendorMap = {};
  2631. allRecords.forEach(row => {
  2632. const statusName = String(row.resolved_status || getStatusName(row.status) || '').trim();
  2633. const statusVal = String(row.status || '').trim();
  2634. // Find matching status in activeStatus (either by ID or legacy name)
  2635. const matchedStatus = activeStatus.find(s =>
  2636. String(s.id) === statusVal ||
  2637. s.status_name.toLowerCase().trim() === statusName.toLowerCase().trim()
  2638. );
  2639. const statusId = matchedStatus ? matchedStatus.id : null;
  2640. const isCompleted = matchedStatus && getStatusKey(matchedStatus.status_name) === 'completed';
  2641. const age = parseInt(calculateAgeOfBill(row.received_date), 10);
  2642. const vendorName = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
  2643. if (statusId !== null) {
  2644. statusCounts[statusId] = (statusCounts[statusId] || 0) + 1;
  2645. }
  2646. if (!isCompleted) {
  2647. if (age >= 1 && age <= 15) age_1_15++;
  2648. else if (age >= 16 && age <= 30) age_16_30++;
  2649. else if (age >= 31 && age <= 60) age_31_60++;
  2650. else if (age >= 61 && age <= 90) age_61_90++;
  2651. else if (age >= 91 && age <= 120) age_91_120++;
  2652. else if (age > 120) age_over_120++;
  2653. }
  2654. if (!vendorMap[vendorName]) {
  2655. vendorMap[vendorName] = {
  2656. vendorName: vendorName,
  2657. total: 0,
  2658. statusCounts: {}, // Map status ID to count
  2659. age_1_15: 0,
  2660. age_16_30: 0,
  2661. age_31_60: 0,
  2662. age_61_90: 0,
  2663. age_91_120: 0,
  2664. age_over_120: 0,
  2665. comp_age_1_15: 0,
  2666. comp_age_16_30: 0,
  2667. comp_age_31_60: 0,
  2668. comp_age_61_90: 0,
  2669. comp_age_91_120: 0,
  2670. comp_age_over_120: 0
  2671. };
  2672. activeStatus.forEach(s => {
  2673. vendorMap[vendorName].statusCounts[s.id] = 0;
  2674. });
  2675. }
  2676. const vData = vendorMap[vendorName];
  2677. vData.total++;
  2678. if (statusId !== null) {
  2679. vData.statusCounts[statusId] = (vData.statusCounts[statusId] || 0) + 1;
  2680. }
  2681. if (isCompleted) {
  2682. if (age >= 1 && age <= 15) vData.comp_age_1_15++;
  2683. else if (age >= 16 && age <= 30) vData.comp_age_16_30++;
  2684. else if (age >= 31 && age <= 60) vData.comp_age_31_60++;
  2685. else if (age >= 61 && age <= 90) vData.comp_age_61_90++;
  2686. else if (age >= 91 && age <= 120) vData.comp_age_91_120++;
  2687. else if (age > 120) vData.comp_age_over_120++;
  2688. } else {
  2689. if (age >= 1 && age <= 15) vData.age_1_15++;
  2690. else if (age >= 16 && age <= 30) vData.age_16_30++;
  2691. else if (age >= 31 && age <= 60) vData.age_31_60++;
  2692. else if (age >= 61 && age <= 90) vData.age_61_90++;
  2693. else if (age >= 91 && age <= 120) vData.age_91_120++;
  2694. else if (age > 120) vData.age_over_120++;
  2695. }
  2696. });
  2697. let completedTotal = 0;
  2698. const completedStatuses = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed');
  2699. completedStatuses.forEach(s => {
  2700. completedTotal += (statusCounts[s.id] || 0);
  2701. });
  2702. const pendingTotal = total - completedTotal;
  2703. let summaryBodyHtml = `
  2704. <td id="summary-total" class="cursor-pointer text-primary" style="background-color: #f8fafc;" onclick="openModalForCount('total', null, 'Total No of files')">${total}</td>
  2705. <td id="summary-completed" class="cursor-pointer text-success" onclick="openModalForCount('completed_custom', null, 'Completed Files')">${completedTotal}</td>
  2706. <td id="summary-pending" class="cursor-pointer text-danger" style="color: #991b1b; font-weight: bold;" onclick="openModalForCount('pending_custom', null, 'Pending Files')">${pendingTotal}</td>
  2707. `;
  2708. summaryBodyHtml += `
  2709. <td id="summary-1-15" class="cursor-pointer" onclick="openModalForCount('age', [1, 15], 'Age: 1 to 15 Days')">${age_1_15}</td>
  2710. <td id="summary-16-30" class="cursor-pointer" onclick="openModalForCount('age', [16, 30], 'Age: 16 to 30 Days')">${age_16_30}</td>
  2711. <td id="summary-31-60" class="cursor-pointer" onclick="openModalForCount('age', [31, 60], 'Age: 31 to 60 Days')">${age_31_60}</td>
  2712. <td id="summary-61-90" class="cursor-pointer" onclick="openModalForCount('age', [61, 90], 'Age: 61 to 90 Days')">${age_61_90}</td>
  2713. <td id="summary-91-120" class="cursor-pointer" onclick="openModalForCount('age', [91, 120], 'Age: 91 to 120 Days')">${age_91_120}</td>
  2714. <td id="summary-over-120" class="cursor-pointer text-danger" onclick="openModalForCount('age', [121, null], 'Age: Over 120 Days')">${age_over_120}</td>
  2715. `;
  2716. const summaryBbody = document.getElementById('summary-tbody-tr');
  2717. if (summaryBbody) {
  2718. summaryBbody.innerHTML = summaryBodyHtml;
  2719. }
  2720. // Render status pie chart dynamically
  2721. const chartLabels = activeStatus.map(s => s.status_name);
  2722. const chartData = activeStatus.map(s => statusCounts[s.id] || 0);
  2723. const chartColors = activeStatus.map(s => {
  2724. const key = getStatusKey(s.status_name);
  2725. if (key === 'completed') return '#2ecc71';
  2726. if (key === 'awaiting details') return '#f1c40f';
  2727. if (key === 'pending with dept') return '#3498db';
  2728. if (key === 'pending') return '#e74c3c';
  2729. if (key === 'no action') return '#95a5a6';
  2730. return '#7f8c8d'; // Fallback
  2731. });
  2732. renderStatusChart(chartLabels, chartData, chartColors);
  2733. vendorBreakdownData = Object.values(vendorMap).sort((a, b) => b.total - a.total);
  2734. renderVendorBreakdown();
  2735. }
  2736. function renderStatusChart(labels, data, colors) {
  2737. const ctx = document.getElementById('statusPieChart')?.getContext('2d');
  2738. if (!ctx) return;
  2739. if (statusChartInstance) {
  2740. statusChartInstance.destroy();
  2741. }
  2742. statusChartInstance = new Chart(ctx, {
  2743. type: 'pie',
  2744. data: {
  2745. labels: labels,
  2746. datasets: [{
  2747. data: data,
  2748. backgroundColor: colors,
  2749. borderWidth: 1,
  2750. borderColor: '#ffffff'
  2751. }]
  2752. },
  2753. options: {
  2754. responsive: true,
  2755. maintainAspectRatio: false,
  2756. plugins: {
  2757. legend: {
  2758. position: 'right',
  2759. labels: {
  2760. boxWidth: 12,
  2761. font: {
  2762. size: 10,
  2763. family: 'Poppins'
  2764. }
  2765. }
  2766. }
  2767. }
  2768. }
  2769. });
  2770. }
  2771. function renderVendorBreakdown() {
  2772. const tbody = document.getElementById('vendor-breakdown-tbody');
  2773. if (!tbody) return;
  2774. tbody.innerHTML = '';
  2775. const searchVal = document.getElementById('vendor-breakdown-search')?.value || '';
  2776. const query = searchVal.toLowerCase().trim();
  2777. const filtered = vendorBreakdownData.filter(v => v.vendorName.toLowerCase().includes(query));
  2778. const completedStatuses = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed');
  2779. const nonCompletedStatuses = activeStatus.filter(s => getStatusKey(s.status_name) !== 'completed');
  2780. if (filtered.length === 0) {
  2781. const colSpan = 2 + activeStatus.length + 1 + 6;
  2782. tbody.innerHTML = `<tr><td colspan="${colSpan}" class="text-secondary py-3">No matching vendors found</td></tr>`;
  2783. return;
  2784. }
  2785. // Keep track of column sums
  2786. let sumTotal = 0;
  2787. const sumStatuses = {};
  2788. activeStatus.forEach(s => {
  2789. sumStatuses[s.id] = 0;
  2790. });
  2791. let sumAge1_15 = 0;
  2792. let sumAge16_30 = 0;
  2793. let sumAge31_60 = 0;
  2794. let sumAge61_90 = 0;
  2795. let sumAge91_120 = 0;
  2796. let sumAgeOver120 = 0;
  2797. filtered.forEach(v => {
  2798. sumTotal += v.total;
  2799. activeStatus.forEach(s => {
  2800. sumStatuses[s.id] += (v.statusCounts[s.id] || 0);
  2801. });
  2802. sumAge1_15 += v.age_1_15;
  2803. sumAge16_30 += v.age_16_30;
  2804. sumAge31_60 += v.age_31_60;
  2805. sumAge61_90 += v.age_61_90;
  2806. sumAge91_120 += v.age_91_120;
  2807. sumAgeOver120 += v.age_over_120;
  2808. const tr = document.createElement('tr');
  2809. // Construct HTML row dynamically
  2810. let trHtml = `
  2811. <td class="fw-semibold text-start text-dark" style="max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(v.vendorName)}</td>
  2812. <td class="cursor-pointer text-primary fw-bold" onclick="openModalForCount('vendor_total', null, 'Vendor: ' + this.getAttribute('data-vendor'), this.getAttribute('data-vendor'))" data-vendor="${escapeHtml(v.vendorName)}">${v.total}</td>
  2813. `;
  2814. // 1. Completed statuses
  2815. let totalCompleted = 0;
  2816. completedStatuses.forEach(s => {
  2817. const count = v.statusCounts[s.id] || 0;
  2818. totalCompleted += count;
  2819. const style = getStatusStyle(s.status_name);
  2820. trHtml += `<td class="cursor-pointer ${style.badgeClass}" onclick="openModalForCount('vendor_status', '${s.id}', 'Vendor: ' + this.getAttribute('data-vendor') + ' (${escapeHtml(s.status_name)})', this.getAttribute('data-vendor'), '${s.id}')" data-vendor="${escapeHtml(v.vendorName)}">${count}</td>`;
  2821. });
  2822. // 2. Custom "Pending" column
  2823. const pendingCount = v.total - totalCompleted;
  2824. trHtml += `<td class="cursor-pointer text-danger fw-bold" onclick="openModalForCount('vendor_pending_custom', null, 'Vendor: ' + this.getAttribute('data-vendor') + ' (Pending)', this.getAttribute('data-vendor'))" data-vendor="${escapeHtml(v.vendorName)}">${pendingCount}</td>`;
  2825. // 3. Non-completed statuses
  2826. nonCompletedStatuses.forEach(s => {
  2827. const count = v.statusCounts[s.id] || 0;
  2828. const style = getStatusStyle(s.status_name);
  2829. trHtml += `<td class="cursor-pointer ${style.badgeClass}" onclick="openModalForCount('vendor_status', '${s.id}', 'Vendor: ' + this.getAttribute('data-vendor') + ' (${escapeHtml(s.status_name)})', this.getAttribute('data-vendor'), '${s.id}')" data-vendor="${escapeHtml(v.vendorName)}">${count}</td>`;
  2830. });
  2831. trHtml += `
  2832. <td class="cursor-pointer" onclick="openModalForCount('vendor_age', [1, 15], 'Vendor: ' + this.getAttribute('data-vendor') + ' (1-15 Days)', this.getAttribute('data-vendor'), 'age_1_15')" data-vendor="${escapeHtml(v.vendorName)}">${v.age_1_15}</td>
  2833. <td class="cursor-pointer" onclick="openModalForCount('vendor_age', [16, 30], 'Vendor: ' + this.getAttribute('data-vendor') + ' (16-30 Days)', this.getAttribute('data-vendor'), 'age_16_30')" data-vendor="${escapeHtml(v.vendorName)}">${v.age_16_30}</td>
  2834. <td class="cursor-pointer" onclick="openModalForCount('vendor_age', [31, 60], 'Vendor: ' + this.getAttribute('data-vendor') + ' (31-60 Days)', this.getAttribute('data-vendor'), 'age_31_60')" data-vendor="${escapeHtml(v.vendorName)}">${v.age_31_60}</td>
  2835. <td class="cursor-pointer" onclick="openModalForCount('vendor_age', [61, 90], 'Vendor: ' + this.getAttribute('data-vendor') + ' (61-90 Days)', this.getAttribute('data-vendor'), 'age_61_90')" data-vendor="${escapeHtml(v.vendorName)}">${v.age_61_90}</td>
  2836. <td class="cursor-pointer" onclick="openModalForCount('vendor_age', [91, 120], 'Vendor: ' + this.getAttribute('data-vendor') + ' (91-120 Days)', this.getAttribute('data-vendor'), 'age_91_120')" data-vendor="${escapeHtml(v.vendorName)}">${v.age_91_120}</td>
  2837. <td class="cursor-pointer text-danger" onclick="openModalForCount('vendor_age', [121, null], 'Vendor: ' + this.getAttribute('data-vendor') + ' (Over 120 Days)', this.getAttribute('data-vendor'), 'age_over_120')" data-vendor="${escapeHtml(v.vendorName)}">${v.age_over_120}</td>
  2838. `;
  2839. tr.innerHTML = trHtml;
  2840. tbody.appendChild(tr);
  2841. });
  2842. // Append TOTAL row at the bottom
  2843. const totalTr = document.createElement('tr');
  2844. totalTr.className = 'table-warning fw-bold text-dark';
  2845. totalTr.style.borderTop = '2px solid #cbd5e1';
  2846. let totalTrHtml = `
  2847. <td class="text-start">TOTAL</td>
  2848. <td class="cursor-pointer text-primary fw-bold" onclick="openModalForCount('total', null, 'Total No of Files')">${sumTotal}</td>
  2849. `;
  2850. // 1. Completed statuses
  2851. let sumCompleted = 0;
  2852. completedStatuses.forEach(s => {
  2853. const style = getStatusStyle(s.status_name);
  2854. sumCompleted += sumStatuses[s.id];
  2855. totalTrHtml += `<td class="cursor-pointer ${style.badgeClass}" onclick="openModalForCount('status', '${s.id}', 'Status: ' + this.getAttribute('data-status-name'))" data-status-name="${escapeHtml(s.status_name)}">${sumStatuses[s.id]}</td>`;
  2856. });
  2857. // 2. Custom "Pending" column
  2858. const sumPending = sumTotal - sumCompleted;
  2859. totalTrHtml += `<td class="cursor-pointer text-danger fw-bold" onclick="openModalForCount('pending_custom', null, 'Pending Files')">${sumPending}</td>`;
  2860. // 3. Non-completed statuses
  2861. nonCompletedStatuses.forEach(s => {
  2862. const style = getStatusStyle(s.status_name);
  2863. totalTrHtml += `<td class="cursor-pointer ${style.badgeClass}" onclick="openModalForCount('status', '${s.id}', 'Status: ' + this.getAttribute('data-status-name'))" data-status-name="${escapeHtml(s.status_name)}">${sumStatuses[s.id]}</td>`;
  2864. });
  2865. totalTrHtml += `
  2866. <td class="cursor-pointer" onclick="openModalForCount('age', [1, 15], 'Age: 1-15 Days')">${sumAge1_15}</td>
  2867. <td class="cursor-pointer" onclick="openModalForCount('age', [16, 30], 'Age: 16-30 Days')">${sumAge16_30}</td>
  2868. <td class="cursor-pointer" onclick="openModalForCount('age', [31, 60], 'Age: 31-60 Days')">${sumAge31_60}</td>
  2869. <td class="cursor-pointer" onclick="openModalForCount('age', [61, 90], 'Age: 61-90 Days')">${sumAge61_90}</td>
  2870. <td class="cursor-pointer" onclick="openModalForCount('age', [91, 120], 'Age: 91-120 Days')">${sumAge91_120}</td>
  2871. <td class="cursor-pointer text-danger" onclick="openModalForCount('age', [121, null], 'Age: Over 120 Days')">${sumAgeOver120}</td>
  2872. `;
  2873. totalTr.innerHTML = totalTrHtml;
  2874. tbody.appendChild(totalTr);
  2875. }
  2876. function filterVendorBreakdown(val) {
  2877. renderVendorBreakdown();
  2878. }
  2879. function setClickFilter(type, value, displayName, vendorName = null, col = null) {
  2880. activeClickFilter = { type, value, displayName, vendorName, col };
  2881. const badge = document.getElementById('click-filter-badge');
  2882. if (badge) {
  2883. badge.innerHTML = `Filter: ${displayName} <i class="fa-solid fa-circle-xmark ms-1 cursor-pointer" onclick="clearClickFilter(event)"></i>`;
  2884. badge.style.display = 'inline-block';
  2885. badge.style.cursor = 'pointer';
  2886. }
  2887. currentPage = 1;
  2888. renderTable();
  2889. // Smoothly scroll down to the table card
  2890. document.querySelector('.table-card')?.scrollIntoView({ behavior: 'smooth' });
  2891. }
  2892. function clearClickFilter(event) {
  2893. if (event) event.stopPropagation();
  2894. activeClickFilter = null;
  2895. const badge = document.getElementById('click-filter-badge');
  2896. if (badge) {
  2897. badge.style.display = 'none';
  2898. }
  2899. currentPage = 1;
  2900. renderTable();
  2901. }
  2902. // --- COUNT DETAILS MODAL LOGIC ---
  2903. let modalClickFilter = null; // Stores { clickType, clickValue, displayName, vendorName, col }
  2904. let countDetailsModalObj = null;
  2905. function openModalForCount(clickType, clickValue, displayName, vendorName = null, col = null) {
  2906. modalClickFilter = { clickType, clickValue, displayName, vendorName, col };
  2907. // Set modal title
  2908. document.getElementById('countDetailsModalLabel').innerHTML = `<i class="fa-solid fa-list-check me-2"></i>Count Details: ${escapeHtml(displayName)}`;
  2909. // Initialize modal dropdown options
  2910. populateModalDropdowns();
  2911. // Set default selected values in modal dropdowns based on click filters
  2912. const modalTypeSelect = document.getElementById('modal-filter-type');
  2913. const modalStatusSelect = document.getElementById('modal-filter-status');
  2914. const modalVendorSelect = document.getElementById('modal-filter-vendor');
  2915. // Reset to default
  2916. modalTypeSelect.value = 'all';
  2917. modalStatusSelect.value = 'all';
  2918. modalVendorSelect.value = 'all';
  2919. const modalSearchInput = document.getElementById('modal-table-search');
  2920. if (modalSearchInput) modalSearchInput.value = '';
  2921. // If click was category/type-based
  2922. if (clickType === 'category' && clickValue) {
  2923. modalTypeSelect.value = clickValue;
  2924. }
  2925. // If click was status-based
  2926. if (clickType === 'status' && clickValue) {
  2927. modalStatusSelect.value = clickValue;
  2928. } else if (clickType === 'vendor_status' && col) {
  2929. modalStatusSelect.value = col;
  2930. }
  2931. // If click was vendor-based
  2932. if (vendorName) {
  2933. // Find matching option or set value
  2934. for (let opt of modalVendorSelect.options) {
  2935. if (opt.textContent.trim().toLowerCase() === vendorName.trim().toLowerCase() || opt.value === vendorName) {
  2936. modalVendorSelect.value = opt.value;
  2937. break;
  2938. }
  2939. }
  2940. }
  2941. // Render the modal table
  2942. applyModalFilters();
  2943. // Show modal
  2944. if (!countDetailsModalObj) {
  2945. countDetailsModalObj = new bootstrap.Modal(document.getElementById('countDetailsModal'));
  2946. }
  2947. countDetailsModalObj.show();
  2948. }
  2949. function populateModalDropdowns() {
  2950. const modalTypeSelect = document.getElementById('modal-filter-type');
  2951. const modalStatusSelect = document.getElementById('modal-filter-status');
  2952. const modalVendorSelect = document.getElementById('modal-filter-vendor');
  2953. if (modalTypeSelect) {
  2954. modalTypeSelect.innerHTML = '<option value="all">- All Types -</option>';
  2955. const optNotUpdated = document.createElement('option');
  2956. optNotUpdated.value = 'status_not_updated';
  2957. optNotUpdated.textContent = 'Status Not Updated';
  2958. modalTypeSelect.appendChild(optNotUpdated);
  2959. activeTypes.forEach(t => {
  2960. const opt = document.createElement('option');
  2961. opt.value = getCategoryCode(t.type_name);
  2962. opt.textContent = t.type_name;
  2963. modalTypeSelect.appendChild(opt);
  2964. });
  2965. }
  2966. if (modalStatusSelect) {
  2967. modalStatusSelect.innerHTML = '<option value="all">- All Statuses -</option>';
  2968. activeStatus.forEach(s => {
  2969. const opt = document.createElement('option');
  2970. opt.value = s.id;
  2971. opt.textContent = s.status_name;
  2972. modalStatusSelect.appendChild(opt);
  2973. });
  2974. }
  2975. if (modalVendorSelect) {
  2976. modalVendorSelect.innerHTML = '<option value="all">- All Vendors -</option>';
  2977. activeVendors.forEach(v => {
  2978. const opt = document.createElement('option');
  2979. opt.value = String(v.vendor_name).trim(); // Match by vendor_name text
  2980. opt.textContent = v.vendor_name;
  2981. modalVendorSelect.appendChild(opt);
  2982. });
  2983. }
  2984. }
  2985. function applyModalFilters() {
  2986. if (!modalClickFilter) return;
  2987. const selectedType = document.getElementById('modal-filter-type').value;
  2988. const selectedStatus = document.getElementById('modal-filter-status').value;
  2989. const selectedVendor = document.getElementById('modal-filter-vendor').value;
  2990. const searchVal = document.getElementById('modal-table-search')?.value || '';
  2991. const query = searchVal.toLowerCase().trim();
  2992. const { clickType, clickValue, vendorName, col } = modalClickFilter;
  2993. let filtered = allRecords.filter(row => {
  2994. // 1. Apply the original click filters from the cell / card
  2995. let age = parseInt(calculateAgeOfBill(row.received_date), 10);
  2996. if (clickType === 'category') {
  2997. if (clickValue === 'status_not_updated') {
  2998. const rowType = String(row.type || '').trim();
  2999. if (rowType !== '') return false;
  3000. } else {
  3001. const rowCategory = getCategoryCode(row.resolved_type || row.type);
  3002. if (rowCategory !== clickValue) return false;
  3003. }
  3004. }
  3005. if (clickType === 'age') {
  3006. // Exclude completed files for pending age column clicks
  3007. const rowStatusStr = String(row.status || '').trim();
  3008. const isCompleted = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed').some(s =>
  3009. rowStatusStr === String(s.id) ||
  3010. rowStatusStr.toLowerCase() === s.status_name.toLowerCase().trim()
  3011. );
  3012. if (isCompleted) return false;
  3013. const [min, max] = clickValue;
  3014. if (max !== null) {
  3015. if (age < min || age > max) return false;
  3016. } else {
  3017. if (age < min) return false;
  3018. }
  3019. }
  3020. if (clickType === 'completed_custom') {
  3021. // Filter for completed status
  3022. const rowStatusStr = String(row.status || '').trim();
  3023. const isCompleted = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed').some(s =>
  3024. rowStatusStr === String(s.id) ||
  3025. rowStatusStr.toLowerCase() === s.status_name.toLowerCase().trim()
  3026. );
  3027. if (!isCompleted) return false;
  3028. }
  3029. if (clickType === 'completed_age') {
  3030. // Filter for completed status
  3031. const rowStatusStr = String(row.status || '').trim();
  3032. const isCompleted = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed').some(s =>
  3033. rowStatusStr === String(s.id) ||
  3034. rowStatusStr.toLowerCase() === s.status_name.toLowerCase().trim()
  3035. );
  3036. if (!isCompleted) return false;
  3037. const [min, max] = clickValue;
  3038. if (max !== null) {
  3039. if (age < min || age > max) return false;
  3040. } else {
  3041. if (age < min) return false;
  3042. }
  3043. }
  3044. if (clickType === 'status') {
  3045. const rowStatusStr = String(row.status || '').trim();
  3046. const matched = activeStatus.find(s => String(s.id) === clickValue);
  3047. if (matched) {
  3048. const matches = rowStatusStr === String(matched.id) ||
  3049. rowStatusStr.toLowerCase() === matched.status_name.toLowerCase().trim();
  3050. if (!matches) return false;
  3051. }
  3052. }
  3053. if (clickType === 'vendor_pending_custom') {
  3054. const rowVendor = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
  3055. if (rowVendor !== vendorName) return false;
  3056. const rowStatusStr = String(row.status || '').trim();
  3057. const isCompleted = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed').some(s =>
  3058. rowStatusStr === String(s.id) ||
  3059. rowStatusStr.toLowerCase() === s.status_name.toLowerCase().trim()
  3060. );
  3061. if (isCompleted) return false;
  3062. }
  3063. if (clickType === 'pending_custom') {
  3064. const rowStatusStr = String(row.status || '').trim();
  3065. const isCompleted = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed').some(s =>
  3066. rowStatusStr === String(s.id) ||
  3067. rowStatusStr.toLowerCase() === s.status_name.toLowerCase().trim()
  3068. );
  3069. if (isCompleted) return false;
  3070. }
  3071. if (clickType === 'vendor_total') {
  3072. const rowVendor = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
  3073. if (rowVendor !== vendorName) return false;
  3074. }
  3075. if (clickType === 'vendor_status') {
  3076. const rowVendor = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
  3077. if (rowVendor !== vendorName) return false;
  3078. const rowStatusStr = String(row.status || '').trim();
  3079. const matched = activeStatus.find(s => String(s.id) === col);
  3080. if (matched) {
  3081. const matches = rowStatusStr === String(matched.id) ||
  3082. rowStatusStr.toLowerCase() === matched.status_name.toLowerCase().trim();
  3083. if (!matches) return false;
  3084. }
  3085. }
  3086. if (clickType === 'vendor_age') {
  3087. const rowVendor = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
  3088. if (rowVendor !== vendorName) return false;
  3089. // Exclude completed files for pending vendor age clicks
  3090. const rowStatusStr = String(row.status || '').trim();
  3091. const isCompleted = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed').some(s =>
  3092. rowStatusStr === String(s.id) ||
  3093. rowStatusStr.toLowerCase() === s.status_name.toLowerCase().trim()
  3094. );
  3095. if (isCompleted) return false;
  3096. const [min, max] = clickValue;
  3097. if (max !== null) {
  3098. if (age < min || age > max) return false;
  3099. } else {
  3100. if (age < min) return false;
  3101. }
  3102. }
  3103. if (clickType === 'vendor_completed_age') {
  3104. const rowVendor = String(row.resolved_vendor || row.vendor_name || 'Unknown').trim();
  3105. if (rowVendor !== vendorName) return false;
  3106. // Filter for completed status
  3107. const rowStatusStr = String(row.status || '').trim();
  3108. const isCompleted = activeStatus.filter(s => getStatusKey(s.status_name) === 'completed').some(s =>
  3109. rowStatusStr === String(s.id) ||
  3110. rowStatusStr.toLowerCase() === s.status_name.toLowerCase().trim()
  3111. );
  3112. if (!isCompleted) return false;
  3113. const [min, max] = clickValue;
  3114. if (max !== null) {
  3115. if (age < min || age > max) return false;
  3116. } else {
  3117. if (age < min) return false;
  3118. }
  3119. }
  3120. if (clickType === 'latest_updates') {
  3121. if (row.updated_date !== clickValue) return false;
  3122. }
  3123. // 2. Apply the modal dropdown filters
  3124. if (selectedType !== 'all') {
  3125. if (selectedType === 'status_not_updated') {
  3126. const rowType = String(row.type || '').trim();
  3127. if (rowType !== '') return false;
  3128. } else {
  3129. const rowCategory = getCategoryCode(row.resolved_type || row.type);
  3130. if (rowCategory !== selectedType) return false;
  3131. }
  3132. }
  3133. if (selectedStatus !== 'all') {
  3134. const rowStatusStr = String(row.status || '').trim();
  3135. const matched = activeStatus.find(s => String(s.id) === selectedStatus);
  3136. if (matched) {
  3137. const matches = rowStatusStr === String(matched.id) ||
  3138. rowStatusStr.toLowerCase() === matched.status_name.toLowerCase().trim();
  3139. if (!matches) return false;
  3140. }
  3141. }
  3142. if (selectedVendor !== 'all') {
  3143. const rowVendor = String(row.resolved_vendor || row.vendor_name || '').trim();
  3144. if (rowVendor.toLowerCase() !== selectedVendor.toLowerCase()) return false;
  3145. }
  3146. // 3. Apply the modal search keyword
  3147. if (query) {
  3148. const vName = String(row.resolved_vendor || row.vendor_name || '').toLowerCase();
  3149. const tName = String(row.resolved_type || row.type || '').toLowerCase();
  3150. const statusName = String(row.resolved_status || getStatusName(row.status) || '').toLowerCase();
  3151. const matchesSearch = (
  3152. vName.includes(query) ||
  3153. tName.includes(query) ||
  3154. (row.subject && String(row.subject).toLowerCase().includes(query)) ||
  3155. (row.amount && String(row.amount).toLowerCase().includes(query)) ||
  3156. (row.invoice_no && String(row.invoice_no).toLowerCase().includes(query)) ||
  3157. (row.receiptno && String(row.receiptno).toLowerCase().includes(query)) ||
  3158. (row.physical_file_no && String(row.physical_file_no).toLowerCase().includes(query)) ||
  3159. (row.remarks && String(row.remarks).toLowerCase().includes(query)) ||
  3160. (row.tat && String(row.tat).toLowerCase().includes(query)) ||
  3161. statusName.includes(query)
  3162. );
  3163. if (!matchesSearch) return false;
  3164. }
  3165. return true;
  3166. });
  3167. renderModalTable(filtered);
  3168. }
  3169. function renderModalTable(data) {
  3170. const tbody = document.getElementById('modal-tracking-tbody');
  3171. const noRecords = document.getElementById('modal-no-records-found');
  3172. tbody.innerHTML = '';
  3173. if (data.length === 0) {
  3174. noRecords.style.display = 'block';
  3175. return;
  3176. }
  3177. noRecords.style.display = 'none';
  3178. const text = val => (val === null || val === undefined || String(val).trim() === '') ? '-' : val;
  3179. const formatDate = dateStr => {
  3180. if (!dateStr) return '-';
  3181. const parts = dateStr.split('-');
  3182. if (parts.length === 3) {
  3183. return `${parts[2]}/${parts[1]}/${parts[0]}`;
  3184. }
  3185. return dateStr;
  3186. };
  3187. const formatDateTime = dateTimeStr => {
  3188. if (!dateTimeStr) return '-';
  3189. try {
  3190. const d = new Date(dateTimeStr);
  3191. if (isNaN(d.getTime())) return dateTimeStr;
  3192. const pad = n => String(n).padStart(2, '0');
  3193. return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
  3194. } catch (e) {
  3195. return dateTimeStr;
  3196. }
  3197. };
  3198. const getTypeBadge = (typeVal, filePath) => {
  3199. const cleanType = String(typeVal || '').trim().toLowerCase();
  3200. let label = cleanType;
  3201. let badgeClass = 'type-sd';
  3202. if (cleanType === 'asd' || cleanType.includes('asd') || cleanType.includes('additional security')) {
  3203. label = 'ASD';
  3204. badgeClass = 'type-asd';
  3205. } else if (cleanType.includes('agreement')) {
  3206. label = 'Agreement';
  3207. badgeClass = 'type-agreement';
  3208. } else if (cleanType === 'sd' || (cleanType.includes('security') && !cleanType.includes('additional') && !cleanType.includes('asd'))) {
  3209. label = 'Security Deposit';
  3210. badgeClass = 'type-sd';
  3211. } else if (cleanType.includes('invoice') || cleanType.includes('bill')) {
  3212. label = 'Tax Invoice';
  3213. badgeClass = 'type-tax-invoice';
  3214. } else if (cleanType === 'emd' || cleanType.includes('earnest')) {
  3215. label = 'EMD';
  3216. badgeClass = 'type-emd';
  3217. } else if (cleanType.includes('guarantee') || cleanType === 'bg') {
  3218. label = 'Bank Guarantee';
  3219. badgeClass = 'type-bank-guarantee';
  3220. } else if (cleanType.includes('chellan') || cleanType.includes('challan') || cleanType === 'dc') {
  3221. label = 'Delivery Chellan';
  3222. badgeClass = 'type-delivery-chellan';
  3223. } else {
  3224. const typeObj = activeTypes.find(t => String(t.id) === cleanType);
  3225. if (typeObj) {
  3226. return getTypeBadge(typeObj.type_name, filePath);
  3227. }
  3228. label = typeVal || '-';
  3229. badgeClass = 'status-default';
  3230. }
  3231. let onClickAttr = '';
  3232. if (filePath) {
  3233. let pdfUrl = filePath;
  3234. if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
  3235. pdfUrl = contextPath + pdfUrl.substring(1);
  3236. } else {
  3237. pdfUrl = contextPath + pdfUrl;
  3238. }
  3239. onClickAttr = `onclick="window.open('${escapeHtml(pdfUrl)}', '_blank')"`;
  3240. } else {
  3241. onClickAttr = `onclick="alert('pdf file not uploaded')"`;
  3242. }
  3243. return `<span class="type-badge ${badgeClass} cursor-pointer" ${onClickAttr} style="cursor: pointer;" title="${filePath ? 'Click to view PDF' : 'PDF not uploaded'}">${label}</span>`;
  3244. };
  3245. data.forEach((row, index) => {
  3246. const tr = document.createElement('tr');
  3247. const tdIndex = document.createElement('td');
  3248. tdIndex.className = 'text-center fw-semibold text-secondary';
  3249. tdIndex.textContent = index + 1;
  3250. tr.appendChild(tdIndex);
  3251. const dispStatus = String(row.resolved_status || getStatusName(row.status) || '').trim();
  3252. tr.innerHTML += `
  3253. <td>${formatDate(row.received_date)}</td>
  3254. <td>${formatDate(row.invoice_date)}</td>
  3255. <td>${text(row.physical_file_no)}</td>
  3256. <td>${text(row.receiptno)}</td>
  3257. <td>${text(row.subject)}</td>
  3258. <td>${getTypeBadge(row.resolved_type || row.type, row.file_path)}</td>
  3259. <td class="fw-semibold text-secondary">${text(row.resolved_vendor || row.vendor_name)}</td>
  3260. <td class="fw-semibold text-dark">&#8377; ${text(row.amount)}</td>
  3261. <td>${calculateAgeOfBill(row.received_date)}</td>
  3262. <td><span class="status-badge status-${dispStatus.toLowerCase().replace(/\s+/g, '-')}">${text(dispStatus)}</span></td>
  3263. <td><small class="text-muted">${text(row.remarks)}</small></td>
  3264. <td>${formatDateTime(row.updated_date)}</td>
  3265. <td class="fw-semibold text-secondary">${text(row.updated_by_name)}</td>
  3266. `;
  3267. // Actions cell
  3268. const tdActions = document.createElement('td');
  3269. tdActions.className = 'text-center';
  3270. tdActions.style.whiteSpace = 'nowrap'; // avoid wrapping action buttons
  3271. if (row.file_path) {
  3272. const btnViewPdf = document.createElement('button');
  3273. btnViewPdf.className = 'btn-grid-action btn-grid-pdf me-1';
  3274. btnViewPdf.innerHTML = '<i class="fa-solid fa-file-pdf"></i>';
  3275. btnViewPdf.title = 'View PDF Document';
  3276. btnViewPdf.onclick = () => {
  3277. let pdfUrl = row.file_path;
  3278. if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
  3279. pdfUrl = contextPath + pdfUrl.substring(1);
  3280. } else {
  3281. pdfUrl = contextPath + pdfUrl;
  3282. }
  3283. window.open(pdfUrl, '_blank');
  3284. };
  3285. tdActions.appendChild(btnViewPdf);
  3286. }
  3287. // History Button
  3288. const btnHistory = document.createElement('button');
  3289. btnHistory.className = 'btn-grid-action btn-outline-secondary me-1';
  3290. btnHistory.innerHTML = '<i class="fa-solid fa-clock-rotate-left"></i>';
  3291. btnHistory.style.border = '1px solid #cbd5e1';
  3292. btnHistory.title = 'View Update History';
  3293. btnHistory.onclick = () => {
  3294. showHistoryModal(row.id);
  3295. };
  3296. tdActions.appendChild(btnHistory);
  3297. if (isAdmin) {
  3298. const btnEdit = document.createElement('button');
  3299. btnEdit.className = 'btn-grid-action btn-grid-edit me-1';
  3300. btnEdit.innerHTML = '<i class="fa-solid fa-pen-to-square"></i>';
  3301. btnEdit.title = 'Edit Record';
  3302. btnEdit.onclick = () => {
  3303. openEditModal(row);
  3304. };
  3305. const btnDelete = document.createElement('button');
  3306. btnDelete.className = 'btn-grid-action btn-grid-delete';
  3307. btnDelete.innerHTML = '<i class="fa-solid fa-trash-can"></i>';
  3308. btnDelete.title = 'Delete Record';
  3309. btnDelete.onclick = () => {
  3310. deleteRecord(row.id);
  3311. };
  3312. tdActions.appendChild(btnEdit);
  3313. tdActions.appendChild(btnDelete);
  3314. }
  3315. tr.appendChild(tdActions);
  3316. tbody.appendChild(tr);
  3317. });
  3318. }
  3319. // --- EXPORT TO EXCEL LOGIC ---
  3320. function exportTableToExcelHelper(tableId, filename) {
  3321. const originalTable = document.getElementById(tableId);
  3322. if (!originalTable) {
  3323. alert("Table element not found.");
  3324. return;
  3325. }
  3326. // Clone table to modify it and strip actions column
  3327. const clonedTable = originalTable.cloneNode(true);
  3328. // Find Actions column index
  3329. const headers = clonedTable.querySelectorAll('thead th');
  3330. let actionsColIndex = -1;
  3331. headers.forEach((th, index) => {
  3332. const text = th.textContent.trim().toLowerCase();
  3333. if (text === 'actions' || text === 'action') {
  3334. actionsColIndex = index;
  3335. th.remove();
  3336. }
  3337. });
  3338. if (actionsColIndex !== -1) {
  3339. const rows = clonedTable.querySelectorAll('tbody tr');
  3340. rows.forEach(row => {
  3341. const cells = row.querySelectorAll('td');
  3342. if (cells[actionsColIndex]) {
  3343. cells[actionsColIndex].remove();
  3344. }
  3345. });
  3346. }
  3347. // Find Type of File column index to append PDF Status
  3348. let typeColIndex = -1;
  3349. clonedTable.querySelectorAll('thead th').forEach((th, index) => {
  3350. if (th.textContent.trim().toLowerCase() === 'type of file') {
  3351. typeColIndex = index;
  3352. }
  3353. });
  3354. if (typeColIndex !== -1) {
  3355. // Insert header "PDF Status" right after "Type of File"
  3356. const th = document.createElement('th');
  3357. th.textContent = 'PDF Status';
  3358. th.style.backgroundColor = '#f0fdf4';
  3359. th.style.color = '#166534';
  3360. const theadTr = clonedTable.querySelector('thead tr');
  3361. const nextTh = theadTr.children[typeColIndex + 1];
  3362. if (nextTh) {
  3363. theadTr.insertBefore(th, nextTh);
  3364. } else {
  3365. theadTr.appendChild(th);
  3366. }
  3367. // Insert td for each row
  3368. clonedTable.querySelectorAll('tbody tr').forEach(row => {
  3369. const cells = row.querySelectorAll('td');
  3370. const typeCell = cells[typeColIndex];
  3371. let pdfStatusText = 'pdf file not uploaded';
  3372. if (typeCell) {
  3373. const span = typeCell.querySelector('span');
  3374. if (span && span.getAttribute('title') && span.getAttribute('title').toLowerCase().includes('click')) {
  3375. pdfStatusText = 'PDF Uploaded';
  3376. }
  3377. }
  3378. const td = document.createElement('td');
  3379. td.textContent = pdfStatusText;
  3380. const nextTd = row.children[typeColIndex + 1];
  3381. if (nextTd) {
  3382. row.insertBefore(td, nextTd);
  3383. } else {
  3384. row.appendChild(td);
  3385. }
  3386. });
  3387. }
  3388. // Convert to Excel workbook and download
  3389. const wb = XLSX.utils.table_to_book(clonedTable, { raw: true, sheet: "Sheet1" });
  3390. XLSX.writeFile(wb, filename);
  3391. }
  3392. function exportModalTableToExcel() {
  3393. exportTableToExcelHelper('modal-tracking-table', 'Payment_Tracking_Details_Modal_Report.xlsx');
  3394. }
  3395. function exportMainTableToExcel() {
  3396. if (!currentFilteredRecords || currentFilteredRecords.length === 0) {
  3397. alert("No records to export.");
  3398. return;
  3399. }
  3400. // Create a temporary table element in memory
  3401. const tempTable = document.createElement('table');
  3402. // 1. Create head
  3403. const thead = document.createElement('thead');
  3404. thead.innerHTML = `
  3405. <tr>
  3406. <th>Sr. No.</th>
  3407. <th>Received Date</th>
  3408. <th>Invoice Date</th>
  3409. <th>Physical File No.</th>
  3410. <th>eFile No./ Receipt No.</th>
  3411. <th>Subject</th>
  3412. <th>Type of File</th>
  3413. <th>PDF Status</th>
  3414. <th>Vendor Name</th>
  3415. <th>Amount</th>
  3416. <th>Age of Bill (Days)</th>
  3417. <th>Status (Pending / Completed)</th>
  3418. <th>Remarks</th>
  3419. <th>Last Updated At</th>
  3420. <th>Last Updated By</th>
  3421. </tr>
  3422. `;
  3423. tempTable.appendChild(thead);
  3424. // Helpers
  3425. const text = val => (val === null || val === undefined || String(val).trim() === '') ? '-' : val;
  3426. const formatDate = dateStr => {
  3427. if (!dateStr) return '-';
  3428. const parts = dateStr.split('-');
  3429. if (parts.length === 3) {
  3430. return `${parts[2]}/${parts[1]}/${parts[0]}`;
  3431. }
  3432. return dateStr;
  3433. };
  3434. const formatDateTime = dateTimeStr => {
  3435. if (!dateTimeStr) return '-';
  3436. try {
  3437. const d = new Date(dateTimeStr);
  3438. if (!isNaN(d.getTime())) {
  3439. const dd = String(d.getDate()).padStart(2, '0');
  3440. const MM = String(d.getMonth() + 1).padStart(2, '0');
  3441. const yyyy = d.getFullYear();
  3442. const HH = String(d.getHours()).padStart(2, '0');
  3443. const mm = String(d.getMinutes()).padStart(2, '0');
  3444. const ss = String(d.getSeconds()).padStart(2, '0');
  3445. return `${dd}/${MM}/${yyyy} ${HH}:${mm}:${ss}`;
  3446. }
  3447. } catch (e) {
  3448. console.error(e);
  3449. }
  3450. return dateTimeStr;
  3451. };
  3452. const getTypeLabel = typeVal => {
  3453. const cleanType = String(typeVal || '').trim().toLowerCase();
  3454. let label = cleanType;
  3455. if (cleanType === 'asd' || cleanType.includes('asd') || cleanType.includes('additional security')) {
  3456. label = 'ASD';
  3457. } else if (cleanType.includes('agreement')) {
  3458. label = 'Agreement';
  3459. } else if (cleanType === 'sd' || (cleanType.includes('security') && !cleanType.includes('additional') && !cleanType.includes('asd'))) {
  3460. label = 'Security Deposit';
  3461. } else if (cleanType.includes('invoice') || cleanType.includes('bill')) {
  3462. label = 'Tax Invoice';
  3463. } else if (cleanType === 'emd' || cleanType.includes('earnest')) {
  3464. label = 'EMD';
  3465. } else if (cleanType.includes('guarantee') || cleanType === 'bg') {
  3466. label = 'Bank Guarantee';
  3467. } else if (cleanType.includes('chellan') || cleanType.includes('challan') || cleanType === 'dc') {
  3468. label = 'Delivery Chellan';
  3469. } else {
  3470. const typeObj = activeTypes.find(t => String(t.id) === cleanType);
  3471. if (typeObj) {
  3472. return typeObj.type_name;
  3473. }
  3474. label = typeVal || '-';
  3475. }
  3476. return label;
  3477. };
  3478. // 2. Create body
  3479. const tbody = document.createElement('tbody');
  3480. currentFilteredRecords.forEach((row, index) => {
  3481. const tr = document.createElement('tr');
  3482. const dispStatus = String(row.resolved_status || getStatusName(row.status) || '').trim();
  3483. tr.innerHTML = `
  3484. <td>${index + 1}</td>
  3485. <td>${formatDate(row.received_date)}</td>
  3486. <td>${formatDate(row.invoice_date)}</td>
  3487. <td>${text(row.physical_file_no)}</td>
  3488. <td>${text(row.receiptno)}</td>
  3489. <td>${text(row.subject)}</td>
  3490. <td>${getTypeLabel(row.resolved_type || row.type)}</td>
  3491. <td>${row.file_path ? 'PDF Uploaded' : 'pdf file not uploaded'}</td>
  3492. <td>${text(row.resolved_vendor || row.vendor_name)}</td>
  3493. <td>${text(row.amount)}</td>
  3494. <td>${calculateAgeOfBill(row.received_date)}</td>
  3495. <td>${text(dispStatus)}</td>
  3496. <td>${text(row.remarks)}</td>
  3497. <td>${formatDateTime(row.updated_date)}</td>
  3498. <td>${text(row.updated_by_name || '-')}</td>
  3499. `;
  3500. tbody.appendChild(tr);
  3501. });
  3502. tempTable.appendChild(tbody);
  3503. // Convert to Excel workbook and download
  3504. const wb = XLSX.utils.table_to_book(tempTable, { raw: true, sheet: "Sheet1" });
  3505. XLSX.writeFile(wb, 'Payment_Tracking_Details_Main_Report.xlsx');
  3506. }
  3507. // --- LATEST UPDATES CORNER BADGE LOGIC ---
  3508. function updateLatestUpdateBadge() {
  3509. const badge = document.getElementById('latest-update-badge');
  3510. if (!badge) return;
  3511. if (!allRecords || allRecords.length === 0) {
  3512. badge.style.setProperty('display', 'none', 'important');
  3513. return;
  3514. }
  3515. // Find the maximum updated_date
  3516. let maxTime = 0;
  3517. let maxDateStr = null;
  3518. allRecords.forEach(r => {
  3519. if (r.updated_date) {
  3520. const time = new Date(r.updated_date).getTime();
  3521. if (!isNaN(time) && time > maxTime) {
  3522. maxTime = time;
  3523. maxDateStr = r.updated_date;
  3524. }
  3525. }
  3526. });
  3527. if (maxTime === 0 || !maxDateStr) {
  3528. badge.style.setProperty('display', 'none', 'important');
  3529. return;
  3530. }
  3531. // Count how many files share this exact latest update date/time stamp
  3532. let count = 0;
  3533. allRecords.forEach(r => {
  3534. if (r.updated_date === maxDateStr) {
  3535. count++;
  3536. }
  3537. });
  3538. // Formatted date string helper
  3539. const formatDateTimeHelper = dateTimeStr => {
  3540. if (!dateTimeStr) return '-';
  3541. try {
  3542. const d = new Date(dateTimeStr);
  3543. if (isNaN(d.getTime())) return dateTimeStr;
  3544. const pad = n => String(n).padStart(2, '0');
  3545. return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
  3546. } catch (e) {
  3547. return dateTimeStr;
  3548. }
  3549. };
  3550. const countEl = document.getElementById('latest-update-count');
  3551. const timeEl = document.getElementById('latest-update-time');
  3552. if (countEl) countEl.textContent = count;
  3553. if (timeEl) timeEl.textContent = formatDateTimeHelper(maxDateStr);
  3554. badge.style.setProperty('display', 'flex', 'important');
  3555. badge.setAttribute('data-latest-date', maxDateStr);
  3556. }
  3557. function openLatestUpdatesModal() {
  3558. const badge = document.getElementById('latest-update-badge');
  3559. if (!badge) return;
  3560. const latestDate = badge.getAttribute('data-latest-date');
  3561. if (!latestDate) return;
  3562. openModalForCount('latest_updates', latestDate, 'Latest Updates');
  3563. }
  3564. function escapeHtml(str) {
  3565. if (!str) return '';
  3566. return String(str)
  3567. .replace(/&/g, "&amp;")
  3568. .replace(/</g, "&lt;")
  3569. .replace(/>/g, "&gt;")
  3570. .replace(/"/g, "&quot;")
  3571. .replace(/'/g, "&#039;");
  3572. }
  3573. // --- END OF NEW METRICS & INTERACTIVE CLICK FILTER LOGIC ---
  3574. // Delete record API call with remarks confirmation modal
  3575. let recordIdToDelete = null;
  3576. let deleteConfirmModalObj = null;
  3577. function deleteRecord(id) {
  3578. recordIdToDelete = id;
  3579. document.getElementById('delete-remarks').value = '';
  3580. if (!deleteConfirmModalObj) {
  3581. deleteConfirmModalObj = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
  3582. }
  3583. deleteConfirmModalObj.show();
  3584. }
  3585. function submitDeleteRecord() {
  3586. if (!recordIdToDelete) return;
  3587. const remarks = document.getElementById('delete-remarks').value.trim();
  3588. if (!remarks) {
  3589. alert('Please enter remarks/reason for deletion.');
  3590. return;
  3591. }
  3592. if (deleteConfirmModalObj) {
  3593. deleteConfirmModalObj.hide();
  3594. }
  3595. showLoader(true, 'Deleting record...');
  3596. const csrfHeaders = getCsrfHeaders();
  3597. const headers = {
  3598. 'Content-Type': 'application/x-www-form-urlencoded',
  3599. ...csrfHeaders
  3600. };
  3601. const body = new URLSearchParams();
  3602. body.append('id', recordIdToDelete);
  3603. body.append('remarks', remarks);
  3604. fetch(contextPath + 'gccservicepaymenttracker/api/delete', {
  3605. method: 'POST',
  3606. headers: headers,
  3607. body: body
  3608. })
  3609. .then(response => response.json())
  3610. .then(data => {
  3611. if (data.success) {
  3612. recordIdToDelete = null;
  3613. loadCounts();
  3614. loadList();
  3615. } else {
  3616. alert(data.error || 'Failed to delete record.');
  3617. showLoader(false);
  3618. }
  3619. })
  3620. .catch(err => {
  3621. console.error("Delete Error:", err);
  3622. alert("Failed to delete record due to connection error.");
  3623. showLoader(false);
  3624. });
  3625. }
  3626. // Show update history for a payment tracking record
  3627. function showHistoryModal(id) {
  3628. const tbody = document.getElementById('history-tbody');
  3629. const noHistoryFound = document.getElementById('no-history-found');
  3630. tbody.innerHTML = '';
  3631. noHistoryFound.style.display = 'none';
  3632. historyModalObj.show();
  3633. // Local formatting helpers for history records
  3634. const text = val => (val === null || val === undefined || String(val).trim() === '') ? '-' : val;
  3635. const formatDate = dateStr => {
  3636. if (!dateStr) return '-';
  3637. const parts = dateStr.split('-');
  3638. if (parts.length === 3) {
  3639. return `${parts[2]}/${parts[1]}/${parts[0]}`;
  3640. }
  3641. return dateStr;
  3642. };
  3643. const formatDateTime = dateTimeStr => {
  3644. if (!dateTimeStr) return '-';
  3645. try {
  3646. const d = new Date(dateTimeStr);
  3647. if (!isNaN(d.getTime())) {
  3648. const dd = String(d.getDate()).padStart(2, '0');
  3649. const MM = String(d.getMonth() + 1).padStart(2, '0');
  3650. const yyyy = d.getFullYear();
  3651. const HH = String(d.getHours()).padStart(2, '0');
  3652. const mm = String(d.getMinutes()).padStart(2, '0');
  3653. const ss = String(d.getSeconds()).padStart(2, '0');
  3654. return `${dd}/${MM}/${yyyy} ${HH}:${mm}:${ss}`;
  3655. }
  3656. } catch (e) {
  3657. console.error(e);
  3658. }
  3659. return dateTimeStr;
  3660. };
  3661. fetch(contextPath + 'gccservicepaymenttracker/api/history?id=' + id)
  3662. .then(response => response.json())
  3663. .then(historyList => {
  3664. if (!historyList || historyList.length === 0) {
  3665. noHistoryFound.style.display = 'block';
  3666. return;
  3667. }
  3668. historyList.forEach((item, index) => {
  3669. const tr = document.createElement('tr');
  3670. const dispStatus = String(item.resolved_status || getStatusName(item.status) || '').trim();
  3671. // Document button if file path is available
  3672. let docBtnHtml = '-';
  3673. if (item.file_path) {
  3674. let pdfUrl = item.file_path;
  3675. if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
  3676. pdfUrl = contextPath + pdfUrl.substring(1);
  3677. } else {
  3678. pdfUrl = contextPath + pdfUrl;
  3679. }
  3680. docBtnHtml = `<a href="${pdfUrl}" target="_blank" class="btn-grid-action btn-grid-pdf"><i class="fa-solid fa-file-pdf"></i></a>`;
  3681. }
  3682. // Resolve type badge
  3683. let typeHtml = '';
  3684. if (item.type) {
  3685. const cleanType = String(item.type).toLowerCase().trim();
  3686. let label = cleanType;
  3687. let badgeClass = 'type-sd';
  3688. if (cleanType === 'asd' || cleanType.includes('asd') || cleanType.includes('additional security')) {
  3689. label = 'ASD';
  3690. badgeClass = 'type-asd';
  3691. } else if (cleanType.includes('agreement')) {
  3692. label = 'Agreement';
  3693. badgeClass = 'type-agreement';
  3694. } else if (cleanType === 'sd' || (cleanType.includes('security') && !cleanType.includes('additional') && !cleanType.includes('asd'))) {
  3695. label = 'Security Deposit';
  3696. badgeClass = 'type-sd';
  3697. } else if (cleanType.includes('invoice') || cleanType.includes('bill')) {
  3698. label = 'Tax Invoice';
  3699. badgeClass = 'type-tax-invoice';
  3700. } else if (cleanType === 'emd' || cleanType.includes('earnest')) {
  3701. label = 'EMD';
  3702. badgeClass = 'type-emd';
  3703. } else if (cleanType.includes('guarantee') || cleanType === 'bg') {
  3704. label = 'Bank Guarantee';
  3705. badgeClass = 'type-bank-guarantee';
  3706. } else if (cleanType.includes('chellan') || cleanType.includes('challan') || cleanType === 'dc') {
  3707. label = 'Delivery Chellan';
  3708. badgeClass = 'type-delivery-chellan';
  3709. } else {
  3710. const typeObj = activeTypes.find(t => String(t.id) === cleanType);
  3711. if (typeObj) {
  3712. label = typeObj.type_name;
  3713. }
  3714. badgeClass = 'status-default';
  3715. }
  3716. typeHtml = `<span class="type-badge ${badgeClass}">${label}</span>`;
  3717. } else {
  3718. typeHtml = '-';
  3719. }
  3720. // Resolve vendor name
  3721. let resolvedVendor = item.resolved_vendor || item.vendor_name || '-';
  3722. tr.innerHTML = `
  3723. <td class="text-center fw-semibold text-secondary">${index + 1}</td>
  3724. <td>${formatDate(item.received_date)}</td>
  3725. <td>${formatDate(item.invoice_date)}</td>
  3726. <td>${text(item.subject)}</td>
  3727. <td>${typeHtml}</td>
  3728. <td class="fw-semibold text-secondary">${text(resolvedVendor)}</td>
  3729. <td class="fw-semibold text-dark">&#8377; ${text(item.amount)}</td>
  3730. <td><span class="status-badge status-${dispStatus.toLowerCase().replace(/\s+/g, '-')}">${text(dispStatus)}</span></td>
  3731. <td><small class="text-muted">${text(item.remarks)}</small></td>
  3732. <td>${formatDateTime(item.updated_at)}</td>
  3733. <td class="fw-semibold text-secondary">${text(item.updated_by_name || '-')}</td>
  3734. <td class="text-center">${docBtnHtml}</td>
  3735. `;
  3736. tbody.appendChild(tr);
  3737. });
  3738. })
  3739. .catch(err => {
  3740. console.error("Fetch History Error:", err);
  3741. tbody.innerHTML = '<tr><td colspan="12" class="text-center text-danger py-3"><i class="fa-solid fa-triangle-exclamation"></i> Failed to load history records.</td></tr>';
  3742. });
  3743. }
  3744. // --- DELETED RECORDS MODAL & RESTORE LOGIC ---
  3745. function openDeletedRecordsModal() {
  3746. if (!deletedRecordsModalObj) {
  3747. deletedRecordsModalObj = new bootstrap.Modal(document.getElementById('deletedRecordsModal'));
  3748. }
  3749. deletedRecordsModalObj.show();
  3750. showLoader(true, 'Fetching deleted records...');
  3751. fetch(contextPath + 'gccservicepaymenttracker/api/deleted-list?_=' + new Date().getTime())
  3752. .then(response => response.json())
  3753. .then(data => {
  3754. renderDeletedRecordsTable(data);
  3755. showLoader(false);
  3756. })
  3757. .catch(err => {
  3758. console.error("Fetch Deleted Records Error:", err);
  3759. alert("Failed to load deleted records.");
  3760. showLoader(false);
  3761. });
  3762. }
  3763. function renderDeletedRecordsTable(data) {
  3764. const tbody = document.getElementById('deleted-tracking-tbody');
  3765. const noRecords = document.getElementById('deleted-no-records-found');
  3766. tbody.innerHTML = '';
  3767. if (!data || data.length === 0) {
  3768. noRecords.style.display = 'block';
  3769. return;
  3770. }
  3771. noRecords.style.display = 'none';
  3772. const text = val => (val === null || val === undefined || String(val).trim() === '') ? '-' : val;
  3773. const formatDate = dateStr => {
  3774. if (!dateStr) return '-';
  3775. const parts = dateStr.split('-');
  3776. if (parts.length === 3) {
  3777. return `${parts[2]}/${parts[1]}/${parts[0]}`;
  3778. }
  3779. return dateStr;
  3780. };
  3781. const formatDateTime = dateTimeStr => {
  3782. if (!dateTimeStr) return '-';
  3783. try {
  3784. const d = new Date(dateTimeStr);
  3785. if (isNaN(d.getTime())) return dateTimeStr;
  3786. const pad = n => String(n).padStart(2, '0');
  3787. return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
  3788. } catch (e) {
  3789. return dateTimeStr;
  3790. }
  3791. };
  3792. const getTypeBadge = (typeVal, filePath) => {
  3793. const cleanType = String(typeVal || '').trim().toLowerCase();
  3794. let label = cleanType;
  3795. let badgeClass = 'type-sd';
  3796. if (cleanType === 'asd' || cleanType.includes('asd') || cleanType.includes('additional security')) {
  3797. label = 'ASD';
  3798. badgeClass = 'type-asd';
  3799. } else if (cleanType.includes('agreement')) {
  3800. label = 'Agreement';
  3801. badgeClass = 'type-agreement';
  3802. } else if (cleanType === 'sd' || (cleanType.includes('security') && !cleanType.includes('additional') && !cleanType.includes('asd'))) {
  3803. label = 'Security Deposit';
  3804. badgeClass = 'type-sd';
  3805. } else if (cleanType.includes('invoice') || cleanType.includes('bill')) {
  3806. label = 'Tax Invoice';
  3807. badgeClass = 'type-tax-invoice';
  3808. } else if (cleanType === 'emd' || cleanType.includes('earnest')) {
  3809. label = 'EMD';
  3810. badgeClass = 'type-emd';
  3811. } else if (cleanType.includes('guarantee') || cleanType === 'bg') {
  3812. label = 'Bank Guarantee';
  3813. badgeClass = 'type-bank-guarantee';
  3814. } else if (cleanType.includes('chellan') || cleanType.includes('challan') || cleanType === 'dc') {
  3815. label = 'Delivery Chellan';
  3816. badgeClass = 'type-delivery-chellan';
  3817. } else {
  3818. const typeObj = activeTypes.find(t => String(t.id) === cleanType);
  3819. if (typeObj) {
  3820. return getTypeBadge(typeObj.type_name, filePath);
  3821. }
  3822. label = typeVal || '-';
  3823. badgeClass = 'status-default';
  3824. }
  3825. let onClickAttr = '';
  3826. if (filePath) {
  3827. let pdfUrl = filePath;
  3828. if (contextPath && contextPath.endsWith('/') && pdfUrl.startsWith('/')) {
  3829. pdfUrl = contextPath + pdfUrl.substring(1);
  3830. } else {
  3831. pdfUrl = contextPath + pdfUrl;
  3832. }
  3833. onClickAttr = `onclick="window.open('${escapeHtml(pdfUrl)}', '_blank')"`;
  3834. } else {
  3835. onClickAttr = `onclick="alert('pdf file not uploaded')"`;
  3836. }
  3837. return `<span class="type-badge ${badgeClass} cursor-pointer" ${onClickAttr} style="cursor: pointer;" title="${filePath ? 'Click to view PDF' : 'PDF not uploaded'}">${label}</span>`;
  3838. };
  3839. data.forEach((row, index) => {
  3840. const tr = document.createElement('tr');
  3841. const tdIndex = document.createElement('td');
  3842. tdIndex.className = 'text-center fw-semibold text-secondary';
  3843. tdIndex.textContent = index + 1;
  3844. tr.appendChild(tdIndex);
  3845. const dispStatus = String(row.resolved_status || getStatusName(row.status) || '').trim();
  3846. tr.innerHTML += `
  3847. <td>${formatDate(row.received_date)}</td>
  3848. <td>${formatDate(row.invoice_date)}</td>
  3849. <td>${text(row.physical_file_no)}</td>
  3850. <td>${text(row.receiptno)}</td>
  3851. <td>${text(row.subject)}</td>
  3852. <td>${getTypeBadge(row.resolved_type || row.type, row.file_path)}</td>
  3853. <td class="fw-semibold text-secondary">${text(row.resolved_vendor || row.vendor_name)}</td>
  3854. <td class="fw-semibold text-dark">&#8377; ${text(row.amount)}</td>
  3855. <td>${calculateAgeOfBill(row.received_date)}</td>
  3856. <td><span class="status-badge status-${dispStatus.toLowerCase().replace(/\s+/g, '-')}">${text(dispStatus)}</span></td>
  3857. <td><small class="text-danger fw-semibold">${text(row.remarks)}</small></td>
  3858. <td>${formatDateTime(row.updated_date)}</td>
  3859. <td class="fw-semibold text-secondary">${text(row.updated_by_name)}</td>
  3860. `;
  3861. // Actions cell
  3862. const tdActions = document.createElement('td');
  3863. tdActions.className = 'text-center';
  3864. tdActions.style.whiteSpace = 'nowrap';
  3865. // Restore Button
  3866. const btnRestore = document.createElement('button');
  3867. btnRestore.className = 'btn btn-sm btn-outline-warning d-inline-flex align-items-center gap-1';
  3868. btnRestore.innerHTML = '<i class="fa-solid fa-trash-arrow-up"></i> Restore';
  3869. btnRestore.style.fontSize = '0.72rem';
  3870. btnRestore.style.fontWeight = '600';
  3871. btnRestore.title = 'Restore Payment Record';
  3872. btnRestore.onclick = () => {
  3873. restoreDeletedRecord(row.id);
  3874. };
  3875. tdActions.appendChild(btnRestore);
  3876. tr.appendChild(tdActions);
  3877. tbody.appendChild(tr);
  3878. });
  3879. }
  3880. function restoreDeletedRecord(id) {
  3881. if (!confirm("Are you sure you want to restore this payment record?")) {
  3882. return;
  3883. }
  3884. if (deletedRecordsModalObj) {
  3885. deletedRecordsModalObj.hide();
  3886. }
  3887. showLoader(true, 'Restoring record...');
  3888. const csrfHeaders = getCsrfHeaders();
  3889. const headers = {
  3890. 'Content-Type': 'application/x-www-form-urlencoded',
  3891. ...csrfHeaders
  3892. };
  3893. const body = new URLSearchParams();
  3894. body.append('id', id);
  3895. fetch(contextPath + 'gccservicepaymenttracker/api/restore', {
  3896. method: 'POST',
  3897. headers: headers,
  3898. body: body
  3899. })
  3900. .then(response => response.json())
  3901. .then(data => {
  3902. if (data.success) {
  3903. alert('Record successfully restored!');
  3904. loadCounts();
  3905. loadList();
  3906. } else {
  3907. alert(data.error || 'Failed to restore record.');
  3908. showLoader(false);
  3909. }
  3910. })
  3911. .catch(err => {
  3912. console.error("Restore Error:", err);
  3913. alert("Failed to restore record due to connection error.");
  3914. showLoader(false);
  3915. });
  3916. }
  3917. // Dynamic stacked modals manager to handle overlays and backdrops dynamically
  3918. document.addEventListener('show.bs.modal', function (event) {
  3919. // Find currently shown modals
  3920. const openModals = document.querySelectorAll('.modal.show');
  3921. const count = openModals.length + 1;
  3922. const zIndex = 1050 + (10 * count);
  3923. event.target.style.setProperty('z-index', zIndex, 'important');
  3924. // Adjust the backdrop z-index on next tick
  3925. setTimeout(() => {
  3926. const backdrops = document.querySelectorAll('.modal-backdrop');
  3927. if (backdrops.length > 0) {
  3928. backdrops[backdrops.length - 1].style.setProperty('z-index', zIndex - 5, 'important');
  3929. }
  3930. }, 0);
  3931. });
  3932. document.addEventListener('hidden.bs.modal', function (event) {
  3933. event.target.style.removeProperty('z-index');
  3934. // If there are still open modals, make sure the body still has the modal-open class
  3935. const openModals = document.querySelectorAll('.modal.show');
  3936. if (openModals.length > 0) {
  3937. document.body.classList.add('modal-open');
  3938. }
  3939. });
  3940. </script>
  3941. </body>
  3942. </html>