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.

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