diff --git a/.github/workflows/frontend-deployment.yml b/.github/workflows/frontend-deployment.yml index 5f310d8d3..ae5bbf465 100644 --- a/.github/workflows/frontend-deployment.yml +++ b/.github/workflows/frontend-deployment.yml @@ -11,7 +11,7 @@ jobs: deploy_front_end: runs-on: ubuntu-latest env: - REACT_APP_BACKEND: https://ck5kt5uaw1.execute-api.us-east-1.amazonaws.com/dev/api + REACT_APP_BACKEND: https://api.carbreg.org COUNTRY_NAME: 'Antarctic Region' COUNTRY_FLAG_URL: 'https://carbon-common-dev.s3.amazonaws.com/flag.png' COUNTRY_CODE: 'NG' diff --git a/README.md b/README.md index d76b37b94..9101eea4a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ # Carbon Registry The National Carbon Registry enables carbon credit trading in order to reduce greenhouse gas emissions. -As an online database, the National Carbon Registry uses standards national and international standards for quantifying and verifying greenhouse gas emissions reductions of programmes, tracking issued carbon credits and enabling credit transfers in an efficient and transparent manner. The Registry functions by receiving, processing, recording and storing data on mitigations projects, the issuance, holding, transfer, acquisition, cancellation, and retirement of emission reduction credits. This information is publicly accessible to increase public confidence in the emissions reduction agenda. +As an online database, the National Carbon Registry uses national and international standards for quantifying and verifying greenhouse gas emissions reductions by programmes, tracking issued carbon credits and enabling credit transfers in an efficient and transparent manner. The Registry functions by receiving, processing, recording and storing data on mitigations projects, the issuance, holding, transfer, acquisition, cancellation, and retirement of emission reduction credits. This information is publicly accessible to increase public confidence in the emissions reduction agenda. The National Carbon Registry enables carbon credit tracking transactions from mitigation activities, as the digital implementation of the Paris Agreement. Any country can customize and deploy a local version of the registry then connect it to other national & international registries, MRV systems, and more. @@ -34,8 +34,83 @@ https://digitalprinciples.org/ ## System Architecture -UNDP Carbon Registry based on Serverless Architecture. It can be ported and hosted on any Function As A Service (FaaS) stack. -![alt text](./documention/imgs/System%20Architecture.png) +UNDP Carbon Registry is based on service oriented architecture (SOA). It can be ported and hosted on any Function As A Service (FaaS) stack. +![alt text](./documention/imgs/System%20Architecture.svg) + +As per the above diagram, system contains 4 main services. + + +### **Services** +#### *National Service* + +Authenticate, Validate and Accept user (Government, Programme Developer/Certifier) API requests related to the following functionalities, +- User and company CRUD operations. +- User authentication. +- Programme life cycle management. +- Credit life cycle management. + +Service is horizontally scalable and state maintained in the following locations, +- File storage. +- Operational Database. +- Ledger Database. + +Uses the Carbon Credit Calculator and Serial Number Generator node modules to estimate the programme carbon credit amount and issue a serial number. +Uses Ledger interface to persist programme and credit life cycles. + +#### *Analytics Service* +Serve all the system analytics. Generate all the statistic using the operational database. +Horizontally scalable. + +#### *Replicator Service* +Replicate ledger database new items to a operational database asynchronously. During the replication process it injects additional query information to the data. +The current setup uses AWS QLDB as the ledger database. When it creates or updates data, the change is added to a AWS Kinesis Data Stream and the Replicator service consumes the stream. + +#### *Operational Service* +Service that use to do following system operations, +1. Data migrations. +2. User data creation and update. +3. Resource creation. + +Internal service. Cannot be invoked by external sources. + +### **Database Architecture** +Primary/secondary database architecture used to store carbon programme and account balances. +Ledger database is the primary database. Add/update programmes and update account balances in a single transaction. Currently implemented only for AWS QLDB + +Operational Database is the secondary database. Eventually replicated to this from primary database via data stream. Implemented based on PostgresSQL + +**Why Two Database Approach?** +1. Cost and Query capabilities - Ledger database (blockchain) read capabilities can be limited and costly. To support rich statistics and minimize the cost, data is replicated in to a cheap query database. +2. Disaster recovery +3. Scalability - Primary/secondary database architecture is scalable since additional secondary databases can be added as needed to handle more read operations. + +**Why Ledger Database?** +1. Immutable and Transparent - Track and maintain a sequenced history of every carbon programme and credit change. +2. Data Integrity (Cryptographic verification by third party). +3. Reconcile carbon credits and company account balance. + +**Ledger Database Interface** + +This enables the capability to add any blockchain or ledger database support to the carbon registry without functionality module changes. Currently the production system interface is implemented for AWS QLDB. For testing purposes the interface is implemented for PostgresSQL as well. + + + +Single database approach used for user and company management. + + +### **Ledger Layout** +Carbon Registry contains 3 ledger tables. +1. Programme ledger - Contains all the programme and credit transactions. +2. Company Account Ledger (Credit) - Contains company accounts credit transactions. +3. Country Account Ledger (Credit) - Contains country credit transactions. + +The below diagram demonstrates the the ledger behavior of programme create, authorise, issue and transfer processes. Blue color document icon denotes a single data block in a ledger. + +![alt text](./documention/imgs/Ledger.png) + +### **Authentication** +- JWT Authentication - All endpoints based on role permissions. +- API Key Authentication - MRV System connectivity. ## Project Structure @@ -63,7 +138,7 @@ UNDP Carbon Registry based on Serverless Architecture. It can be ported and host ## Run Services Locally - Setup postgreSQL locally and create a new database. -- Update following DB configurations in the .env.local file (If file does not exist please create a new .env.local) +- Update following DB configurations in the .env.local file (If the file does not exist please create a new .env.local) - DB_HOST (Default localhost) - DB_PORT (Default 5432) - DB_USER (Default root) @@ -73,7 +148,7 @@ UNDP Carbon Registry based on Serverless Architecture. It can be ported and host - Run `yarn run sls:install ` - Initial user data setup `serverless invoke local --stage=local --function setup --data '{"rootEmail": "","systemCountryCode": "", "name": "", "logoBase64": ""}'` - Start all the services by executing `sls offline --stage=local` -- Now all the system services are up and running. Swagger documentation will be available on `http://localhost:3000/local/api/national/docs#/` +- Now all the system services are up and running. Swagger documentation will be available on `http://localhost:3000/local/national` ## Deploy System on the AWS Cloud @@ -108,7 +183,7 @@ Serial Number generation implemented in a separate node module. [Please refer th ## User Onboarding and Permissions Model ### User Roles -System pre-defined user roles as follows, +System pre-defined user roles are as follows, - Root - Company Level (National Government, Programme and Certification Company come under this level) - Admin @@ -125,7 +200,7 @@ System pre-defined user roles as follows, ### User Management -All the CRUD operations can perform as per the following table, +All the CRUD operations can be performed as per the following table, | Company Role | New User Role | Authorized User Roles (Company) | | --- | --- | --- | diff --git a/documention/Carbon Registry Architecture.drawio b/documention/Carbon Registry Architecture.drawio index c1a191acb..a399ec526 100644 --- a/documention/Carbon Registry Architecture.drawio +++ b/documention/Carbon Registry Architecture.drawio @@ -1 +1 @@ -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 \ No newline at end of file +7b1Zl6NG1jX8a7zW9124FvNwCUKAJAQIJIG48WKe55lf/0ZkZdaQmWW7u6tc7n5cdmUlwRTD2fvscwiCX/BduUid2yTnOgiLXzAkWH7BhV8wjCUJ8BMWrB8LMJTFPpbEXRp8LEM/F5jpFj4XIs+lYxqE/VcHDnVdDGnzdaFfV1XoD1+VuV1Xz18fFtXF13dt3Dh8U2D6bvG21EqDIXkuRSn28w45TOPk+dYMRn/cUbovBz+3pE/coJ6/KML3v+C7rq6Hj7+Vyy4sYOe99MvH88Rv7P1UsS6shj9zQt74RX7srNQTKnZnh8jRo399HozJLcbnBithEIcdKBPcwfXcPnyu/bC+dElXj1UQwquiv+D8nKRDaDauD/fOwAhAWTKUxfPuKeyGFHQnV6RxBcqGGh7wfFewL1y+2Rz0UycB6wrrMhy6FRzyfAJOPvfrs2X9Sr+YzPx5nLCXg5IvhghFngvdZ9uIP138c/eBX5578P3evO03/cZx2qETf017JiZ3dPsrSpNv+vMXjCrAjXkP9CkVD58a/0WHNnVaDU91IXnwP6jdDvmFBHt2cOsDRr4qeL1Nf12Avt2C1/i64PU2/XUB+vry6Kv7o68r+EXBm62vLo+8uj/yRQXB/zhfj0ORVuHuE6IRUBh3bpAC09jVRd2Bsqquwq8t7T1DjOpqeOYUFHvZfu54eFWAyQb+Xi4xpK8P7twTH2Jg4M3TLQ+AVd7d+xv49Te/qMfgN/dpdPuhq/PwpXK/YDj4T4SWw0dpUbyq9PuYcJ+3ijB6uiJoSVrFytOWgCPPtf/iFrudKLIsKA/cPvmEx8L1woJ3/Tx+wukXx0dPf8AhoFWDC3q4e+6FJ/sLu/0UfjRD5OmYonCbPvU+9VUX+mPXp1NohP3HLkU+GfMb2L4D7m8iGX2NZAp5i2SaeAfJOPodkDzelLT5dRRucVwMg/2baVryryj7BqWQF19sp+6GpI7ryi32n0v5z8QIe+bzMUoNB/hpdLJwGNZni3THof7ahEEfdqv9fP7TxgNuALA8bwrLlzuF9cstPexS0PhPo/rKWlhuR/Pi741YX4+d/8LxUc/jOv8YKc9YJrI71rvt1+c+GdwuDoff6bxnDwg77HfHvwsLdwAG9bXT/g+G8vdq/QUlQ88GSsyhC93yzTj3eTj4yZfQeE3N79LzexT9Lk2/peqvDnsiz3fu8LrwvTL6bSH69rAXvn1b+F7Ze87l9dnoO2ejr87+NrW/MtRPzPma8qERE6QgYl/sE1JAS0P6RNMVRNwrxgXnkByO8OR7HP2JED8RKPIFOyuQSPW6T58v79XDUJd/SN9+WD1h8Etc/5EbcvvmY3dE6QLr8b5f6sKPCP3olQAf9+/5pxz0b5/2vwXAyH/rn0y8/9T4FwmLfh/mJgjsa+ZmcPoD+Ya7Keotdb+UfXe4o2/gflGEt5rrH5T/fVEu0sweIf41lAsIuUPp/zMob0e3Gsbyt+IpWnsC+1Os9l1gjdPoK0HGoG9A/QL9L0H9Uvbd5RiOvAHw/yk59o1ok/6Tgoz43oLs6VSu69z1iwOeWfTzlXVY8IVZEa90Pk4yryzj4yU/28mnuv0nMTn9xiOYQ93BvM9rm/qPiOs5uHvFSxhNoyj1hpeeD/7bUdLrUBD9Fk15o59Dm/sz8eZ3oCTste18Q2mQH3DmHbHB/CBewsify0ufqejxFRP91DAR/bO0xPysOPH9sWR+6liiv/w4H9ODoRg4mA2HPFG4fZ/6L8ViWrzU5S8Y8r+HJ6Kw1xmn58St+GdP+N6u63d79QvPpTVh50K/4MKz4f3hz9cZ5ssYPvXiN1P4/0Q/f9/ohxBosPNfi37AH4Jn/89EP8FauWUdeD8qr0G9zmu892yJfich/VL2/fPR7z1Y+uu91X8kFpQEJQj+0u5/7fqqUI/KbxhoGP4nPQf2s8TC71b7i/GQamBgVRk++Y2PzKx3NbDasgwhF4dTWNTN06PVj3t3EGNRCkv+kJ7fSvKPz5KR18j+YrDejTheJ0cR+N+LjP+CCT5B/N+kindZ8A1/vOaAOopSP/ww9mHXfxjCyq2G39ygTKsfhXTyRfW9IB2lmA/MG6yT72QwyR8VVKDU/wi0iT8JbfS7q8L/DNvEH8iuW/+E4v/PgHM3XtBsjh+xDQv//7eI/l6AJQiW/Pyc9zsC9g0230Xwa8BOYeiWHxpgb27xY5FK0/SHP4VVgvhAvEUr8enQ745X4qc8K/5Cb2HYW/wiCAUfo7+K/Z5t6svA73cf4v8hxl+E0h9iHKf+FpEfSr8K5FDm1aypVydgLxO9vnHCfxz5/W63fkFBN1Xc7XbvEcsTINPyaSLbH6P/G/m6IIzc8SmM/AY9vJbvTzfkXkqRlxJ4Kfh4Hec+bmJi1oQxDJcwkc3AD4LjLmbuHI2Y47kLB7bAvxxoiojNuz3H5RK/nM19cjP3y9kSjRv4G9uSEVsS79uyET+kxH+ISx5KC+LKZhzLRl3IxsM6XPfp0cw7ZSVGcWE2TTgP6hoPStrzu7g+CTGiSQt3kVakPOweg2IiE/gL/x3Owg38S4C/zPBS9o2/n85RIjj4HL8/cIrP7+czf4i53f6yF/YXGexaOM7jONCkeQ9aCv794g9/ecg8d95x8Bp7bn8BPbEXuHh/BHvg4RI4i3s6Oz7v95ebtE/3y+Uugrbvr4dGSvrrcXcrj+t88W8HVhRAu3dn4pQyO+P6+AXj5ZQxnA1J7I81cUA/C6AWPPf6z3wRufi5vssejsa83++42/5jDT5Wl+MOoFWX5Hzbm4dCTGJHShheXXpfNkQB1CTTE8bU4SjG3PnpzAT2Awe7SF2M/SXZG/uDsTd2e3tmCe545Dg0pzhRVpfyAADBu7TmSTt7knt16KchmEW0AsU3TV/vSqoU/UUPnIkY8QqBEORPm56r531qIWom+nev3U6Yru/vGrKte0wcBI9hp4tFtF5drs5EJbwfHAXDqQlgi3yepUEk3Gr5gHPj4mA9sFHxFqqcPqraVgZTMt0bO8dioe7XDeIdmR7DOTOjFTTieM0Nn7Xv59S1V02wMvcKjhhJ4lQRQ7F03hVwkDhz9EKp4Jdovh6Iao3c/Wje8wgX7UeJ5a3a+lFTs0JT2jhHLoyEWw8UO16vEpNnFbML5Itw0Xm/HbwH4Gj+OtprI1s4a2xgS2wUpqdubqXrxwRsswUzrGgDDIqvNS/CTluyx6mTEm0mqEJ3VPGjmwnF1dQgKLnoJqvMqpzucfVg7AfZJZN7iYzHOrXSfRMq1beqEMnlZWdRM2zEvmuGIQBXv9BU5+aW93CKTJcfplV4C6JRmjS1u65JJO2CHuis04bmQV+IQK2JxDSxgAlIEUNOoxZkdOyxaXEz9olmE5cWtwngHfgiF9xU17KAPnfqxG/Xektm/eCJzn0hqsu5ygKJcYUBi+mddrB9xxW6XU3PtjAT2V2lbtJhuQ5bXSUzIPqsFBr5HqTr6d6jsnaRxQ6TLfWa9OSgES2438MoJMzdr2KzlRGRWSmbMBepH1Cqy/0btMhNLHuMEY/JwAXtOJ76k1I1pyHvLmCvOvsIlQoQwA6h1Ldx1/Btekj3h/SQ7MCPy6eNU0o6D4YYVWV37Xoms4gK52cQQvISa7ndIqvn2HuwhWRPhLuzBhK4oYMQ9uzqJufBFisXEmkFQaqAERBb0XEeS3SgT2Bjmh73o2uXZOajJ+ee35j9XdoIH1zdxCfZiTqf5Sbh2O34BwpLU0mfOBlfVWC2Ctg+9qLyKC5G6QCLHrfH7GOa7jIq4BqX3uPqqJWzLcqWvuxqZy+RhTYni6snUqcR1Y3vJ/QKTkgQuSTxKuUHzV4LzdX3bXSsmX4XYNHc4VdT3KTx0JvIPvNNBbPbVDlbcZr00UJphdfQSCIHD/1Gx+WtVKHtUKTsSsDyeJLgneR2HXPfnnxb8728uGPuFh47bWbviqsYtdv5kRHCxhCFctM3UrZx4SYx8WiI6CNI+p7u12PjBw3KT2dabG1VmnlnoLPhaJ+mVjuPahmPGsHl4CLcHfyYu8N4LLJziASjOCORz0a8ZZUNZmTqY+n40eV48mSa+FnG0rZ1HrhMZ4bjiLFq0nf+iIqTBaQJXznaHdiLGOuUpdf3bnfU0Y2NgcWLp4Ddb+49U6+GaYBDcYmtUPXA3gmgKMTdI8uX62ZrTqlBvtNH8POAyvNVsgiZTlYTxw7gKmnormzUeyuSzEeeH3fCI2Z2TScxpdOG9MRw+wz2TI1GN7XSb7VhDT5KtAi6y3xkUHdieC6ZrqeQ++UitN6x5QkNl9Ou7q/3bVXXTMUQBjjbGpdXNxoID+gH78R3PZFYWAjjnxSqnG5NDz1rty1pPkKsUzC1PxKeOG29z3mhuswHoq0J3hqbtmhUjKXPoVAjewzsQVnRO/fE8YCfu8PVQaIg8AgzEJdBXC/hA1dinxL2SZ+0sTo27HQLH0TinpXWwPFBXXAvlELyYg7d8WZrkvQUkfHMQDG+UQYJ+rhKOtIouqImoHv48XBQx3S6Kzc1f2BGMqIzzxPirVJOItlQoUF7uhfAPgsmk95dwhPLQKM9uSakqV5hCHqRCNfz0x297GxFbhbAzyL5kKN8JHBq3zhFqmLuuCrEg9lzMc4jceaKbHu0vmaH8bpv+8uiF0HrIlUZxFQ5U113v0HClerNDTqeppcoR/bg1kO+HsuSUe19xvKJPwq7xtZnsKMrfG8KSdKPmC5SzLvonFHF9U1USfzBK88ec8XdK8JJOXM/kkql1W5TEpqebtDv35R4FM2740n4uPSjrWE7cgy9lOM98jJI0DNSPDp3EXkT8b7eXVtxIOswxHE7v+rWFkkMK/UeOMxFInq816BG/ENbi2yTKFs2SXyR2aTfxa7miv120gpxqRwEKftx01gXGr7JCT2CSX1LnlxkLWbBvUnbVIuGQ8yn8dgGU3Srx7BrreUcHBjMRfZAHIghG+wamYiJglY1aIjax6FHwoYRdVEmshnVipxRE7/t0vEwtkfbnDAqYZQAah6epci9fU+KHRXe+6vqZY1GeGZ4sXAf8Tgxw0eN6YY1mI8sHJJ5J6nTANEIe91D2ECpT5ImAjmEqK6up9dBF0+71BEkTI17tAZOT993dNsWvbdj6RjtDD/v5Ko6dwN2sTphd0iirJyCC3YgzRtzPPiwFT6QOniGYkJ7v/rR2uTV0ZGusjhKyTa1B8znyXY6laflPkWreVQck4yJlt7IMMMPu6gNeZKTUXveFIR2geyBwky8DeKVPQKlzU+wLSeortoCY3lGMVpZ0aqojiqjIhM/PEwkdk+S0mX4zQaeuD0cDvpNkUTsYaf9oNHAS3shQlkjmdEUkprzoGNyWS7YHFMEqdj0KXfujsJx20W4N4GknwcZeNLHmVuWwsQN3B7YJsF8Y/KW21yXegQDhIc/d7LqnDe/vxqWp1x5bYkw5DFIDAXUX9QxQkPhxKnvnCwXsGS75+tZkcXGKpwDAUXRI93sijW1OWBmDI7TQmRpt++Z9TKnjXfGHuomuZUD9mSaGzw4lM5GRLmguVVk1iobMyu2VrBVZymfTtZZ412Ci9OFx2IKLbEbdqVbBwtXMj0isjvTXNRH46Ddo7XsBB7zG69a8lK7jzHuErY0Gb6lp8ZgD2Xuo6xEkgErku4BG707ZkWhhd5LRdzuN6TQT946SJFEAzyIZyC8yM4mGTaCfkGmz8RdubaKlC3bPMkiI7gS4GfcQR40mjQKiPYy7+FdggOrZYeG8IT7SsUSTloui+N3P3Eftis5IMRCd2WGJCf8uvITlrJPek47luZhHvUwG0pw80tgx16WXzq3n9ZjtZ2GGNOFUgZHqLVVn1zsjDjXq8beLFi5Yn8ibUBozd38LHjKKNXpvY5qLNeV7TWDUQjHSzLbq0fvxH5EqT3IAxMbIGjZbF0YWO1xMlwF23tpaBLntBzDtU29LtpzCg35HAomMhr1R43tdIY7IkuPjqlBoGNAzxNVNb3Nx6r/OLSp0fTE8FgmdfE9lkOqelKO9O56W8mxyUKGxciRLiNcW+wiRnA9ik9ydoGVlFiDJxHAZqnLtFVfPTo155PQRdEL4dosWiExdUUPMtOOJumK9FWae7EUMxPxynw3MyKqnIvQnpR02B3jx6zCYMP0XBPoHKs/I0jesORyIOHEc/6yQ+iTWdNaUQbDtuO3ADeo7oh6DwnXAkgxd9mzHyoBQnRMzIVkdPbOqrEHT4hnCS+9J63CZw97r0p9zvmrx3tH87JTs8MCHIoqXyw7GCIj9gZbzYCODnp77NF1XurNlNQGXNXM1Kn1VV4/uAlB3/2+denedYBE1w6bxOCuwvI+Kxau/ahbNAx3jnc83R7aSWlnWmgc3MmvJkFfW46sHqV0ExrSWlR01CZfdfSLcu+1WroYqXa2OMYMGo1pU8TK5uTyKK7zJTyGvng/XQ9nC1TmXqrsxrM4xAAfMuMlv4/0iZHJGJ2ZWPT3VujdleEOwzHEpjrFf6xO1cz2nqd5egsqVLB049JifXIvMKPnq3MylfIVeyCChlFRQkTepX107ukGdJ6IXlfsjKJa22LmKKECaa0zO6taQbJVqoRsFFDKTSQNg68FK+Y4i+N6TuPstjmS2YkaTqdSOhhDDb240SNeXbnVDj/A8ERNR3nnV49TuLi4FjEcFCVmWM6BeK6LhI5VoKTPipSjhwkElH7XiPNju6ShyBgs4QqjrypXIlYmI4xzqbYT7lDfC014DEdjzfeyOrnmmVH5YfRFp02rLGvc1t1nfYOpCuDK49ElLMxdasw1elspquQRVej+gvRBacI8DLcbDgGhQRyWJIxn9ndoDgoqalfcPm8YI6sHJUG5CZQWVu2asakuY7fJTTFa/mnJlyq6MAJF0ihgfJjASYmog5RrdbjlkfytjG4k2F7VRr+xTy458Pl2RLKAyftMi0Mfh51iNeIwBeSNQq+d7G/bkS1Oh/QkfpZKU2rfQuOurkLpKbxy1ZHY9aITY+klvO+5K0FcuFpXR3/ASNgCES34Z6MV2M7TtdaTjjqNW68QePzAQ6oArs48H+eJLcgNoTEtuFpAXIvDRQMcOQzb1rF3t78p7i6jRdgt94LfUu1ALyQXIDDERo/6PEwkgfJDqSYCTZ9hU2CegKmP93NL5WhhCX3IdlLmb1qvn0j93OY684gr7uZHuoPc6DLcC9d9GTNT4XoJYzmihBOWKscIH5wO/eDeT6fJCfTB1CSACxj5gSqXKVIpCxyWnRyzVefRnVp0FgzllPReWtAPXuNGOFK61j4MaOdS9RTC94NvmaSqDU8BCcNhwoYoVqF70ygXdKH3NiaD0KSFvHJgJj9xJp/EmRpmccZT3Y44kfXIRSMmOSsrWrwVd+sKTcePbq2FRmsVdw30V+pJWdfKOqMxxZI42067piJ8QDWlMwjlSO33EdaSnT4YeqbR652FficFY30K9Kcc0VcJI7hxP6NEsuysI2zGNSmne+XUrsIIj2y+O2s8A2YFmscPUXB/ZwVxzYHMNFY8RqnCuy5s/XJWws6GJD/tKAfWO6qlylb3ZK2nUs5alUYm7s0RLvAaBsez3L1aBVMpnbSR28tYHcvzbYBCPMAHnY8QKYmpCmZ+ShueInvp46g89awK/NMF9QH7ghA2ayw35kj84ErCeqR4UyZCA2vwKjpEx+5SrPez0ggpkRV5ea6PlzlKdx1wpvX98cmjKsdz3V77tryXm64ka4AnFEYp1lE1SPE2ndxUvF8XcMc8ibdYTzDWgn1ajfcgj2p9mlVcVpDb0DqNOi4A9eI6lba4+gp81Y0n4iPpRpR+NwDONPmuuzL2KLUw2s2sGaYZ6EzosHOn33cwDUEdJUigT3m4wTL3soMEhhaFAh2ZCwTMqEz83eSMZeI2vbtit23nyMNxlRsbaobA61gQfVX3oDGXPTWfZwarVJYDWD6HOFBN1dFVbPd6Cdqk76Ao5vNSImuY/ThdaGS3Lp6hAhr0EariYiZVFIkquWXyZwoblevG98NuHkflLngWnqy3Hnuw0IkmPorHl6gfiszfcUZ7GDXDUhSTyWAuZXCdzgxZERdWua9mJNil+LlFNCykFDjuiRLS+YrBAAdpzIt6dqCcjkc5NI6UoorDOeMuFwP4qQMed0cmnHsE2eqNPiK9xk50dgtNpj8XyyM6H4Y8ICLo5EXKUYnmibhcSFxzCJMD1HWg9hzsYFRTCR9cF14YaDxzCa4YNN8bU6hOllRLJEQmTl3ZyTgP95K5Wl5GdekGYNLSvOunyUk/uUUOA56TVMEIY6w2JORtQ8mKM2KfmciY/YizT9tVYqSEJ5XLPjNPcVY2qq8/WfQ1pe3eZ4LmIaskrYUYNq4tdibddPGuMf6UEBs8CnpxOFo7oITzSjse/WvIsiob0DR6Fx+4Jtq4J6qKiw2qhHcucufXYB0BXtNqZPaxGvLBjHH7kN1QBo0KXdlTZ8RvD8A377qbEd762N0R5cwdj9osmLe4dNnaX56ScbudfjxdRap+Dq9PnRPMO7NSki4NkSqWXX3c1fph120OblSY3U+o18i5ffJm93G/dJKMkzuePXolJIz6rswLmYXsgojd8RLyimxA13kjFDNzLBUxFlGr2/ooC4u1Cjo22lUKTKhpjld6Sw1Wa6acG9TVacK1eZTVxcc1XcVhnood09oyR6GsS3GBPnhdogVXn/KgaG3JaF1EqneXj1h0U90rmWf16D2I297zJ2bH9tn1NgUVdj8cUkW/ry8JBYX0lThxyLzLZNIyUP3snZVUqXA5xmCTCnXQs8lKO60fmfPZxz0uWK/2HPINc4Ee5NT2Y0qUKhb1TXdtVC7EFdwmCy+OSL+0DlFjujueTo78wB4P4iT4zXV/5OXkelGlcIG5glLzoVwnDewa6qoSrYPRpP4Z7TJ6ALsDXfCDqsGdx61J6/rR4duRw6C+5zsGWMRaa0XTZ1EqZINk97lgILbulGc1sZ9iIX8gHCimhGO0uDp2GtraJqKtmMh0npxO5jzaYWftNLIqzO+gtBB0iS/HjXc7maNeoLumXzoJCXAxXLdO2YR+fVy5ALu5Uecq8iJmyAm5GxgA3SkVEQfZU1blE27GjsaJjAtGJY7zZt+YKD3XfX4b8g3mv0j3oem9RnfLDEJhNTpR2ZGwz76liZV65CUiiXaYrQrJhRAdwRab9nwfFDmO9xJxGHdqfYuycN+3D2bB4sxEMRTyqBeKtz4/N9o68OeQpLOMrqA7VhbMf9LBY++aKqNQfjoEGbYO7KWPF11wTjC0cw4YRmQ3wpvvS/eg1tw5IX3uyqo2w2v74am1DuQupoDfiiABmlqWAEu4B6JnjdLkh2kKpFoMIsH70bg+5ZTv1lPiWvat0WPX3ZSUJmtFxFFWgXJHzWMN8yR8ssEcQwK5A9CU0LGPOY+5fU5xfHPUeatNV7mEbJ5O7uyVLAHD/xNKXOOzqjcPjB463LWBbvYa55gPvme0ZYMfkaAICRznDckMr7pjlaVMVeOAkhWgZ1GlTlhdCyfvxA9nXPUcpJGSaWZsX5NQp1HIija7Wbr0ONAn9JJu0boJTfCYht7rhNBBmdFtWCHd90pPHclHMAwGCTi7ZY6Nrfm2dlRSL69cE+g15nYAen3BOiUvFJyypMXeuHO3ndj+nEeR7/Gbat/km3Y4jbumvIQC2QHMSsXcb1TQjWPt88t9s+7uZTb3ZxAJanZR2PUYDsV2wCeGSx4kHu/uyIlfxlFow1YIDo+5Pz6lORbZJKZFHdLbMuaX/bLUALmwx2m93EaH8XS14dkd6Q9RspEu5e4OIcLk1QNtDqEZihuB2Vt+g1L2nJFArVC77gjIa03oO9FTdeXbnFfiPVS0nOAMMA9fFidiKLX0lMcBw/WQ8utgp54IrohKBWUCnjscIsaYqRQmK+lRo4txxzJx6w8gHgdl4GIHEr9YT7nCu3EKT2ZbCl6B5Nlw1MvrzLFidz9eh+yBqRMQwFKW885x4PZXdTeQ0/FaC0TGngpOSXBSwY397dR/0oji8miuy2po3kkjjIZwA4A4qOv5oGWaOiYbl7CF+GJMPDC7iyu1zKE9KSffm0MTNGFEBr2fME3GNEaZkoXbEI8KyPHpYSO2py0VeJoZyZnR7EXeOkfVTbUfk+ujDbTkvTzm8oNCx0i9lGfyvCLbiCYjm139/Y6Gauym1rAXDhYQeVTm6OPCr3pBduo5zLrzPB826KJhskMUFrZN6IBxmArytt9Ns7KfVtZWTE2ngNNyqnU8jvx6OFAMyjun+8GxjDxAu9TCpnaLPWHpJxtw/7nF5XulPphQ8qjhwsbB2ZK5gd2robXw86q3g2oY4XWIMFZbYSSIutvUwrQvr01tYzhebOVxZ7oX43ZqHOfBYzUdHqJWlUNXj1wV34RNNm6892ijwsgHLDkj0DxEiyxrjRhwjfDCcKRWrDJQmXBEAys3yurxAi9bCbIcd26Pp3CTo9bwuicNYQY41D5mbA+HI8/zUhxTO2GBWUKx1h8oURP3XfxgN+HeTNb4cFJGvYCoiNfQBtHk8+linVT/4sXYAcsuTZquwFGzXL4lo8/RG9ttFnTDc8UI231THvomH90yrfeWnddsKyakuEUWdMe4E+rYRaUmCLz1ysqAGxocvVl4fY9EAhUvmNr47Owinr3nQncbu3VrK1cq8ggZxoi9ozx6t48nnzLoqiLTK+HlS5H40ahqExvcBnZw9UEM01HSHFcw11Lcnp4eLzMiUJcgOsN8+l0pciV9oSDTDE9QAVqJvF/PNxd4wxMFAo60OhfMTjWfksiKoeqhvsiEASU+TET1lV/i6FHAbUdkLBTnJ5oqGlEBgHOlsysNrsLxV6tE+RJYDXIXK4jy6uk5G3t2OY5TOJfj+x27NxwXKTjHesBw00p7mH1v9VFiVHosIBqSC3vbegb49mmz9/RKXUA4ZvvN+aIaNz1UlYu/VEh20fe4frdspZmzpj7wkxwvpUfliuGJ9U4dHfU6g76Jz2MBgs/4JGRMy69Otefz1obm4PSblRENRM6qc9qROKlFnlfFUcVXPejRnI7Vleq1+thmapZ6uFWSRzbfA7UmRw/OwoXy7h/asJpXRQ9w0lSg4TEEXWa4RW8EDYW/aXSyveuy4DpFYd4/7EFkpUyf/Ck62ryEoAy1lD4iopPvmh7nr0IDPO9paaU1AA69dGHYuQcBlcEonvVAfBAh3lrHss+mneeole05ieNugMqU61fBr3hnemZ3pkM0k6mMb+RlQDEys7QWploExzZnzDlo6EOVcszXRjyPRPfSVoWSt0fTeXi7q0WOtU5kPr3qZUscrX0oK0kepEWZ+QKRh1rI+gjLm+hEFerhAPHoDp46Gg0y4DhLTuUkKG4RUicpoD1dpvxYym1sHym+hKP8/QSUsNejswJkRY3zPHT8F/Ka92fxJih7wlKmyIMa18WmaLsbaTgn2/UmeIAYYSxC3ebCJcQuuVSFHZ4LlOTRLcF6xXMW2T3z6D6dfU1cYMZ0N4zio5NvHDmCazi0HCLA28AgjNpKbqypVLvI2gNIvpKqei3GCAxIr7l11i4N+PPax8HD5GRN6rRhwJPNk3Cu8RHFXm1Z5oAkEHD0emjsswM9o3bA5V0VUBN1EtZgYYsbhiV9ZuBlFIxMCHBx4jcToY7LvbP5uRV3BJ6zwo2yBTXqQ9xG5Ym/2WJfuumOVHy2Ws5jtHcXyr+36RG5N5EcpGoV7u6Kg5k2IXt42l8Nd7EJ4bq59qJuGb2FFDgHPqxJTtXBhGywnNgJ5TRWOaHFkGnqpcYt7fKw2+TQwS49aLYDQuqZiMYpGS85ht2flLmOXaP2MWE7m9Wh2m/b9ERfHD6SdcTF8PLKFgoSmndFbrYdQ+PQaeWKsNpC5+zPytMDM9pzjkLm9wyUkELjbO4xCRap9teAu2+zflgyRa8OIU3w9IGEfE35qIsZSQ2og1MvXM8jO1aUFU+eRi8A3usuOpI6qSTlF8zgeRdIX3xuqxbqHUrJNAC1umt08bb5Fu/ao92oZzQzyUWwYvuoxdxpIrKsoCzXiTdLYnxHiyf/WhazNAtXz8Ov7d1d5H3jXypnh9GPlR4HwrM1PN+RhFKvaMsJorEI4xK7fEMvYgloIkJB35Y12dkP6LMv8lrjWNrZmhcryaSzJ3n16hFrH5jHEVrAKoFOC23npeV+KMrgdr/Oq1gFj5VNLyqQ7+ToCddHdqu1fOEHbtksTm9hLgQLvMfSezXbYAiG2ORJ6FrMdLDowEy67KxSLeRtdY7go36Tr083fsMRBgU+LnPq/K5fM9ITPRfOBeE1dXJu+YOTYu3QI35JbZs6thlT+ee8c0a8PPpLgxT4lLtllkzDinXMGq5WXh2iw25V7BFaTzHK8mwlCCI8sCSTks5KZw47hUBfHJQH428Zeq0xvdCKY5LrlN2nh1r03059mQpiVDyUBWVteFwvJBF31tOTIM3WXczHUwMoMi8ocSPw/dK4+fFWn1L62o4ciOjUA5wVYMAMiRYkWe4FIDIJCG3udOa++u3pVjCOT1ZLP8pcdOYbKjPWO3CZuBrUlZNHyqMmy9ukBAfP3+lFiIymaKGNi/QhUrIx7U2czz+Asmp3196+BbbEdjCfnKwg9qkszlRiNzgwokX0UVtSDak120R1XeMU9h3mw9SohHPUQHvu0GS1R5o0w8Z3w40+HmIXaMzED+7X6y1HMxCclbQt3IxJYmAGjVNEGz9uwFlduIUUjkbn9cECM6/YAicf1WtVFF65ndPCYzZzpxnu5RKaKWmf9f4coijLbT0RqMCKEuZYuBxj0waO2NnR2Lu9is1sVQ6FcfO4dtt3XFA6UsTaI3D/0qKXnSfeRsGkHudsYNiFACq7z42He+eZjH50hnOSM44xMBE7tDD9Tdq3TuVCuyJ5MDAJ2SUfH+H5/bHHdgYuGNEsohS2rRXf2EmVySRvkSJwtQ9jN+dpmU2DjxsUzhTsRj5ucy+JTKItGezvwAlDHpJLQ1MHxtVd+OC27c1TAyIXvhpuSGdMgTO1k1mteFOFiNwTRQ0BgeTVajc9Mpyoky5n3dW814avyuntNh/T3GUwlMP0m7sQp8MS9t0sYDeBA5H9UUD4gRJ99WG6jzDA27HSOdF/CoKRG8z1G/N5I2W+xrw1mCUEiPRgac02akaJY5eYUaqQ6ObOXqWr1oaoMpV0QwqaGGsXezCI8XANTyIIlDx61WTlBkgPqT2dCHv+xmryCoWVFS3qrYypdn6ol7EmstMD11uHd/IHMCac7NyBJDkPlWZHQzdcugybrY0muYFzgrAlZSNUmM6FUnvhtCvMivjKHBXHe2VrdFNmgGg9WQ0Dq2G2mSkIkrnPaCussnNDr84ORufcIQf6ibzYodNPQa8K5p6vVUMPN6CMtspT7cBrqHa8Ifc7CCP4PMeLrRJrMi/nOwnIw3eugqZWax6ivTd7T1EeD1tn2mcCVi2PbjQfbFtdGqowEdjtya0UBXprLudQwovDhrsm33qjq3E4ffW73hCrPWrFPFBAIvH1g4CvNmC0ciJ04bAj5nNI24xQ+dUOAyrbRe3a12aU7G0t9YVHn/vWXvZ9mkXDu/hY4psKJ0Ly2DZkGCUL7DKfIlGtqebI4TJMR53EY552IbeDsd0KU74yfPggYJQHLCE4mDrVKXWQBuJVGMfUmzPu+DElLgb3cFoP1cNbiqd5pUVAyAbZqtr1brc0j6c4i5qHGsfHvIPCyISaax9u9IMK6EJ7NMVM4B63qfIyIVWN37QBHtWijrWalZaHJHyu9qDGCyrUTZuvagsncyDSkpGVrccwaMfuaX2uTm7uYX3XMwNSbKxgMSUMLvdNXMSJ1IQBSpLEMUo2Ex9HCT4H8Avb51crUl3TYnxr8smZsFEhvR/v4W2vnrWlJrAxAAHmzojuWxgp5ShIflB0dirtNWHn8TUKZ+kKSYu3G6mRWt/EstZTt8ykH3iMs6zDFMuJipFmFAy6Gw82dbZFLOKNgc9RRTMUa9U0xMBZz5UYqPpO9NlcL0mozHgSqgavdowREmS23UvDLBHz3gpX/6yZzV443Q7dpOnqTVNkNy0CTH1sgyS73j735uC6bEhHd0x1cIvKQ7ykQXuUq3xIPjMdB/LRovenHXA3zgHX+Bd3Q2F5NWMPPfPNZUE0TJNn2Fsiw7FZYuwZXWvakRIPhWvtMhb42fNpl2cR7NAtJjOsSne7Bt8xHZWe5tA44T6NjdmEj51ibjfs0NNAV7vqYXK2EMRiMDvSJ5chytUTft2x3TIGuB310VFjQpiIaU4nOaLF4/6cqe4BlWj8xhrk7dbPPTs/rp13UOXsSRA0/qk+BponbA8G5hGaw/0Y5cuT99rRtVUhJ5byjsA/Bwo3e/go10qhnPdXD1tvxNNzYvG8R7KuOnfz0xPzwY5HqUfGnCN2JsnwfofPNk9ZF5Lx+XO4MM1HUW/l2BYeZSOy9o58WfZarrujNKebgWBwJmV83OfY1dY0psKuNdFRRQ0j1AMxIxaIMzHOgkGm2RbfRv/bDcO6SULaZtuOGA4qlrvAUIbdYbJG5doIYqi58i6xlXyJD01pA3GmsTWdMAcinOKG3dsdLi5KOkm9wNfgiuI6QhmKZEdLezC2S1iH57TpIR2UOblv3LnQ5vLcVTnZHB5SnhI4TDh5t+6RKgOTNcJ9JRwJjWsUXG4ekW82QnxyqCKbw/c6vs+6mjjyAaWRz39evdSMkuw7r0/R7AeKfOdtR/wHvT1Fvl2w7f/w61R/9m3o7/861X/0yiTx9nVoq+4K4DkRz63y/4a3lpJhgEuKcx9hOM/zhxm2AFb/Q91BLw6XzX16u1sM4OwtcYYLbgCiBuP8awHMDW7UfuoWvz6Vfcia+DstfoThHzD294BMf8DfAJlFP7y7Rtt3eA3yECyTll9dNq5DfbO9MAjtX1+WSPjCBvYlcMHw3diwm1L/n2VI/lmG5H9qGZI+LZsi/C2EVv5b/2zjP+z1Z2AKX8Oewah3lkAj31n/7MctVUD/DF/9rfXPPu37y9Y/+72VSv76tbD+s6H8Kcuf/bze/5stPvfGd3I6iDUQyR3C2V3/8Zz/RZ7zn0XK/9Bzuk36W/xs2t9pHR/6a+eIMW9X7PpLXSOG/2/w6TsrO/7uYoV/E0J9u3Ti81cfjLApADAG+ErNNwOTdyngRwPwHTzhMAjNnw58vd45y9EE+g4BPeckvgOmKOIVphj8PcFJvbMOHvWjskU48VMF549cpPXfh+h7C239N2D05bs//+XRw1+aNnw/kkD+pOj9m6zChFOvPjDw01dh+t1ufe9rbp8/5vaxAA78L/AzhC9jT7Vj/fGAFyv4ougLk38phBf49eOHtThwAOjE5ePlP5308YZn4/5yT9Coj7f9uirw+cM3vzX3NXz+4ON938i6vqxM968S2b/wBYrX6y2jJP5OivPlsxRf5Td/1BKsxM91PX8lW31fV/S72eK/3hW9/5UIhnmDdb1LJxCjgEI1HKAO+xOfcvwndvzhsePU+N+mpu/82Yk/9WnC975M+D1Y6N0PtqLoG5v7C1joz8uXV3j+N/XMWxIx70FxqzbZkIyWvlWKLeParz9Vvb5dJVb9vETsP4Hl7342jvoaVTj7FlV/aVj5lvw5MI4r6Of+n8H8F7MEP30wceznCrUfmCP46ku//4FK+y99RPX2KcnO7TyAKAzZdWGQwlDol6evAuP8zi38sXjO8wlhE1aBW/nvPEn5u09FKUHD3A9D09Vx2H3o4PSysSlqN4Bza2GgCCedwtdnqzoIf8v631Igmn714dcIfn36te7SOAWm/KGpvteEFPyVLCKZt8EZ9U5wRv0o08B/iiz60TD9s7PB/mYwfTsZDHjQ9EkXqWPpwWXyP8FUCiu4tP4/KP0LUEq8zLX8WSj9wy8r/KO0fn8K0N9MaZE/RWn9PXLuL6n0/5JP3hH0q5gLeyaDH5pBf+mkf+a1fHGHf+a1/M/mJr/7vJY33739K+e1vJuA/CnTWv7dpyDvUu8f8v23c41f0/27/YP9RdL79yr5Bd0qTx+IB2UHiIzIfW8W/j/a+t9IYZJf4/KvjIDftbuf8nTy38PXt3HzJ+awsH8Rvt6t5Ht5Ynh3vqj93E/ctHozCm+s/ovu+1rgCO85shdQvvZiL0hJy6cXjNOnFbW2sQthC70vqiNysPS3zzX87TnI+tBPn8HwxvLfGcTfmXyJfQ0GiiHfmSj2kij+6jkZ/YNG6p23kQCVDTFw3MLbx7Z/ewr8yHcf5jRPP7Lhx1fS4HYDt59eTyvLunqiRPgXrrH+0uK2+C0swiZxq+H7jTpBEB9eAvrP39Fm3xl4nP1A4++M/b+OUrDZwY8ufhGkQPF1BrwPj/h/7Vldb5swFP01SO1DImw+85hkaTetlapl2p4dcMErYGZMkvbXzyYmwEzUZkvSqEqqqHD8gTnnnusLMaxpur5lKI/vaYgTA5rh2rA+GRAOoOuKfxJ53iBwZDsbJGIk3GCgAebkBSvQVGhJQlx0OnJKE07yLhjQLMMB72CIMbrqdnukSfeqOYqwBswDlOjoTxLyeIP6jtngnzGJ4vrKwFQtKao7K6CIUUhXLciaGdaUUco3R+l6ihPJXs3LZtzNjtbtwhjO+FsGPJPyl+243Lwt+I97j6+L6deBmmWJklLdsFosf64ZiBgtc8OaPNKMT2lCWQVbvin/BC6asxDLiwBxVnBGn7ZUWQLp6IMW9bSKlSVmHK933hLYEiVCDNMUc/YsutQDfF9No8JrAICtkFWjlldjcUsp31IgUhESbadvSBQHisc9OLU0Th8YjRhKUyzgOxxGmGksNyxKTlcx4Xieo0C2roS1BBbzNOmQ3BLDNC3x1eiXfSXBRMTzOCFRJjBO860or5C/O2J2KjLq6uHoYgDQI4brHksMRxNjyjDiWBOg5ukOLXDyQAvCCZV8LSjnNO3y388pUmeBIFAo/LdMGc2wtBFJko5yk9kNlJ1jlMulpOtIptIheikZHoreclBOiZxzthRTF71OA0cUFUAwhE5HWBfoytre0NGldY6lrKsrS9McZXLh4yAQfuItu7mJWM9kIY8ieXT1naGseJRN5hxn4fVHdSS0z82SnibcuOQxZaS4uHIvV8Jh15TO+5sS6gVF40q4tzO/4QCTJf6w5rTBuZkT9GyYUjDWn1Y/piyOe26y1HV5S5Y5yaJEVpSVX1BQZcbdgoDXBWknQZUXQ1TEO2p72CfhqPqc3kddvSxLF8z1ewQDR0uEvqbXl6IopVxXBhQ8mKIMDQnXM9tlx9unDrW8ni3PH3qn3PNGmtT1DiacA81JQoMnQz7vajvd4BIHB4oD4FnvXfqAvgcStXM2yrckdn+XtIIpCzEbBBsRxtUU7GowaOPXFZ31iDqAmv34klT+ea8fvS2pnDiY9KTS1NEPjKSougc9p1xC4gCPzN45hgTU3yteSsCdJWDfe+ATl4DQ/n8PXyqEQzn4DCoE2PdsXQeEetXR80LkktIPkB3s0waEOG1+2avaWj+QWrM/ \ No newline at end of file diff --git a/documention/imgs/Ledger.png b/documention/imgs/Ledger.png new file mode 100644 index 000000000..7811f94a5 Binary files /dev/null and b/documention/imgs/Ledger.png differ diff --git a/documention/imgs/Ledger.svg b/documention/imgs/Ledger.svg deleted file mode 100644 index 69eecd8d4..000000000 --- a/documention/imgs/Ledger.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
Programme 1 Chain
Programme 1 Chain
Carbon Ledger
Carbon Led...
Credit Issue Block
Credit I...
Programme 2 Chain
Programme 2 Chain
Verified Block
Verified...
Labeled Block
Labeled...
Transfered Block
(- Credit)
Transfer...
Retired Block
Retired...
Credit Issue Block
Credit I...
Verified Block
Verified...
Transfered Block
(+ Credit)
Transfer...
Programme 3 Chain
Programme 3 Chain
Credit Issue Block
Credit I...
Depreciation Block
(- Credit)
Deprecia...
Transfered Block
(+ Credit)
Transfer...
Owner Change
Block
Owner Ch...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/documention/imgs/System Architecture.png b/documention/imgs/System Architecture.png index 96063842c..773e60598 100644 Binary files a/documention/imgs/System Architecture.png and b/documention/imgs/System Architecture.png differ diff --git a/documention/imgs/System Architecture.svg b/documention/imgs/System Architecture.svg index 41cb44ef2..6d85e79c8 100644 --- a/documention/imgs/System Architecture.svg +++ b/documention/imgs/System Architecture.svg @@ -1,4 +1,225 @@ - - - -

Data Stream
Data Stream
Ledger Database
Ledger Datab...
Storage
Storage
Operational / Query Database
Operational...
Government / Project Developer / Certifier
Governmen...
Operational User (Root / Super Root)
Operati...
UNFCCCWorld bank
Email Service
Email Ser...
API Gateway
API Gatew...
Data Replicator Service
Data Repli...
MRV
MRV
Private Network
Privat...
National Service
National S...
Analytics Service
Analytics...
Carbon CreditCalculator DependancySerial NumberGenerator Dependancy
Operational Service
Operationa...
API Gateway
API Gatew...
Text is not SVG - cannot display
\ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lambda/services/.env.dev b/lambda/services/.env.dev index 797ad05b7..23ceb50dc 100644 --- a/lambda/services/.env.dev +++ b/lambda/services/.env.dev @@ -5,4 +5,5 @@ DB_USER=root DB_PASSWORD=abcd1234 DB_NAME=carbondbdev LOG_LEVEL=debug -carbon_dev_common=carbon-www-common \ No newline at end of file +carbon_dev_common=carbon-www-common +SOURCE_EMAIL=nce.digital@undp.org \ No newline at end of file diff --git a/lambda/services/serverless.yml b/lambda/services/serverless.yml index 8af3c0f45..9fcacffff 100644 --- a/lambda/services/serverless.yml +++ b/lambda/services/serverless.yml @@ -42,7 +42,10 @@ functions: events: - http: method: any - path: /api/national/{any+} + path: /national/{any+} + - http: + method: any + path: /national ssmToEnvironment: - DB_PASSWORD - USER_JWT_SECRET @@ -60,7 +63,10 @@ functions: events: - http: method: any - path: /api/stats/{any+} + path: /stats/{any+} + - http: + method: any + path: /stats ssmToEnvironment: - DB_PASSWORD - USER_JWT_SECRET diff --git a/lambda/services/src/analytics-api/aggregate.api.service.ts b/lambda/services/src/analytics-api/aggregate.api.service.ts index fd9bdb9da..203b0b18a 100644 --- a/lambda/services/src/analytics-api/aggregate.api.service.ts +++ b/lambda/services/src/analytics-api/aggregate.api.service.ts @@ -15,7 +15,15 @@ import { AggrEntry } from "../shared/dto/aggr.entry"; import { Company } from "../shared/entities/company.entity"; import { StatFilter } from "../shared/dto/stat.filter"; import { ProgrammeStage } from "../shared/enum/programme-status.enum"; +import { Stat } from "../shared/dto/stat.dto"; +import { Sector } from "../shared/enum/sector.enum"; +import { + StatusGroupedByTimedata, + StatusGroupedByTimedataThere, +} from "../shared/dto/programmeStatus.timeGrouped.result"; import { TransferStatus } from "../shared/enum/transform.status.enum"; +import { CompanyRole } from "../shared/enum/company.role.enum"; +import { PRECISION } from "carbon-credit-calculator/dist/esm/calculator"; @Injectable() export class AggregateAPIService { @@ -28,491 +36,1543 @@ export class AggregateAPIService { private programmeTransferRepo: Repository ) {} - private getFilterAndByStatFilter(statFilter: StatFilter, mineFilter: FilterEntry) { - const filters: FilterEntry[] = [] + private getFilterAndByStatFilter( + statFilter: StatFilter, + mineFilter: FilterEntry, + timeField: string + ) { + const filters: FilterEntry[] = []; if (statFilter) { if (statFilter.startTime) { filters.push({ - key: 'createdTime', - operation: '>=', + key: timeField, + operation: ">=", value: statFilter.startTime, - }) + }); } if (statFilter.endTime) { filters.push({ - key: 'createdTime', - operation: '<=', + key: timeField, + operation: "<=", value: statFilter.endTime, - }) + }); } if (statFilter.onlyMine == true && mineFilter) { - filters.push(mineFilter) + filters.push(mineFilter); } - + return filters; } else { return null; } } - private async getLastTime(repo: Repository, tableName: string, whereC: string, timeCol: string){ - console.log('getLastTime', whereC); - const resp = await repo.createQueryBuilder(tableName) - .select(`"${timeCol}"`) - .where(whereC) - .orderBy(`"${timeCol}"`, 'DESC') - .limit(1).getRawOne(); + private async getLastTime( + repo: Repository, + tableName: string, + whereC: string, + timeCol: string + ) { + console.log("getLastTime", whereC); + const resp = await repo + .createQueryBuilder(tableName) + .select(`"${timeCol}"`) + .where(whereC) + .orderBy(`"${timeCol}"`, "DESC", "NULLS LAST") + .limit(1) + .getRawOne(); - console.log('Resp', resp) + console.log("Resp", resp); if (resp) { - return resp[timeCol] + return resp[timeCol]; } return 0; } - private async genAggregateTypeOrmQuery(repo: Repository, - tableName: string, - groupBy: string[], - aggregates: AggrEntry[], - filterAnd: FilterEntry[], - filterOr: FilterEntry[], - sort: SortEntry, - abilityCondition: string, - lastTimeForWhere: any, - timeCol: string, - timeGroupingCol?: string, - timeGroupingAccuracy?: string, - ) { - - const query = new QueryDto() - query.filterAnd = filterAnd; - query.sort = sort; - - const whereC = this.helperService.generateWhereSQL(query, this.helperService.parseMongoQueryToSQLWithTable(tableName, abilityCondition)); - let queryBuild = repo.createQueryBuilder(tableName).where(whereC) + private firstLower(lower) { + return (lower && lower[0].toLowerCase() + lower.slice(1)) || lower; + } - if (aggregates) { - const selectQuery = aggregates.map( a => `${a.operation}(${a.outerQuery? '('+a.outerQuery+'(': ''}"${tableName}"."${a.key}"${a.outerQuery? ')s )': ''}) as ${a.fieldName}`).join(',') - queryBuild = queryBuild.select(selectQuery); + private async getTimeGroupedDataStatusConverted(data) { + const passedResult = data; + let result: StatusGroupedByTimedata = { + awaitingAuthorization: [], + authorised: [], + rejected: [], + authorisedCredits: [], + issuedCredits: [], + transferredCredits: [], + retiredCredits: [], + }; + const groupedDataFiltered = passedResult?.filter( + (item) => String(item.time_group) !== "0" + ); + const groupedDatasObject = groupedDataFiltered.reduce((acc, curr) => { + const time_group = curr.time_group; + if (!acc[time_group]) { + acc[time_group] = []; } + acc[time_group].push(curr); + return acc; + }, {}); + const timeLabel = Object.getOwnPropertyNames(groupedDatasObject); + timeLabel?.map((timeLabelItem) => { + const arrResultForTimeGroup = groupedDatasObject[timeLabelItem]; + let authorisedCreditsSum = 0; + let issuedCreditsSum = 0; + let transferredCreditsSum = 0; + let retiredCreditsSum = 0; + let resultThere: StatusGroupedByTimedataThere = { + awaitingAuthorization: false, + authorised: false, + rejected: false, + }; + const statusArray = Object.values(ProgrammeStage); + arrResultForTimeGroup?.map((timeGroupItem) => { + console.log("status array ----- > ", statusArray); + if (timeGroupItem?.currentStage === ProgrammeStage.AUTHORISED) { + authorisedCreditsSum = + authorisedCreditsSum + + (parseFloat(timeGroupItem?.totalestcredit) - + parseFloat(timeGroupItem?.totalissuedcredit)); + } else { + authorisedCreditsSum = authorisedCreditsSum + 0; + } + issuedCreditsSum = + issuedCreditsSum + + (parseFloat(timeGroupItem?.totalissuedcredit) - + parseFloat(timeGroupItem?.totaltxcredit) - + parseFloat(timeGroupItem?.totalretiredcredit) - + parseFloat(timeGroupItem?.totalfreezecredit)); + transferredCreditsSum = + transferredCreditsSum + parseFloat(timeGroupItem?.totaltxcredit); + retiredCreditsSum = + retiredCreditsSum + parseFloat(timeGroupItem?.totalretiredcredit); + statusArray?.map((status) => { + if (timeGroupItem?.currentStage === status) { + resultThere[this.firstLower(timeGroupItem?.currentStage)] = true; + result[this.firstLower(timeGroupItem?.currentStage)]?.push( + parseInt(timeGroupItem?.count) + ); + } + }); + }); + statusArray?.map((status) => { + if (resultThere[this.firstLower(status)] === false) { + result[this.firstLower(status)]?.push(0); + } + }); + result["authorisedCredits"]?.push(authorisedCreditsSum); + result["issuedCredits"]?.push(issuedCreditsSum); + result["transferredCredits"]?.push(transferredCreditsSum); + result["retiredCredits"]?.push(retiredCreditsSum); + }); + + const resultS = { + timeLabel, + ...result, + }; + return resultS; + } - if (sort) { - queryBuild = queryBuild.orderBy(query?.sort?.key && `"${query?.sort?.key}"`, query?.sort?.order) + private async getTimeGroupedDataSectorConverted(data) { + const passedResult = data; + const groupedDataFiltered = passedResult?.filter( + (item) => String(item.time_group) !== "0" + ); + const groupedDatasObject = groupedDataFiltered.reduce((acc, curr) => { + const time_group = curr.time_group; + if (!acc[time_group]) { + acc[time_group] = []; } - - let grpByAll = undefined - if (groupBy) { - const groupQuery = groupBy.map( gb => `"${tableName}"."${gb}"`).join(',') - queryBuild = queryBuild.addSelect(groupQuery) - grpByAll = groupQuery + acc[time_group].push(curr); + return acc; + }, {}); + let result: any = {}; + const sectorsArray = Object.values(Sector); + sectorsArray?.map((sector) => { + result[this.firstLower(sector)] = []; + }); + const timeLabel = Object.getOwnPropertyNames(groupedDatasObject); + for (let timeIndex = 0; timeIndex < timeLabel.length; timeIndex++) { + const arrResultForTimeGroup = groupedDatasObject[timeLabel[timeIndex]]; + let resultThere: any = {}; + const sectorsArray = Object.values(Sector); + sectorsArray?.map((sector) => { + resultThere[this.firstLower(sector)] = false; + }); + for ( + let arrResultForTimeGroupIndex = 0; + arrResultForTimeGroupIndex < arrResultForTimeGroup.length; + arrResultForTimeGroupIndex++ + ) { + sectorsArray?.map((sector) => { + if ( + arrResultForTimeGroup[arrResultForTimeGroupIndex]?.sector === sector + ) { + resultThere[ + arrResultForTimeGroup[ + arrResultForTimeGroupIndex + ]?.sector?.toLowerCase() + ] = true; + result[ + arrResultForTimeGroup[ + arrResultForTimeGroupIndex + ]?.sector?.toLowerCase() + ]?.push( + parseInt(arrResultForTimeGroup[arrResultForTimeGroupIndex]?.count) + ); + } + }); } - if (timeGroupingCol && timeGroupingAccuracy) { - const groupQuery = `date_trunc('${timeGroupingAccuracy}', "${timeGroupingCol}") as time_group` - queryBuild = queryBuild.addSelect(groupQuery) - if (!grpByAll) { - grpByAll = 'time_group' - } else { - grpByAll += ', time_group' + sectorsArray?.map((sector) => { + if (resultThere[sector?.toLocaleLowerCase()] === false) { + result[sector?.toLocaleLowerCase()]?.push(0); } + }); + } + + const resultS = { + timeLabel, + ...result, + }; + + return resultS; + } + + private async programmeLocationDataFormatter(data) { + const locationData = [...data]; + let locationsGeoData: any = {}; + let features: any[] = []; + locationsGeoData.type = "FeatureCollection"; + locationData?.map((locationDataItem, index) => { + if (locationDataItem?.loc && locationDataItem !== null) { + let programmeGeoData: any = {}; + let location: any = locationDataItem?.loc; + programmeGeoData.type = "Feature"; + let properties: any = {}; + let geometry: any = {}; + properties.id = String(index); + properties.count = parseInt(locationDataItem?.count); + properties.stage = locationDataItem?.stage; + geometry.type = "Point"; + geometry.coordinates = location; + programmeGeoData.properties = properties; + programmeGeoData.geometry = geometry; + features.push(programmeGeoData); } - if (grpByAll != '') { - queryBuild = queryBuild.groupBy(grpByAll) + }); + + locationsGeoData.features = [...features]; + return locationsGeoData; + } + + private async genAggregateTypeOrmQuery( + repo: Repository, + tableName: string, + groupBy: string[], + aggregates: AggrEntry[], + filterAnd: FilterEntry[], + filterOr: FilterEntry[], + sort: SortEntry, + abilityCondition: string, + lastTimeForWhere: any, + statCache: any, + timeCol: string[], + timeGroupingCol?: string, + timeGroupingAccuracy?: string + ) { + const query = new QueryDto(); + query.filterAnd = filterAnd; + query.filterOr = filterOr; + query.sort = sort; + + const whereC = this.helperService.generateWhereSQL( + query, + this.helperService.parseMongoQueryToSQLWithTable( + tableName, + abilityCondition + ) + ); + let queryBuild = repo.createQueryBuilder(tableName).where(whereC); + + if (aggregates) { + const selectQuery = aggregates + .map((a) => { + const fieldCol = `${ + a.outerQuery ? "(" + a.outerQuery + "(" : "" + }"${tableName}"."${a.key}"${a.outerQuery ? ")s )" : ""}`; + + let mineCompField = fieldCol; + if (a.mineCompanyId) { + mineCompField = [ + "creditTransferred", + "creditRetired", + "creditFrozen", + ].includes(a.key) + ? `"${tableName}"."${a.key}"[array_position("${tableName}"."companyId", ${a.mineCompanyId})]` + : `"${tableName}"."${ + a.key === "creditBalance" + ? "creditOwnerPercentage" + : "proponentPercentage" + }"[array_position("${tableName}"."companyId", ${ + a.mineCompanyId + })]*${fieldCol}/100`; + } + return `${a.operation}(${mineCompField}) as ${a.fieldName}`; + }) + .join(","); + queryBuild = queryBuild.select(selectQuery); + } + + if (sort) { + queryBuild = queryBuild.orderBy( + sort?.key && `"${sort?.key}"`, + sort?.order, + "NULLS LAST" + ); + } + + let grpByAll = undefined; + if (groupBy) { + const groupQuery = groupBy + .map((gb) => { + let val; + if (gb.includes("->>")) { + const parts = gb.split("->>"); + val = `"${parts[0]}"->>'${parts[1]}'`; + } else { + val = `"${gb}"`; + } + return `"${tableName}".${val}`; + }) + .join(","); + const groupSelectQuery = groupBy + .map((gb) => { + let val; + if (gb.includes("->>")) { + const parts = gb.split("->>"); + val = `"${parts[0]}"->>'${parts[1]}' as ${parts[1]}`; + } else { + val = `"${gb}"`; + } + return `"${tableName}".${val}`; + }) + .join(","); + queryBuild = queryBuild.addSelect(groupSelectQuery); + grpByAll = groupQuery; + } + if (timeGroupingCol && timeGroupingAccuracy) { + const groupQuery = `date_trunc('${timeGroupingAccuracy}', "${timeGroupingCol}") as time_group`; + queryBuild = queryBuild.addSelect(groupQuery); + if (!grpByAll) { + grpByAll = "time_group"; + } else { + grpByAll += ", time_group"; } + } + if (grpByAll != "") { + queryBuild = queryBuild.groupBy(grpByAll); + } - let d = await queryBuild.getRawMany(); - - let t = 0; - if (timeCol) { - if (lastTimeForWhere[whereC]) { - console.log('Last time hit from the cache') - t = lastTimeForWhere[whereC]; - } else { - t = await this.getLastTime(repo, tableName, whereC, timeCol); - lastTimeForWhere[whereC] = t; + const key = + (grpByAll ? grpByAll : "") + " " + whereC + " from " + tableName; + if (statCache[key]) { + return statCache[key]; + } + let d = await queryBuild.getRawMany(); + let dTimeGrouped; + + let lastTime: any; + if (timeCol) { + const cacheKey = whereC + " from " + tableName; + if (lastTimeForWhere[cacheKey]) { + console.log("Last time hit from the cache"); + lastTime = lastTimeForWhere[cacheKey]; + } else { + const allTimes = {}; + let maxTime = 0; + for (const tc of timeCol) { + const colTime = await this.getLastTime(repo, tableName, whereC, tc); + allTimes[tc] = colTime; + if (colTime > maxTime) { + maxTime = colTime; + } } + lastTime = { + max: maxTime, + all: allTimes, + }; + lastTimeForWhere[cacheKey] = lastTime; } - for (const row of d) { - for (const k in row) { - if (row[k] === null) { - row[k] = 0; - } + } + for (const row of d) { + for (const k in row) { + if (row[k] === null) { + row[k] = 0; + } else if (row[k] !== undefined && !isNaN(row[k]) && row[k] % 1 !== 0) { + row[k] = parseFloat(Number(row[k]).toFixed(PRECISION)); } } - return { - 'data': d, - 'last': t + } + if (timeGroupingCol && timeGroupingAccuracy && groupBy) { + console.log("coming into this condition ---- groupBy[0]", groupBy[0]); + if (groupBy[0] === "currentStage") { + dTimeGrouped = await this.getTimeGroupedDataStatusConverted(d); + } else if (groupBy[0] === "sector") { + dTimeGrouped = await this.getTimeGroupedDataSectorConverted(d); + } + } else if (timeGroupingCol && timeGroupingAccuracy) { + console.log("coming into this condition ---- !groupBy[0]"); + const map = {}; + for (const en of d) { + if (!map[en.time_group]) { + map[en.time_group] = []; + } + map[en.time_group].push(en); } + dTimeGrouped = map; + } + statCache[key] = { + data: timeGroupingCol && timeGroupingAccuracy ? dTimeGrouped : d, + last: lastTime.max, + }; + + if (lastTime.all && Object.keys(lastTime.all).length > 0) { + statCache[key]["all"] = lastTime.all; + } + + return statCache[key]; } - private async getAllAuthProgramme(stat, abilityCondition, lastTimeForWhere, companyId?){ - let filtAuth = this.getFilterAndByStatFilter(stat.statFilter, - { value: ProgrammeStage.AUTHORISED, - key: "currentStage", - operation: '=' - }); + private async getAllAuthProgramme( + stat, + abilityCondition, + lastTimeForWhere, + statCache, + timeGroup: boolean, + companyId? + ) { + let filtAuth = this.getFilterAndByStatFilter( + stat.statFilter, + companyId + ? { + value: companyId, + key: "companyId", + operation: "ANY", + } + : undefined, + "createdTime" + ); - if (companyId) { - if (!filtAuth) { - filtAuth = [] - } - filtAuth.push({ - value: companyId, - key: "companyId", - operation: 'ANY' - }) + if (!filtAuth) { + filtAuth = []; } + filtAuth.push({ + value: ProgrammeStage.AUTHORISED, + key: "currentStage", + operation: "=", + }); + return await this.genAggregateTypeOrmQuery( this.programmeRepo, - "programme", + "programme", null, - [new AggrEntry('programmeId', 'COUNT', "count"), new AggrEntry('creditEst', 'SUM', "sum")], - filtAuth, + [ + new AggrEntry("programmeId", "COUNT", "count"), + { + key: "creditEst", + operation: "SUM", + fieldName: "sum", + mineCompanyId: + stat?.statFilter?.onlyMine && companyId ? companyId : undefined, + }, + ], + filtAuth, null, - null, + timeGroup ? { key: "time_group", order: "ASC" } : null, abilityCondition, lastTimeForWhere, - "createdTime" + statCache, + ["statusUpdateTime"], + timeGroup ? "createdAt" : undefined, + timeGroup ? "day" : undefined ); } - private async getCertifiedByMePrgrammes(statFilter, companyId, certifyField, abilityCondition, lastTimeForWhere) { - const filtC = this.getFilterAndByStatFilter(statFilter, - { value: companyId, - key: certifyField, - operation: 'ANY' - }); + private async getCertifiedByMePrgrammes( + statFilter, + companyId, + certifyField, + abilityCondition, + lastTimeForWhere, + statCache, + companyRole, + timeGroup?: boolean + ) { + let filtC = this.getFilterAndByStatFilter( + statFilter, + { + value: companyId, + key: certifyField, + operation: "ANY", + }, + "createdTime" + ); + + if (!filtC) { + filtC = []; + } + filtC.push({ + value: ProgrammeStage.AUTHORISED, + key: "currentStage", + operation: "=", + }); + return await this.genAggregateTypeOrmQuery( this.programmeRepo, - "programme", - null, - [new AggrEntry('programmeId', 'COUNT', "count"), new AggrEntry('creditEst', 'SUM', "sum")], - filtC, + "programme", + null, + [ + new AggrEntry("programmeId", "COUNT", "count"), + new AggrEntry("creditEst", "SUM", "sum"), + { + key: "creditEst", + operation: "SUM", + fieldName: "sum", + mineCompanyId: + statFilter?.onlyMine && + companyId && + companyRole === CompanyRole.PROGRAMME_DEVELOPER + ? companyId + : undefined, + }, + ], + filtC, + null, null, - null, abilityCondition, - lastTimeForWhere, - undefined + lastTimeForWhere, + statCache, + ["certifiedTime"], + timeGroup ? "createdAt" : undefined, + timeGroup ? "day" : undefined ); } - private async getCertifiedProgrammes(statFilter, abilityCondition, lastTimeForWhere, companyId, cardinalityField, frzAgg) { - - let filters = this.getFilterAndByStatFilter(statFilter, { value: companyId, key: 'companyId', operation: 'ANY' }); + private async getCertifiedProgrammes( + statFilter, + abilityCondition, + lastTimeForWhere, + statCache, + companyId, + cardinalityFilters: FilterEntry[], + frzAgg, + companyField: string, + companyRole: CompanyRole, + timeGroup?: boolean + ) { + let filters = this.getFilterAndByStatFilter( + statFilter, + null, + "createdTime" + ); if (!filters) { filters = []; } + for (const fl of cardinalityFilters) { + filters.push({ + key: fl.key, + operation: fl.operation, + value: fl.value, + keyOperation: "cardinality", + }); + } + filters.push({ - key: cardinalityField, - operation: ">", - value: 0, - keyOperation: 'cardinality' + value: ProgrammeStage.AUTHORISED, + key: "currentStage", + operation: "=", }); - console.log('getCertifiedProgrammes') + let filterOr = undefined; + if (statFilter && statFilter.onlyMine) { + filterOr = [ + { + value: companyId, + key: companyField, + operation: "ANY", + }, + { + value: companyId, + key: "companyId", + operation: "ANY", + }, + ]; + } + + frzAgg.mineCompanyId = + statFilter?.onlyMine && companyRole === CompanyRole.PROGRAMME_DEVELOPER + ? companyId + : undefined; return await this.genAggregateTypeOrmQuery( - this.programmeRepo, - "programme", - null, + this.programmeRepo, + "programme", + null, [ - new AggrEntry('programmeId', 'COUNT', "count"), - new AggrEntry('creditEst', 'SUM', "totalEstCredit"), - new AggrEntry('creditIssued', 'SUM', "totalIssuedCredit"), - new AggrEntry('creditRetired', 'SUM', "totalRetiredCredit"), - new AggrEntry('creditTransferred', 'SUM', "totalTxCredit"), + new AggrEntry("programmeId", "COUNT", "count"), + { + key: "creditEst", + operation: "SUM", + fieldName: "totalEstCredit", + mineCompanyId: frzAgg.mineCompanyId, + }, + { + key: "creditIssued", + operation: "SUM", + fieldName: "totalIssuedCredit", + mineCompanyId: frzAgg.mineCompanyId, + }, + { + key: "creditBalance", + operation: "SUM", + fieldName: "totalBalanceCredit", + mineCompanyId: frzAgg.mineCompanyId, + }, + { + key: "creditRetired", + operation: "SUM", + fieldName: "totalRetiredCredit", + mineCompanyId: frzAgg.mineCompanyId, + outerQuery: "select sum(s) from unnest", + }, + { + key: "creditTransferred", + operation: "SUM", + fieldName: "totalTxCredit", + mineCompanyId: frzAgg.mineCompanyId, + outerQuery: "select sum(s) from unnest", + }, frzAgg, - ], + ], filters, - null, - null, + filterOr, + timeGroup ? { key: "time_group", order: "ASC" } : null, abilityCondition, lastTimeForWhere, - "createdTime" + statCache, + ["certifiedTime"], + timeGroup ? "createdAt" : undefined, + timeGroup ? "day" : undefined + ); + } + + async calcStat( + stat: Stat, + results, + frzAgg, + abilityCondition, + lastTimeForWhere, + statCache, + companyId, + companyRole + ) { + const key = stat.key ? stat.key : stat.type; + switch (stat.type) { + case StatType.AGG_PROGRAMME_BY_STATUS: + case StatType.AGG_PROGRAMME_BY_SECTOR: + case StatType.MY_AGG_PROGRAMME_BY_STATUS: + case StatType.MY_AGG_PROGRAMME_BY_SECTOR: + case StatType.AGG_AUTH_PROGRAMME_BY_STATUS: + case StatType.MY_AGG_AUTH_PROGRAMME_BY_STATUS: + results[key] = await this.generateProgrammeAggregates( + stat, + frzAgg, + abilityCondition, + lastTimeForWhere, + statCache, + companyId + ); + break; + + case StatType.MY_CREDIT: + results[key] = await this.getCompanyCredits(companyId); + break; + case StatType.PENDING_TRANSFER_INIT: + case StatType.PENDING_TRANSFER_RECV: + results[key] = await this.getPendingTxStats( + stat, + companyId, + abilityCondition, + lastTimeForWhere, + statCache + ); + break; + case StatType.CERTIFIED_BY_ME: + case StatType.REVOKED_BY_ME: + stat.statFilter + ? (stat.statFilter.onlyMine = true) + : (stat.statFilter = { onlyMine: true }); + results[key] = await this.getCertifiedByMePrgrammes( + stat.statFilter, + companyId, + stat.type === StatType.CERTIFIED_BY_ME + ? "certifierId" + : "revokedCertifierId", + abilityCondition, + lastTimeForWhere, + statCache, + undefined + ); + break; + case StatType.CERTIFIED_REVOKED_BY_ME: + case StatType.UNCERTIFIED_BY_ME: + if (stat.statFilter) { + stat.statFilter.onlyMine = true; + } else { + stat.statFilter = { onlyMine: true }; + } + results[key] = await this.getCertifiedStatData( + results, + stat, + abilityCondition, + lastTimeForWhere, + statCache, + companyId, + StatType.UNCERTIFIED_BY_ME === stat.type, + companyRole + ); + break; + case StatType.ALL_AUTH_PROGRAMMES: + results[key] = await this.getAllAuthProgramme( + stat, + abilityCondition, + lastTimeForWhere, + statCache, + stat.statFilter.timeGroup + ); + break; + // case StatType.CERTIFIED_PROGRAMMES: + // case StatType.REVOKED_PROGRAMMES: + // if (!results[stat.type]) { + // results[stat.type] = await this.getCertifiedProgrammes( + // stat.statFilter, + // abilityCondition, + // lastTimeForWhere, + // statCache, + // companyId, + // stat.type === StatType.CERTIFIED_PROGRAMMES + // ? ["certifierId"] + // : ["revokedCertifierId"], + // frzAgg + // ); + // } + // break; + case StatType.CERTIFIED_REVOKED_PROGRAMMES: + case StatType.MY_CERTIFIED_REVOKED_PROGRAMMES: + if (stat.type === StatType.MY_CERTIFIED_REVOKED_PROGRAMMES) { + stat.statFilter + ? (stat.statFilter.onlyMine = true) + : (stat.statFilter = { onlyMine: true }); + } + results[key] = await this.getCertifiedRevokedAgg( + stat, + results, + abilityCondition, + lastTimeForWhere, + statCache, + companyId, + frzAgg, + companyRole + ); + break; + case StatType.CERTIFIED_BY_ME_BY_STATE: + case StatType.CERTIFIED_BY_ME_BY_SECTOR: + case StatType.AUTH_CERTIFIED_BY_ME_BY_STATE: + if (stat.statFilter) { + stat.statFilter.onlyMine = true; + } else { + stat.statFilter = { onlyMine: true }; + } + let filtCState = this.getFilterAndByStatFilter( + stat.statFilter, + { + value: companyId, + key: "certifierId", + operation: "ANY", + }, + "createdTime" + ); + + if (stat.type === StatType.AUTH_CERTIFIED_BY_ME_BY_STATE) { + if (!filtCState) { + filtCState = []; + } + filtCState.push({ + value: ProgrammeStage.AUTHORISED, + key: "currentStage", + operation: "=", + }); + } + + results[key] = await this.genAggregateTypeOrmQuery( + this.programmeRepo, + "programme", + [ + StatType.AUTH_CERTIFIED_BY_ME_BY_STATE, + StatType.CERTIFIED_BY_ME_BY_STATE, + ].includes(stat.type) + ? ["currentStage"] + : ["sector"], + [ + new AggrEntry("programmeId", "COUNT", "count"), + new AggrEntry("creditEst", "SUM", "totalEstCredit"), + new AggrEntry("creditIssued", "SUM", "totalIssuedCredit"), + new AggrEntry("creditBalance", "SUM", "totalBalanceCredit"), + { + key: "creditRetired", + operation: "SUM", + fieldName: "totalRetiredCredit", + outerQuery: "select sum(s) from unnest", + }, + { + key: "creditTransferred", + operation: "SUM", + fieldName: "totalTxCredit", + outerQuery: "select sum(s) from unnest", + }, + frzAgg, + ], + filtCState, + null, + null, + abilityCondition, + lastTimeForWhere, + statCache, + ["certifiedTime"], + stat.statFilter?.timeGroup ? "createdAt" : undefined, + stat.statFilter?.timeGroup ? "day" : undefined + ); + break; + case StatType.ALL_PROGRAMME_LOCATION: + case StatType.MY_PROGRAMME_LOCATION: + if (stat.type === StatType.MY_PROGRAMME_LOCATION) { + stat.statFilter + ? (stat.statFilter.onlyMine = true) + : (stat.statFilter = { onlyMine: true }); + } + const whereC = []; + whereC.push(`p."programmeId" != 'null'`); + if (stat.statFilter && stat.statFilter.onlyMine) { + whereC.push( + `${companyId} = ANY(b."companyId") or ${companyId} = ANY(b."certifierId")` + ); + } + if (stat.statFilter && stat.statFilter.startTime) { + whereC.push(`"createdTime" >= ${stat.statFilter.startTime}`); + } + if (stat.statFilter && stat.statFilter.endTime) { + whereC.push(`"createdTime" <= ${stat.statFilter.endTime}`); + } + const resultsProgrammeLocations = await this.programmeRepo.manager + .query(`SELECT p."programmeId" as loc, b."currentStage" as stage, count(*) AS count + FROM programme b, jsonb_array_elements(b."geographicalLocationCordintes") p("programmeId") + ${whereC.length > 0 ? " where " : " "} + ${whereC.join(" and ")} + GROUP BY p."programmeId", b."currentStage"`); + results[key] = await this.programmeLocationDataFormatter( + resultsProgrammeLocations + ); + break; + case StatType.ALL_TRANSFER_LOCATION: + case StatType.MY_TRANSFER_LOCATION: + if (stat.type === StatType.MY_TRANSFER_LOCATION) { + stat.statFilter + ? (stat.statFilter.onlyMine = true) + : (stat.statFilter = { onlyMine: true }); + } + + let filtCom = this.getFilterAndByStatFilter( + stat.statFilter, + null, + "txTime" + ); + if (!filtCom) { + filtCom = []; + } + filtCom.push({ + value: "0", + key: "retirementType", + operation: "=", + }); + filtCom.push({ + value: TransferStatus.RECOGNISED, + key: "status", + operation: "=", + }); + + let filterOr = undefined; + if (stat.statFilter && stat.statFilter.onlyMine) { + filterOr = []; + filterOr.push({ + value: companyId, + key: "fromCompanyId", + operation: "=", + }); + filterOr.push({ + value: companyId, + key: "programmeCertifierId", + operation: "ANY", + }); + } + results[key] = await this.genAggregateTypeOrmQuery( + this.programmeTransferRepo, + "transfer", + [`toCompanyMeta->>country`], + [new AggrEntry("requestId", "COUNT", "count")], + filtCom, + filterOr, + null, + abilityCondition, + lastTimeForWhere, + statCache, + ["createdTime"] + ); + break; + } + return results; + } + + async getCertifiedRevokedAgg( + stat: Stat, + results, + abilityCondition, + lastTimeForWhere, + statCache, + companyId, + frzAgg, + companyRole + ): Promise { + const revoked = await this.getCertifiedProgrammes( + stat.statFilter, + abilityCondition, + lastTimeForWhere, + statCache, + companyId, + [{ key: "certifierId", operation: "=", value: 0 }], + frzAgg, + "revokedCertifierId", + companyRole, + stat.statFilter?.timeGroup ? true : false ); + + const allAuth = await this.getAllAuthProgramme( + stat, + abilityCondition, + lastTimeForWhere, + statCache, + stat.statFilter?.timeGroup ? true : false, + companyId + ); + + const certified = await this.getCertifiedProgrammes( + stat.statFilter, + abilityCondition, + lastTimeForWhere, + statCache, + companyId, + [{ key: "certifierId", operation: ">", value: 0 }], + frzAgg, + "certifierId", + companyRole, + stat.statFilter?.timeGroup ? true : false + ); + + if (!stat.statFilter || stat.statFilter.timeGroup != true) { + return { + last: Math.max(allAuth.last, certified.last, revoked.last), + data: { + certifiedSum: Number( + certified && certified.data.length > 0 && certified?.data[0] + ? certified?.data[0]["totalestcredit"] + : 0 + ), + revokedSum: Number( + revoked && revoked.data.length > 0 && revoked?.data[0] + ? revoked.data[0]["totalestcredit"] + : 0 + ), + uncertifiedSum: + Number( + allAuth && allAuth.data.length > 0 && allAuth?.data[0] + ? allAuth?.data[0]["sum"] + : 0 + ) - + Number( + certified && certified?.data.length > 0 && certified?.data[0] + ? certified?.data[0]["totalestcredit"] + : 0 + ) - + Number( + revoked && revoked?.data.length > 0 && revoked?.data[0] + ? revoked.data[0]["totalestcredit"] + : 0 + ), + }, + }; + } else { + const groupedData = {}; + for (const d in certified.data) { + groupedData[d] = { + certifiedSum: Number( + certified && certified.data[d] && certified.data[d].length > 0 + ? certified.data[d][0]["totalestcredit"] + : 0 + ), + revokedSum: 0, + }; + } + for (const d in revoked.data) { + if (!groupedData[d]) { + groupedData[d] = { + revokedSum: Number( + revoked && revoked.data[d] && revoked.data[d].length > 0 + ? revoked.data[d][0]["totalestcredit"] + : 0 + ), + certifiedSum: 0, + }; + } else { + groupedData[d]["revokedSum"] = Number( + revoked && revoked.data[d] && revoked.data[d].length > 0 + ? revoked.data[d][0]["totalestcredit"] + : 0 + ); + } + } + for (const d in allAuth.data) { + if (!groupedData[d]) { + groupedData[d] = { + revokedSum: 0, + certifiedSum: 0, + uncertifiedSum: Number( + allAuth && allAuth.data[d] && allAuth.data[d].length > 0 + ? allAuth.data[d][0]["sum"] + : 0 + ), + }; + } else { + groupedData[d]["uncertifiedSum"] = + Number( + allAuth && allAuth.data[d] && allAuth.data[d].length > 0 + ? allAuth.data[d][0]["sum"] + : 0 + ) - + groupedData[d]["certifiedSum"] - + groupedData[d]["revokedSum"]; + } + } + + const timeLabel = Object.getOwnPropertyNames(groupedData); + timeLabel.sort((a: any, b: any) => { + let dateA: any = new Date(a); + let dateB: any = new Date(b); + return dateA - dateB; + }); + + const sortedGroupedData = {}; + timeLabel?.map((time) => { + if (!sortedGroupedData[time]) { + sortedGroupedData[time] = { + certifiedSum: groupedData[time]["certifiedSum"], + uncertifiedSum: groupedData[time]["uncertifiedSum"], + revokedSum: groupedData[time]["revokedSum"], + }; + } else { + sortedGroupedData[time]["certifiedSum"] = + groupedData[time]["certifiedSum"]; + sortedGroupedData[time]["uncertifiedSum"] = + groupedData[time]["uncertifiedSum"]; + sortedGroupedData[time]["revokedSum"] = + groupedData[time]["revokedSum"]; + } + }); + + const chartData = { + timeLabel: [], + certifiedSum: [], + uncertifiedSum: [], + revokedSum: [], + }; + for (const tg in sortedGroupedData) { + chartData.timeLabel.push(tg); + chartData.certifiedSum.push( + parseFloat(sortedGroupedData[tg]["certifiedSum"]) + ); + chartData.uncertifiedSum.push( + parseFloat(sortedGroupedData[tg]["uncertifiedSum"]) + ); + chartData.revokedSum.push( + parseFloat(sortedGroupedData[tg]["revokedSum"]) + ); + } + + return { + last: Math.max(allAuth.last, certified.last, revoked.last), + data: chartData, + }; + } } - + + // async getMultipleStats(sourceStats: Stat[], newStat: Stat, results, frzAgg, abilityCondition, lastTimeForWhere, companyId, aggFunc: any) { + // for (const s of sourceStats) { + // if (!results[s.type]) { + // results = this.calcStat(s, results, frzAgg, abilityCondition, lastTimeForWhere, companyId); + // } + // } + // results[newStat.type] = aggFunc(results); + // } + async getAggregateQuery( abilityCondition: string, query: StatList, - companyId: any + companyId: any, + companyRole: CompanyRole ): Promise { let results = {}; let lastTimeForWhere = {}; + let statCache = {}; - const frzAgg = new AggrEntry("creditFrozen", 'SUM', "totalFreezeCredit"); - frzAgg.outerQuery = 'select sum(s) from unnest' + const frzAgg = new AggrEntry("creditFrozen", "SUM", "totalFreezeCredit"); + frzAgg.outerQuery = "select sum(s) from unnest"; for (const stat of query.stats) { - switch (stat.type) { - case StatType.AGG_PROGRAMME_BY_STATUS: - case StatType.AGG_PROGRAMME_BY_SECTOR: - case StatType.MY_AGG_PROGRAMME_BY_STATUS: - case StatType.MY_AGG_PROGRAMME_BY_SECTOR: - - if ([StatType.MY_AGG_PROGRAMME_BY_SECTOR, StatType.MY_AGG_PROGRAMME_BY_STATUS].includes(stat.type)) { - stat.statFilter.onlyMine = true; - } - results[stat.type] = await this.genAggregateTypeOrmQuery( - this.programmeRepo, - "programme", - ([StatType.AGG_PROGRAMME_BY_STATUS, StatType.MY_AGG_PROGRAMME_BY_STATUS].includes(stat.type)) ? ["currentStage"] : ["sector"], - [ - new AggrEntry('programmeId', 'COUNT', "count"), - new AggrEntry('creditEst', 'SUM', "totalEstCredit"), - new AggrEntry('creditIssued', 'SUM', "totalIssuedCredit"), - new AggrEntry("creditBalance", "SUM", "totalBalanceCredit"), - new AggrEntry('creditRetired', 'SUM', "totalRetiredCredit"), - new AggrEntry('creditTransferred', 'SUM', "totalTxCredit"), - frzAgg, - ], - this.getFilterAndByStatFilter(stat.statFilter, { value: companyId, key: 'companyId', operation: 'ANY' }), - null, - null, - abilityCondition, - lastTimeForWhere, - "createdTime", - stat.statFilter?.timeGroup ? "createdAt" : undefined, - stat.statFilter?.timeGroup ? "day" : undefined, - ); - break; - case StatType.MY_CREDIT: - const comp = await this.companyRepo.findOne({ - where: { - companyId: companyId, - }, - }); - results[stat.type] = { - data: { - 'primary': comp ? comp.creditBalance : 0, - 'secondary': comp ? comp.secondaryAccountBalance : 0 - } - } - break; - case StatType.PENDING_TRANSFER_INIT: - case StatType.PENDING_TRANSFER_RECV: - if (stat.statFilter) { - stat.statFilter.onlyMine = true; - } else { - stat.statFilter = { onlyMine: true } - } - let filt = this.getFilterAndByStatFilter(stat.statFilter, { value: companyId, - key: stat.type === StatType.PENDING_TRANSFER_INIT ? "initiatorCompanyId" : "fromCompanyId", - operation: '=' - }); - - if (!filt) { - filt = [] - } - filt.push({ - value: TransferStatus.PENDING, - operation: '=', - key: 'status' - }) - - // if (stat.type === StatType.PENDING_TRANSFER_RECV) { - // filterOr.push({ value: companyId, - // key: "fromCompanyId", - // operation: '=' - // }) - // } - results[stat.type] = await this.genAggregateTypeOrmQuery( - this.programmeTransferRepo, - "transfer", - null, - [new AggrEntry('requestId', 'COUNT', 'count')], - filt, - null, - null, - abilityCondition, - lastTimeForWhere, - "txTime" - ); - break; - case StatType.CERTIFIED_BY_ME: - case StatType.REVOKED_BY_ME: - if (stat.statFilter) { - stat.statFilter.onlyMine = true; - } else { - stat.statFilter = { onlyMine: true } - } - if (!results[StatType.ALL_AUTH_PROGRAMMES]){ - results[StatType.ALL_AUTH_PROGRAMMES] = await this.getAllAuthProgramme(stat, abilityCondition, lastTimeForWhere) - } - - if (!results[stat.type]){ - results[stat.type] = await this.getCertifiedByMePrgrammes(stat.statFilter, companyId, stat.type === StatType.CERTIFIED_BY_ME ? "certifierId" : "revokedCertifierId", abilityCondition, lastTimeForWhere) - } + await this.calcStat( + stat, + results, + frzAgg, + abilityCondition, + lastTimeForWhere, + statCache, + companyId, + companyRole + ); + } + return new DataCountResponseDto(results); + } - break; - case StatType.CERTIFIED_REVOKED_BY_ME: - if (stat.statFilter) { - stat.statFilter.onlyMine = true; - } else { - stat.statFilter = { onlyMine: true } - } - if (!results[StatType.ALL_AUTH_PROGRAMMES]){ - results[StatType.ALL_AUTH_PROGRAMMES] = await this.getAllAuthProgramme(stat, abilityCondition, lastTimeForWhere) - } - - if (!results[StatType.CERTIFIED_BY_ME]){ - results[StatType.CERTIFIED_BY_ME] = await this.getCertifiedByMePrgrammes(stat.statFilter, companyId, "certifierId", abilityCondition, lastTimeForWhere) - } + async getCertifiedStatData( + results, + stat, + abilityCondition, + lastTimeForWhere, + statCache, + companyId, + onlyUncertified, + companyRole + ): Promise { + const allAuth = await this.getAllAuthProgramme( + stat, + abilityCondition, + lastTimeForWhere, + statCache, + stat.statFilter?.timeGroup ? true : false + ); - if (!results[StatType.REVOKED_BY_ME]){ - results[StatType.REVOKED_BY_ME] = await this.getCertifiedByMePrgrammes(stat.statFilter, companyId, "revokedCertifierId", abilityCondition, lastTimeForWhere) - } + console.log("Credit minus allAuth", allAuth); + const certified = await this.getCertifiedByMePrgrammes( + stat.statFilter, + companyId, + "certifierId", + abilityCondition, + lastTimeForWhere, + statCache, + companyRole, + stat.statFilter?.timeGroup ? true : false + ); - results[stat.type] = { - last: results[StatType.ALL_AUTH_PROGRAMMES].last, - data: { - "certifiedSum": Number(results[StatType.CERTIFIED_BY_ME].data[0]['totalestcredit']), - "revokedSum": Number(results[StatType.REVOKED_BY_ME].data[0]['totalestcredit']), - "uncertifiedSum": Number(results[StatType.ALL_AUTH_PROGRAMMES].data[0]['sum']) - Number(results[StatType.REVOKED_BY_ME].data[0]['totalestcredit']) - Number(results[StatType.CERTIFIED_BY_ME].data[0]['totalestcredit']), - "certifiedCount": Number(results[StatType.CERTIFIED_BY_ME].data[0]['count']), - "revokedCount": Number(results[StatType.REVOKED_BY_ME].data[0]['count']), - "uncertifiedCount": Number(results[StatType.ALL_AUTH_PROGRAMMES].data[0]['count']) - Number(results[StatType.REVOKED_BY_ME].data[0]['count']) - Number(results[StatType.CERTIFIED_BY_ME].data[0]['count']), - } - } + console.log("Credit minus certified", certified); + if (!onlyUncertified) { + const revoked = await this.getCertifiedByMePrgrammes( + stat.statFilter, + companyId, + "revokedCertifierId", + abilityCondition, + lastTimeForWhere, + statCache, + companyRole, + stat.statFilter?.timeGroup ? true : false + ); - break; - case StatType.ALL_AUTH_PROGRAMMES: - if (!results[StatType.ALL_AUTH_PROGRAMMES]){ - results[StatType.ALL_AUTH_PROGRAMMES] = await this.getAllAuthProgramme(stat, abilityCondition, lastTimeForWhere) - } - // results[stat.type] = results[StatType.ALL_AUTH_PROGRAMMES]; - break; - case StatType.CERTIFIED_PROGRAMMES: - case StatType.REVOKED_PROGRAMMES: - if (!results[StatType.ALL_AUTH_PROGRAMMES]){ - results[StatType.ALL_AUTH_PROGRAMMES] = await this.getAllAuthProgramme(stat, abilityCondition, lastTimeForWhere) - } - if (!results[stat.type]) { - results[stat.type] = await this.getCertifiedProgrammes(stat.statFilter, - abilityCondition, - lastTimeForWhere, - companyId, - (stat.type === StatType.CERTIFIED_PROGRAMMES ? ["certifierId"] : ["revokedCertifierId"]), - frzAgg + console.log("Credit minus revoked", revoked); + if (!stat.statFilter || stat.statFilter.timeGroup != true) { + return { + last: Math.max(revoked.last, certified.last, allAuth.last), + data: { + certifiedCount: Number( + certified && certified.data.length > 0 + ? certified.data[0]["count"] + : 0 + ), + revokedCount: Number( + revoked && revoked.data.length > 0 ? revoked.data[0]["count"] : 0 + ), + uncertifiedCount: + Number( + allAuth && allAuth.data.length > 0 + ? allAuth.data[0]["count"] + : 0 + ) - + Number( + revoked && revoked.data.length > 0 + ? revoked.data[0]["count"] + : 0 + ) - + Number( + certified && certified.data.length > 0 + ? certified.data[0]["count"] + : 0 + ), + revokedSum: Number( + revoked && revoked.data.length > 0 ? revoked.data[0]["sum"] : 0 + ), + certifiedSum: Number( + certified && certified.data.length > 0 + ? certified.data[0]["sum"] + : 0 + ), + uncertifiedSum: + Number( + allAuth && allAuth.data.length > 0 ? allAuth.data[0]["sum"] : 0 + ) - + Number( + revoked && revoked.data.length > 0 ? revoked.data[0]["sum"] : 0 + ) - + Number( + certified && certified.data.length > 0 + ? certified.data[0]["sum"] + : 0 + ), + }, + }; + } else { + const groupedData = {}; + for (const d in certified.data) { + groupedData[d] = { + certifiedSum: Number( + certified && certified.data[d] && certified.data[d].length > 0 + ? certified.data[d][0]["sum"] + : 0 + ), + revokedSum: 0, + }; + } + for (const d in revoked.data) { + if (!groupedData[d]) { + groupedData[d] = { + revokedSum: Number( + revoked && revoked.data[d] && revoked.data[d].length > 0 + ? revoked.data[d][0]["sum"] + : 0 + ), + certifiedSum: 0, + }; + } else { + groupedData[d]["revokedSum"] = Number( + revoked && revoked.data[d] && revoked.data[d].length > 0 + ? revoked.data[d][0]["sum"] + : 0 ); } - break; - case StatType.CERTIFIED_REVOKED_PROGRAMMES: - case StatType.MY_CERTIFIED_REVOKED_PROGRAMMES: - - let allValues; - if (stat.type === StatType.CERTIFIED_REVOKED_PROGRAMMES){ - if (!results[StatType.ALL_AUTH_PROGRAMMES]) { - results[StatType.ALL_AUTH_PROGRAMMES] = await this.getAllAuthProgramme(stat, abilityCondition, lastTimeForWhere); - } - allValues = results[StatType.ALL_AUTH_PROGRAMMES] - stat.statFilter ? stat.statFilter.onlyMine = false : stat.statFilter = { onlyMine: false } + } + for (const d in allAuth.data) { + if (!groupedData[d]) { + groupedData[d] = { + revokedSum: 0, + certifiedSum: 0, + uncertifiedSum: Number( + allAuth && allAuth.data[d] && allAuth.data[d].length > 0 + ? allAuth.data[d][0]["sum"] + : 0 + ), + }; + } else { + groupedData[d]["uncertifiedSum"] = + Number( + allAuth && allAuth.data[d] && allAuth.data[d].length > 0 + ? allAuth.data[d][0]["sum"] + : 0 + ) - + groupedData[d]["certifiedSum"] - + groupedData[d]["revokedSum"]; } + } - if (stat.type === StatType.MY_CERTIFIED_REVOKED_PROGRAMMES){ - if (!results[StatType.ALL_AUTH_PROGRAMME_MINE]) { - results[StatType.ALL_AUTH_PROGRAMME_MINE] = await this.getAllAuthProgramme(stat, abilityCondition, lastTimeForWhere, companyId) - } - allValues = results[StatType.ALL_AUTH_PROGRAMME_MINE] - stat.statFilter ? stat.statFilter.onlyMine = true : stat.statFilter = { onlyMine: true } - } + const timeLabel = Object.getOwnPropertyNames(groupedData); + timeLabel.sort((a: any, b: any) => { + let dateA: any = new Date(a); + let dateB: any = new Date(b); + return dateA - dateB; + }); - if (!results[StatType.REVOKED_PROGRAMMES]){ - results[StatType.REVOKED_PROGRAMMES] = await this.getCertifiedProgrammes(stat.statFilter, abilityCondition, lastTimeForWhere, companyId, ["revokedCertifierId"], frzAgg) - } - if (!results[StatType.CERTIFIED_PROGRAMMES]){ - results[StatType.CERTIFIED_PROGRAMMES] = await this.getCertifiedProgrammes(stat.statFilter, abilityCondition, lastTimeForWhere, companyId, ["certifierId"], frzAgg) - } - results[stat.type] = { - last: allValues.last, - data: { - "certifiedSum": Number(results[StatType.CERTIFIED_PROGRAMMES].data[0]['totalestcredit']), - "revokedSum": Number(results[StatType.REVOKED_PROGRAMMES].data[0]['totalestcredit']), - "uncertifiedSum": Number(allValues.data[0]['sum']) - Number(results[StatType.REVOKED_PROGRAMMES].data[0]['totalestcredit']) - Number(results[StatType.CERTIFIED_PROGRAMMES].data[0]['totalestcredit']), - } - } - break; - case StatType.CERTIFIED_BY_ME_BY_STATE: - case StatType.CERTIFIED_BY_ME_BY_SECTOR: - if (stat.statFilter) { - stat.statFilter.onlyMine = true; + const sortedGroupedData = {}; + timeLabel?.map((time) => { + if (!sortedGroupedData[time]) { + sortedGroupedData[time] = { + certifiedSum: groupedData[time]["certifiedSum"], + uncertifiedSum: groupedData[time]["uncertifiedSum"], + revokedSum: groupedData[time]["revokedSum"], + }; } else { - stat.statFilter = { onlyMine: true } - } - let filtCState = this.getFilterAndByStatFilter(stat.statFilter, - { value: companyId, - key: "certifierId", - operation: 'ANY' - }); - - results[stat.type] = await this.genAggregateTypeOrmQuery( - this.programmeRepo, - "programme", - stat.type === StatType.CERTIFIED_BY_ME_BY_STATE ? ["currentStage"] : ["sector"], - [ - new AggrEntry('programmeId', 'COUNT', "count"), - new AggrEntry('creditEst', 'SUM', "totalEstCredit"), - new AggrEntry('creditIssued', 'SUM', "totalIssuedCredit"), - new AggrEntry('creditRetired', 'SUM', "totalRetiredCredit"), - new AggrEntry('creditTransferred', 'SUM', "totalTxCredit"), - frzAgg, - ], - filtCState, - null, - null, - abilityCondition, - lastTimeForWhere, - undefined - ); - break; - case StatType.ALL_PROGRAMME_LOCATION: - case StatType.MY_PROGRAMME_LOCATION: - case StatType.MY_CERTIFIED_PROGRAMME_LOCATION: - results[stat.type] = await this.programmeRepo.manager.query(`SELECT p."programmeId" as loc, count(*) AS count - FROM programme b, jsonb_array_elements(b."geographicalLocationCordintes") p("programmeId") - ${stat.type === StatType.MY_PROGRAMME_LOCATION ? `where ${companyId} = ANY(b."companyId")` : ''} - ${stat.type === StatType.MY_CERTIFIED_PROGRAMME_LOCATION ? `where ${companyId} = ANY(b."certifierId")` : ''} - GROUP BY p."programmeId"`); - break; - case StatType.ALL_TRANSFER_LOCATION: - case StatType.MY_TRANSFER_LOCATION: - case StatType.MY_CERTIFIED_TRANSFER_LOCATION: - if (stat.type === StatType.MY_TRANSFER_LOCATION) { - stat.statFilter ? stat.statFilter.onlyMine = true : stat.statFilter = { onlyMine: true } + sortedGroupedData[time]["certifiedSum"] = + groupedData[time]["certifiedSum"]; + sortedGroupedData[time]["uncertifiedSum"] = + groupedData[time]["uncertifiedSum"]; + sortedGroupedData[time]["revokedSum"] = + groupedData[time]["revokedSum"]; } + }); - let filtCom = this.getFilterAndByStatFilter(stat.statFilter, - { value: companyId, - key: "fromCompanyId", - operation: '=' - }); - if (!filtCom) { - filtCom = [] - } - filtCom.push({ - value: '0', - key: "retirementType", - operation: '=' - }) - if (stat.type === StatType.MY_CERTIFIED_TRANSFER_LOCATION) - filtCom.push({ - value: companyId, - key: 'certifier"->>"certifierId', - operation: 'ANY' - }) - results[stat.type] = await this.genAggregateTypeOrmQuery( - this.programmeTransferRepo, - "transfer", - ['toCompanyMeta"->>country'], - [new AggrEntry('requestId', 'COUNT', 'count')], - filtCom, - null, - null, - abilityCondition, - lastTimeForWhere, - "txTime" + const chartData = { + timeLabel: [], + certifiedSum: [], + uncertifiedSum: [], + revokedSum: [], + }; + for (const tg in sortedGroupedData) { + chartData.timeLabel.push(tg); + chartData.certifiedSum.push( + parseFloat(sortedGroupedData[tg]["certifiedSum"]) + ); + chartData.uncertifiedSum.push( + parseFloat(sortedGroupedData[tg]["uncertifiedSum"]) ); - break; + chartData.revokedSum.push( + parseFloat(sortedGroupedData[tg]["revokedSum"]) + ); + } + + return { + last: Math.max(allAuth.last, certified.last, revoked.last), + data: chartData, + }; } + } else { + return { + last: Math.max(certified.last, allAuth.last), + data: { + uncertifiedSum: + Number( + allAuth && allAuth.data.length > 0 ? allAuth.data[0]["sum"] : 0 + ) - + Number( + certified && certified.data.length > 0 + ? certified.data[0]["sum"] + : 0 + ), + uncertifiedCount: + Number( + allAuth && allAuth.data.length > 0 ? allAuth.data[0]["count"] : 0 + ) - + Number( + certified && certified.data.length > 0 + ? certified.data[0]["count"] + : 0 + ), + }, + }; } - return new DataCountResponseDto(results); } -} \ No newline at end of file + + async getCompanyCredits(companyId: any) { + const comp = await this.companyRepo.findOne({ + where: { + companyId: companyId, + }, + }); + return { + data: { + primary: comp ? comp.creditBalance : 0, + secondary: comp ? comp.secondaryAccountBalance : 0, + }, + last: comp.creditTxTime, + }; + } + async getPendingTxStats( + stat, + companyId, + abilityCondition, + lastTimeForWhere, + statCache + ) { + if (stat.statFilter) { + stat.statFilter.onlyMine = false; + } else { + stat.statFilter = { onlyMine: false }; + } + let filt = this.getFilterAndByStatFilter(stat.statFilter, null, "txTime"); + + if (filt == null) { + filt = []; + } + filt.push({ + key: "status", + operation: "=", + value: TransferStatus.PENDING, + }); + filt.push({ + key: "isRetirement", + operation: "!=", + value: true, + }); + + const filterOr = [ + { + value: companyId, + key: + stat.type === StatType.PENDING_TRANSFER_INIT + ? "initiatorCompanyId" + : "toCompanyId", + operation: "=", + }, + ]; + if (stat.type === StatType.PENDING_TRANSFER_RECV) { + filterOr.push({ + value: companyId, + key: "fromCompanyId", + operation: "=", + }); + } + return await this.genAggregateTypeOrmQuery( + this.programmeTransferRepo, + "transfer", + null, + [new AggrEntry("requestId", "COUNT", "count")], + filt, + filterOr, + null, + abilityCondition, + lastTimeForWhere, + statCache, + ["txTime"] + ); + } + async generateProgrammeAggregates( + stat, + frzAgg, + abilityCondition, + lastTimeForWhere, + statCache, + companyId + ) { + if ( + [ + StatType.MY_AGG_PROGRAMME_BY_SECTOR, + StatType.MY_AGG_PROGRAMME_BY_STATUS, + StatType.MY_AGG_AUTH_PROGRAMME_BY_STATUS, + ].includes(stat.type) + ) { + stat.statFilter + ? (stat.statFilter.onlyMine = true) + : (stat.statFilter = { onlyMine: true }); + } + + let filterAnd = this.getFilterAndByStatFilter( + stat.statFilter, + { + value: companyId, + key: "companyId", + operation: "ANY", + }, + "createdTime" + ); + + if ( + [ + StatType.AGG_AUTH_PROGRAMME_BY_STATUS, + StatType.MY_AGG_AUTH_PROGRAMME_BY_STATUS, + ].includes(stat.type) + ) { + if (!filterAnd) { + filterAnd = []; + } + filterAnd.push({ + value: ProgrammeStage.AUTHORISED, + key: "currentStage", + operation: "=", + }); + } + + frzAgg.mineCompanyId = stat?.statFilter?.onlyMine ? companyId : undefined; + + return await this.genAggregateTypeOrmQuery( + this.programmeRepo, + "programme", + [ + StatType.AGG_PROGRAMME_BY_STATUS, + StatType.MY_AGG_PROGRAMME_BY_STATUS, + StatType.MY_AGG_AUTH_PROGRAMME_BY_STATUS, + StatType.AGG_AUTH_PROGRAMME_BY_STATUS, + ].includes(stat.type) + ? ["currentStage"] + : ["sector"], + [ + new AggrEntry("programmeId", "COUNT", "count"), + { + key: "creditEst", + operation: "SUM", + fieldName: "totalEstCredit", + mineCompanyId: stat?.statFilter?.onlyMine ? companyId : undefined, + }, + { + key: "creditIssued", + operation: "SUM", + fieldName: "totalIssuedCredit", + mineCompanyId: stat?.statFilter?.onlyMine ? companyId : undefined, + }, + { + key: "creditBalance", + operation: "SUM", + fieldName: "totalBalanceCredit", + mineCompanyId: stat?.statFilter?.onlyMine ? companyId : undefined, + }, + { + key: "creditRetired", + operation: "SUM", + fieldName: "totalRetiredCredit", + mineCompanyId: stat?.statFilter?.onlyMine ? companyId : undefined, + outerQuery: "select sum(s) from unnest", + }, + { + key: "creditTransferred", + operation: "SUM", + fieldName: "totalTxCredit", + mineCompanyId: stat?.statFilter?.onlyMine ? companyId : undefined, + outerQuery: "select sum(s) from unnest", + }, + frzAgg, + ], + filterAnd, + null, + stat.statFilter?.timeGroup ? { key: "time_group", order: "ASC" } : null, + abilityCondition, + lastTimeForWhere, + statCache, + ["statusUpdateTime", "creditUpdateTime"], + stat.statFilter?.timeGroup ? "createdAt" : undefined, + stat.statFilter?.timeGroup ? "day" : undefined + ); + } + + private groupByStatus(key: string, data: any) { + const mapping = {}; + for (const d of data) { + if (!mapping[d[key]]) { + mapping[d[key]] = []; + } + mapping[d[key]].push(d); + } + + return data; + } +} diff --git a/lambda/services/src/analytics-api/handler.ts b/lambda/services/src/analytics-api/handler.ts index 232b51fdd..3112581a8 100644 --- a/lambda/services/src/analytics-api/handler.ts +++ b/lambda/services/src/analytics-api/handler.ts @@ -8,7 +8,7 @@ import { AnalyticsAPIModule } from "./analytics.api.module"; let cachedServer: Server; export const handler: Handler = async (event: any, context: Context) => { - const httpBase = "/api/stats"; + const httpBase = "/stats"; cachedServer = await bootstrapServer( cachedServer, AnalyticsAPIModule, diff --git a/lambda/services/src/analytics-api/programme.controller.ts b/lambda/services/src/analytics-api/programme.controller.ts index 600a19e30..f9b402e61 100644 --- a/lambda/services/src/analytics-api/programme.controller.ts +++ b/lambda/services/src/analytics-api/programme.controller.ts @@ -82,7 +82,8 @@ export class ProgrammeController { return this.aggService.getAggregateQuery( req.abilityCondition, query, - companyId + companyId, + req.user?.companyRole ); } } diff --git a/lambda/services/src/ledger-replicator/ledger-replicator.service.ts b/lambda/services/src/ledger-replicator/ledger-replicator.service.ts index 7179adf86..fa1e747b2 100644 --- a/lambda/services/src/ledger-replicator/ledger-replicator.service.ts +++ b/lambda/services/src/ledger-replicator/ledger-replicator.service.ts @@ -37,8 +37,10 @@ export class LedgerReplicatorService { encodeURIComponent(address[index]) + ".json?access_token=" + ACCESS_TOKEN + - "&limit=1"+ - `&country=${this.configService.get('systemCountry')}&autocomplete=false&types=region%2Cdistrict` + "&limit=1" + + `&country=${this.configService.get( + "systemCountry" + )}&autocomplete=false&types=region%2Cdistrict`; console.log("geocoding request urls -> ", index, url); await axios .get(url) @@ -52,7 +54,7 @@ export class LedgerReplicatorService { if (response?.data?.features.length > 0) { geoCodinates.push([...response?.data?.features[0]?.center]); } else { - geoCodinates.push(null) + geoCodinates.push(null); } }) .catch((err) => { @@ -96,58 +98,108 @@ export class LedgerReplicatorService { Programme, JSON.parse(JSON.stringify(payload)) ); - try { - let address: any[] = []; - if (programme && programme.programmeProperties) { - if (programme.currentStage === "AwaitingAuthorization") { - const programmeProperties = programme.programmeProperties; - if (programmeProperties.geographicalLocation) { - for ( - let index = 0; - index < programmeProperties.geographicalLocation.length; - index++ - ) { - address.push( - programmeProperties.geographicalLocation[index] + + if (programme != null) { + const previousProgramme = await this.programmeRepo.findOneBy({ + programmeId: programme.programmeId, + }); + + if ( + previousProgramme == null || + programme.txTime == undefined || + previousProgramme.txTime == undefined || + previousProgramme.txTime <= programme.txTime + ) { + try { + let address: any[] = []; + if (programme && programme.programmeProperties) { + if (programme.txType === TxType.CREATE) { + const programmeProperties = programme.programmeProperties; + if (programmeProperties.geographicalLocation) { + for ( + let index = 0; + index < + programmeProperties.geographicalLocation.length; + index++ + ) { + address.push( + programmeProperties.geographicalLocation[index] + ); + } + } + await this.forwardGeocoding([...address]).then( + (response: any) => { + console.log( + "response from forwardGeoCoding function -> ", + response + ); + programme.geographicalLocationCordintes = [ + ...response, + ]; + } ); + } else if ( + programme.txType === TxType.CERTIFY || + programme.txType === TxType.REVOKE + ) { + programme.certifiedTime = programme.txTime; + } else if (programme.txType === TxType.AUTH) { + programme.authTime = programme.txTime; } - } - await this.forwardGeocoding([...address]).then( - (response: any) => { - console.log( - "response from forwardGeoCoding function -> ", - response - ); - programme.geographicalLocationCordintes = [...response]; + + if ( + [TxType.AUTH, TxType.REJECT, TxType.CREATE].includes( + programme.txType + ) + ) { + programme.statusUpdateTime = programme.txTime; + } else if ( + [TxType.ISSUE, TxType.RETIRE, TxType.TRANSFER].includes( + programme.txType + ) + ) { + programme.creditUpdateTime = programme.txTime; } + } + } catch (error) { + console.log( + "Getting cordinates with forward geocoding failed -> ", + error + ); + } finally { + programme.updatedAt = new Date(programme.txTime); + programme.createdAt = new Date(programme.createdTime); + const columns = + this.programmeRepo.manager.connection.getMetadata( + "Programme" + ).columns; + // const columnNames = columns + // .filter(function (item) { + // return ( + // item.propertyName !== "programmeId" && + // item.propertyName !== "geographicalLocationCordintes" + // ); + // }) + // .map((e) => e.propertyName); + const columnNames = columns + .filter(function (item) { + return programme[item.propertyName] != undefined; + }) + .map((e) => e.propertyName); + + this.logger.debug( + `${columnNames} ${JSON.stringify(programme)}` ); + return await this.programmeRepo + .createQueryBuilder() + .insert() + .values(programme) + .orUpdate(columnNames, ["programmeId"]) + .execute(); } + } else { + this.logger.error(`Skipping the programme due to old record ${JSON.stringify(programme)} ${previousProgramme}`) } - } catch (error) { - console.log( - "Getting cordinates with forward geocoding failed -> ", - error - ); - } finally { - programme.updatedAt = new Date(programme.txTime) - programme.createdAt = new Date(programme.createdTime) - const columns = - this.programmeRepo.manager.connection.getMetadata( - "Programme" - ).columns; - const columnNames = columns - .filter(function (item) { - return (item.propertyName !== "programmeId" && item.propertyName !== "geographicalLocationCordintes"); - }) - .map((e) => e.propertyName); - - this.logger.debug(`${columnNames} ${JSON.stringify(programme)}`); - return await this.programmeRepo - .createQueryBuilder() - .insert() - .values(programme) - .orUpdate(columnNames, ["programmeId"]) - .execute(); } } else if ( tableName == this.configService.get("ledger.companyTable") @@ -171,55 +223,66 @@ export class LedgerReplicatorService { companyId: companyId, }); - const meta = JSON.parse( - JSON.stringify( - ionRecord.get("payload").get("revision").get("metadata") - ) - ); + if (company) { + const meta = JSON.parse( + JSON.stringify( + ionRecord.get("payload").get("revision").get("metadata") + ) + ); - if (company && meta["version"]) { - if (company.lastUpdateVersion >= parseInt(meta["version"])) { - return; + if (company && meta["version"]) { + if (company.lastUpdateVersion >= parseInt(meta["version"])) { + return; + } } - } - let updateObj; - if (account) { - if (company.secondaryAccountBalance) { - company.secondaryAccountBalance[account]["total"] = - overall.credit; - company.secondaryAccountBalance[account]["count"] += 1; + let updateObj; + if (account) { + if ( + company.secondaryAccountBalance && + company.secondaryAccountBalance[account] + ) { + company.secondaryAccountBalance[account]["total"] = + overall.credit; + company.secondaryAccountBalance[account]["count"] += 1; + } else { + company.secondaryAccountBalance = { + account: { total: overall.credit, count: 1 }, + }; + } + + updateObj = { + secondaryAccountBalance: company.secondaryAccountBalance, + lastUpdateVersion: parseInt(meta["version"]), + }; } else { - company.secondaryAccountBalance = { - account: { total: overall.credit, count: 1 }, + updateObj = { + creditBalance: overall.credit, + programmeCount: + Number(company.programmeCount) + + (overall.txType == TxType.AUTH ? 1 : 0), + lastUpdateVersion: parseInt(meta["version"]), + creditTxTime: new Date(meta.txTime).getTime(), }; } - updateObj = { - secondaryAccountBalance: company.secondaryAccountBalance, - lastUpdateVersion: parseInt(meta["version"]), - }; + const response = await this.companyRepo + .update( + { + companyId: parseInt(overall.txId), + }, + updateObj + ) + .catch((err: any) => { + this.logger.error(err); + return err; + }); } else { - updateObj = { - creditBalance: overall.credit, - programmeCount: - Number(company.programmeCount) + - (overall.txType == TxType.AUTH ? 1 : 0), - lastUpdateVersion: parseInt(meta["version"]), - }; + this.logger.error( + "Unexpected programme. Company does not found", + companyId + ); } - - const response = await this.companyRepo - .update( - { - companyId: parseInt(overall.txId), - }, - updateObj - ) - .catch((err: any) => { - this.logger.error(err); - return err; - }); } } }) diff --git a/lambda/services/src/main.ts b/lambda/services/src/main.ts index b5941ba11..cc4af7a52 100644 --- a/lambda/services/src/main.ts +++ b/lambda/services/src/main.ts @@ -4,7 +4,7 @@ import { NationalAPIModule } from './national-api/national.api.module'; async function bootstrap() { const app = await NestFactory.create(AnalyticsAPIModule); - app.setGlobalPrefix('/api/analytics') + app.setGlobalPrefix('/stats') await app.listen(3000); } bootstrap(); diff --git a/lambda/services/src/national-api/company.controller.ts b/lambda/services/src/national-api/company.controller.ts index a92f24265..54d631c47 100644 --- a/lambda/services/src/national-api/company.controller.ts +++ b/lambda/services/src/national-api/company.controller.ts @@ -46,13 +46,13 @@ export class CompanyController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, PoliciesGuardEx(true, Action.Update, Company)) + @UseGuards(JwtAuthGuard, PoliciesGuardEx(true, Action.Delete, Company)) @Put('activate') - revoke(@Query('id') companyId: number, @Request() req) { + revoke(@Query('id') companyId: number, @Body() body: OrganisationSuspendDto, @Request() req) { if (companyId == req.user.companyId) { throw new HttpException("Can not activate your own company", HttpStatus.FORBIDDEN) } - return this.companyService.activate(companyId, req.abilityCondition) + return this.companyService.activate(companyId, req.user, body.remarks, req.abilityCondition) } @ApiBearerAuth() diff --git a/lambda/services/src/national-api/handler.ts b/lambda/services/src/national-api/handler.ts index 7bc775385..46b9030f3 100644 --- a/lambda/services/src/national-api/handler.ts +++ b/lambda/services/src/national-api/handler.ts @@ -8,7 +8,7 @@ import { NationalAPIModule } from './national.api.module'; let cachedServer: Server; export const handler: Handler = async (event: any, context: Context) => { - const httpBase = '/api/national' + const httpBase = '/national' // event.path = event.path.includes('swagger-ui') ? `${event.path}` : event.path cachedServer = await bootstrapServer(cachedServer, NationalAPIModule, httpBase); return proxy(cachedServer, event, context, 'PROMISE').promise; diff --git a/lambda/services/src/national-api/programme.controller.ts b/lambda/services/src/national-api/programme.controller.ts index 01402f5ee..55b4f8341 100644 --- a/lambda/services/src/national-api/programme.controller.ts +++ b/lambda/services/src/national-api/programme.controller.ts @@ -56,7 +56,7 @@ export class ProgrammeController { @UseGuards(ApiKeyJwtAuthGuard, PoliciesGuardEx(true, Action.Read, Programme, true)) @Get('getHistory') async getHistory(@Query('programmeId') programmeId: string, @Request() req) { - return this.programmeService.getProgrammeEvents(programmeId, req.user.companyId) + return this.programmeService.getProgrammeEvents(programmeId, req.user) } @ApiBearerAuth() @@ -127,14 +127,14 @@ export class ProgrammeController { @UseGuards(ApiKeyJwtAuthGuard, PoliciesGuardEx(true, Action.Delete, ProgrammeTransfer)) @Post('transferReject') async transferReject(@Body() body: ProgrammeTransferReject, @Request() req) { - return this.programmeService.transferReject(body, req.user.companyId) + return this.programmeService.transferReject(body, req.user) } @ApiBearerAuth() @UseGuards(ApiKeyJwtAuthGuard, PoliciesGuardEx(true, Action.Delete, ProgrammeTransfer)) @Post('transferCancel') async transferCancel(@Body() body: ProgrammeTransferCancel, @Request() req) { - return this.programmeService.transferCancel(body, req.user.companyId) + return this.programmeService.transferCancel(body, req.user) } @ApiBearerAuth() @@ -142,6 +142,14 @@ export class ProgrammeController { @Post('transferQuery') queryUser(@Body()query: QueryDto, @Request() req) { console.log(req.abilityCondition) - return this.programmeService.queryProgrammeTransfers(query, req.abilityCondition) + return this.programmeService.queryProgrammeTransfers(query, req.abilityCondition, req.user) + } + + @ApiBearerAuth() + @UseGuards(ApiKeyJwtAuthGuard, PoliciesGuardEx(true, Action.Read, ProgrammeTransfer, true)) + @Get('transfersByProgrammeId') + transfersByProgrammeId(@Query('programmeId') programmeId: string, @Request() req) { + console.log(req.abilityCondition) + return this.programmeService.getTransferByProgrammeId(programmeId, req.abilityCondition, req.user) } } \ No newline at end of file diff --git a/lambda/services/src/shared/auth/auth.module.ts b/lambda/services/src/shared/auth/auth.module.ts index 3a56a1aae..12b3c4747 100644 --- a/lambda/services/src/shared/auth/auth.module.ts +++ b/lambda/services/src/shared/auth/auth.module.ts @@ -18,7 +18,7 @@ import { UserModule } from "../user/user.module"; useFactory: async (configService: ConfigService) => ({ secretOrPrivateKey: configService.get("jwt.userSecret"), signOptions: { - expiresIn: 3600, + expiresIn: 3600*2, }, }), inject: [ConfigService], diff --git a/lambda/services/src/shared/company/company.module.ts b/lambda/services/src/shared/company/company.module.ts index 5117aaf00..a1a49b40c 100644 --- a/lambda/services/src/shared/company/company.module.ts +++ b/lambda/services/src/shared/company/company.module.ts @@ -1,4 +1,4 @@ -import { Logger, Module } from '@nestjs/common'; +import { forwardRef, Logger, Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Company } from '../entities/company.entity'; @@ -10,6 +10,7 @@ import { CompanyService } from './company.service'; import { UtilModule } from '../util/util.module'; import { ProgrammeLedgerModule } from '../programme-ledger/programme-ledger.module'; import { ProgrammeTransfer } from '../entities/programme.transfer'; +import { EmailHelperModule } from '../email-helper/email-helper.module'; @Module({ imports: [ @@ -26,7 +27,8 @@ import { ProgrammeTransfer } from '../entities/programme.transfer'; CaslModule, EmailModule, UtilModule, - ProgrammeLedgerModule + ProgrammeLedgerModule, + forwardRef(() => EmailHelperModule) ], providers: [CompanyService, Logger], exports: [CompanyService] diff --git a/lambda/services/src/shared/company/company.service.ts b/lambda/services/src/shared/company/company.service.ts index bb4f60073..b630da9aa 100644 --- a/lambda/services/src/shared/company/company.service.ts +++ b/lambda/services/src/shared/company/company.service.ts @@ -1,5 +1,5 @@ import { PG_UNIQUE_VIOLATION } from "@drdgvhbh/postgres-error-codes"; -import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; +import { forwardRef, HttpException, HttpStatus, Inject, Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { InjectRepository } from "@nestjs/typeorm"; import { OrganisationDto } from "../dto/organisation.dto"; @@ -18,6 +18,10 @@ import { DataResponseDto } from "../dto/data.response.dto"; import { ProgrammeTransfer } from "../entities/programme.transfer"; import { TransferStatus } from "../enum/transform.status.enum"; import { User } from "../entities/user.entity"; +import { EmailHelperService } from "../email-helper/email-helper.service"; +import { Programme } from "../entities/programme.entity"; +import { EmailTemplates } from "../email/email.template"; +import { SystemActionType } from "../enum/system.action.type"; @Injectable() export class CompanyService { @@ -27,6 +31,8 @@ export class CompanyService { private configService: ConfigService, private helperService: HelperService, private programmeLedgerService: ProgrammeLedgerService, + @Inject(forwardRef(() => EmailHelperService)) + private emailHelperService: EmailHelperService, @InjectRepository(ProgrammeTransfer) private programmeTransferRepo: Repository ) {} @@ -75,17 +81,28 @@ export class CompanyService { if (company.companyRole === CompanyRole.PROGRAMME_DEVELOPER) { await this.programmeLedgerService.freezeCompany( companyId, - remarks, - user, - company.name + this.getUserRefWithRemarks(user, `${remarks}#${company.name}`), + true ); - await this.companyTransferCancel(companyId); + await this.companyTransferCancel(companyId, `${remarks}#${user.companyId}#${user.id}#${SystemActionType.SUSPEND_AUTO_CANCEL}#${company.name}`); + await this.emailHelperService.sendEmail(company.email,EmailTemplates.PROGRAMME_DEVELOPER_ORG_DEACTIVATION,{},user.companyId) } else if (company.companyRole === CompanyRole.CERTIFIER) { await this.programmeLedgerService.revokeCompanyCertifications( companyId, - remarks, - user.id.toString() + this.getUserRefWithRemarks(user, `${remarks}#${SystemActionType.SUSPEND_REVOKE}#${company.name}`), + async (programme:Programme) => { + const hostAddress = this.configService.get("host"); + await this.emailHelperService.sendEmailToProgrammeOwnerAdmins(programme.programmeId,EmailTemplates.PROGRAMME_CERTIFICATION_REVOKE_BY_SYSTEM,{ + organisationName: company.name, + programmeName: programme.title, + credits: programme.creditBalance, + serialNumber: programme.serialNo, + pageLink: hostAddress + `/programmeManagement/view?id=${programme.programmeId}` + }) + } ); + + await this.emailHelperService.sendEmail(company.email,EmailTemplates.CERTIFIER_ORG_DEACTIVATION,{},user.companyId) } return new BasicResponseDto( HttpStatus.OK, @@ -98,7 +115,7 @@ export class CompanyService { ); } - async activate(companyId: number, abilityCondition: string): Promise { + async activate(companyId: number,user: User, remarks: string, abilityCondition: string): Promise { this.logger.verbose("revoke company", companyId); const company = await this.companyRepo .createQueryBuilder() @@ -132,6 +149,12 @@ export class CompanyService { }); if (result.affected > 0) { + await this.programmeLedgerService.freezeCompany( + companyId, + this.getUserRefWithRemarks(user, `${remarks}#${company.name}`), + false + ); + await this.emailHelperService.sendEmail(company.email,EmailTemplates.ORG_REACTIVATION,{},user.companyId); return new BasicResponseDto( HttpStatus.OK, "Successfully activated company" @@ -168,7 +191,8 @@ export class CompanyService { .createQueryBuilder() .select([ '"companyId"', - '"name"' + '"name"', + '"state"' ]) .where( this.helperService.generateWhereSQL( @@ -307,11 +331,11 @@ export class CompanyService { ); } - async companyTransferCancel(companyId: number) { + async companyTransferCancel(companyId: number, remark: string) { await this.programmeTransferRepo .createQueryBuilder() .update(ProgrammeTransfer) - .set({ status: TransferStatus.CANCELLED }) + .set({ status: TransferStatus.CANCELLED, txRef: remark }) .where( "(fromCompanyId = :companyId OR toCompanyId = :companyId) AND status = :status", { @@ -325,4 +349,8 @@ export class CompanyService { return err; }); } + + private getUserRefWithRemarks = (user: any, remarks: string) => { + return `${user.companyId}#${user.companyName}#${user.id}#${remarks}`; +} } diff --git a/lambda/services/src/shared/configuration.ts b/lambda/services/src/shared/configuration.ts index eb1b0a33f..1ecd084e1 100644 --- a/lambda/services/src/shared/configuration.ts +++ b/lambda/services/src/shared/configuration.ts @@ -1,6 +1,7 @@ export default () => ({ stage: process.env.STAGE || 'local', systemCountry: process.env.systemCountryCode || 'NG', + systemCountryName: process.env.systemCountryName || 'Antarctic Region', defaultCreditUnit: process.env.defaultCreditUnit || 'ITMO', dateTimeFormat: 'DD LLLL yyyy @ HH:mm', dateFormat: 'DD LLLL yyyy', @@ -34,5 +35,8 @@ export default () => ({ }, s3CommonBucket: { name: 'carbon-common-'+ (process.env.NODE_ENV || 'dev'), - } + }, + host: process.env.HOST || 'https://test.carbreg.org', + liveChat : 'https://undp2020cdo.typeform.com/to/emSWOmDo', + helpDocumentation: 'carbreg.org/help', }); \ No newline at end of file diff --git a/lambda/services/src/shared/dto/aggr.entry.ts b/lambda/services/src/shared/dto/aggr.entry.ts index 7e44cd209..1d4c18500 100644 --- a/lambda/services/src/shared/dto/aggr.entry.ts +++ b/lambda/services/src/shared/dto/aggr.entry.ts @@ -27,6 +27,12 @@ export class AggrEntry { @IsOptional() outerQuery?: string; + @IsNotEmpty() + @IsString() + @ApiPropertyOptional() + @IsOptional() + mineCompanyId?: boolean; + // @IsNotEmpty() // @IsString() // @ApiPropertyOptional() diff --git a/lambda/services/src/shared/dto/programme.retire.ts b/lambda/services/src/shared/dto/programme.retire.ts index 579e1df1f..322c9698a 100644 --- a/lambda/services/src/shared/dto/programme.retire.ts +++ b/lambda/services/src/shared/dto/programme.retire.ts @@ -1,6 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Type } from "class-transformer"; -import { IsArray, IsEnum, IsInt, IsNotEmpty, IsNumber, IsOptional, IsString, Length, Min, ValidateNested } from "class-validator"; +import { IsArray, IsEnum, IsInt, IsNotEmpty, IsNotEmptyObject, IsNumber, IsOptional, IsPositive, IsString, Length, Min, ValidateIf, ValidateNested } from "class-validator"; import { RetireType } from "../enum/retire.type.enum"; import { BasicOrgInfo } from "./basic.organisation.dto"; @@ -19,7 +19,6 @@ export class ProgrammeRetire { @ApiPropertyOptional() @IsArray() - @IsNumber({},{each: true}) @Min(0, { each: true }) @IsOptional() companyCredit: number[]; @@ -31,9 +30,8 @@ export class ProgrammeRetire { // toAccount: string; @ApiPropertyOptional() - @IsNotEmpty() - @IsNotEmpty() - @IsOptional() + @ValidateIf(o => o.type === RetireType.CROSS_BORDER) + @IsNotEmptyObject() @ValidateNested() @Type(() => BasicOrgInfo) toCompanyMeta: BasicOrgInfo; diff --git a/lambda/services/src/shared/dto/programmeStatus.timeGrouped.result.ts b/lambda/services/src/shared/dto/programmeStatus.timeGrouped.result.ts new file mode 100644 index 000000000..e76363f5f --- /dev/null +++ b/lambda/services/src/shared/dto/programmeStatus.timeGrouped.result.ts @@ -0,0 +1,15 @@ +export class StatusGroupedByTimedata { + awaitingAuthorization: any[]; + authorised: any[]; + rejected: any[]; + authorisedCredits: any[]; + issuedCredits: any[]; + transferredCredits: any[]; + retiredCredits: any[]; +} + +export class StatusGroupedByTimedataThere { + awaitingAuthorization: boolean; + authorised: boolean; + rejected: boolean; +} diff --git a/lambda/services/src/shared/dto/sort.entry.ts b/lambda/services/src/shared/dto/sort.entry.ts index d544dd1b9..20ffc0a7b 100644 --- a/lambda/services/src/shared/dto/sort.entry.ts +++ b/lambda/services/src/shared/dto/sort.entry.ts @@ -1,14 +1,22 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Type } from "class-transformer"; -import { IsInt, IsNotEmpty, IsNumber, IsPositive, IsString } from "class-validator"; +import { + IsInt, + IsNotEmpty, + IsNumber, + IsPositive, + IsString, +} from "class-validator"; export class SortEntry { + @IsNotEmpty() + @ApiProperty() + key: any; - @IsNotEmpty() - @ApiProperty() - key: any; + @IsNotEmpty() + @ApiProperty() + order: any; - @IsNotEmpty() - @ApiProperty() - order: any; -} \ No newline at end of file + @ApiPropertyOptional() + nullFirst?: boolean; +} diff --git a/lambda/services/src/shared/dto/stat.dto.ts b/lambda/services/src/shared/dto/stat.dto.ts index f87ceb78a..0d3c36e8c 100644 --- a/lambda/services/src/shared/dto/stat.dto.ts +++ b/lambda/services/src/shared/dto/stat.dto.ts @@ -15,6 +15,8 @@ export class Stat extends EntitySubject { value?: any; data?: any; + key?: string; + @ApiPropertyOptional() @Type(() => StatFilter) statFilter: StatFilter; diff --git a/lambda/services/src/shared/email-helper/email-helper.module.ts b/lambda/services/src/shared/email-helper/email-helper.module.ts new file mode 100644 index 000000000..3636521d4 --- /dev/null +++ b/lambda/services/src/shared/email-helper/email-helper.module.ts @@ -0,0 +1,13 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { CompanyModule } from '../company/company.module'; +import { EmailModule } from '../email/email.module'; +import { ProgrammeLedgerModule } from '../programme-ledger/programme-ledger.module'; +import { UserModule } from '../user/user.module'; +import { EmailHelperService } from './email-helper.service'; + +@Module({ + providers: [EmailHelperService], + exports: [EmailHelperService], + imports: [forwardRef(() => UserModule), EmailModule, ProgrammeLedgerModule, forwardRef(() => CompanyModule)] +}) +export class EmailHelperModule {} diff --git a/lambda/services/src/shared/email-helper/email-helper.service.ts b/lambda/services/src/shared/email-helper/email-helper.service.ts new file mode 100644 index 000000000..5dc434f67 --- /dev/null +++ b/lambda/services/src/shared/email-helper/email-helper.service.ts @@ -0,0 +1,321 @@ +import { forwardRef, Inject, Injectable } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { CompanyService } from "../company/company.service"; +import { EmailService } from "../email/email.service"; +import { Company } from "../entities/company.entity"; +import { Programme } from "../entities/programme.entity"; +import { ProgrammeLedgerService } from "../programme-ledger/programme-ledger.service"; +import { UserService } from "../user/user.service"; + +@Injectable() +export class EmailHelperService { + constructor( + @Inject(forwardRef(() => UserService)) + private userService: UserService, + private configService: ConfigService, + private emailService: EmailService, + @Inject(forwardRef(() => CompanyService)) + private companyService: CompanyService, + private programmeLedger: ProgrammeLedgerService + ) {} + + public async sendEmailToProgrammeOwnerAdmins( + programmeId: string, + template: any, + templateData: {}, + companyId?: number, + governmentId?: number + ) { + const programme = await this.programmeLedger.getProgrammeById(programmeId); + const hostAddress = this.configService.get("host"); + let companyDetails: Company; + + switch (template.id) { + case "PROGRAMME_REJECTION": + templateData = { + ...templateData, + programmeName: programme.title, + date: new Date(programme.txTime), + pageLink: hostAddress + `/programmeManagement/view?id=${programmeId}`, + }; + break; + + case "PROGRAMME_CERTIFICATION": + companyDetails = await this.companyService.findByCompanyId(companyId); + templateData = { + ...templateData, + programmeName: programme.title, + credits: programme.creditBalance, + serialNumber: programme.serialNo, + organisationName: companyDetails.name, + pageLink: hostAddress + `/programmeManagement/view?id=${programmeId}`, + }; + break; + + case "PROGRAMME_CERTIFICATION_REVOKE_BY_CERT": + companyDetails = await this.companyService.findByCompanyId(companyId); + templateData = { + ...templateData, + programmeName: programme.title, + credits: programme.creditBalance, + serialNumber: programme.serialNo, + organisationName: companyDetails.name, + pageLink: hostAddress + `/programmeManagement/view?id=${programmeId}`, + }; + break; + + case "PROGRAMME_CERTIFICATION_REVOKE_BY_GOVT_TO_PROGRAMME": + companyDetails = await this.companyService.findByCompanyId(companyId); + const government = await this.companyService.findByCompanyId( + governmentId + ); + templateData = { + ...templateData, + programmeName: programme.title, + credits: programme.creditBalance, + serialNumber: programme.serialNo, + organisationName: companyDetails.name, + government: government.name, + pageLink: hostAddress + `/programmeManagement/view?id=${programmeId}`, + }; + break; + + default: + break; + } + + programme.companyId.forEach(async (companyId: number) => { + this.sendEmailToOrganisationAdmins(companyId, template, templateData); + }); + } + + public async sendEmailToOrganisationAdmins( + companyId: number, + template, + templateData: any, + receiverCompanyId?: number, + programmeId?: string + ) { + const systemCountryName = this.configService.get("systemCountryName"); + const users = await this.userService.getOrganisationAdminAndManagerUsers( + companyId + ); + let companyDetails: Company; + let programme: Programme; + const hostAddress = this.configService.get("host"); + + switch (template.id) { + case "CREDIT_TRANSFER_GOV": + companyDetails = await this.companyService.findByCompanyId( + receiverCompanyId + ); + templateData = { + ...templateData, + organisationName: companyDetails.name, + }; + break; + + case "CREDIT_TRANSFER_CANCELLATION": + programme = await this.programmeLedger.getProgrammeById(programmeId); + companyDetails = await this.companyService.findByCompanyId( + receiverCompanyId + ); + templateData = { + ...templateData, + organisationName: companyDetails.name, + serialNumber: programme.serialNo, + programmeName: programme.title, + pageLink: hostAddress + "/creditTransfers/viewAll", + }; + break; + + case "CREDIT_TRANSFER_ACCEPTED": + companyDetails = await this.companyService.findByCompanyId( + receiverCompanyId + ); + programme = await this.programmeLedger.getProgrammeById(programmeId); + templateData = { + ...templateData, + organisationName: companyDetails.name, + serialNumber: programme.serialNo, + programmeName: programme.title, + pageLink: hostAddress + "/creditTransfers/viewAll", + }; + break; + + case "CREDIT_TRANSFER_REJECTED": + companyDetails = await this.companyService.findByCompanyId( + receiverCompanyId + ); + programme = await this.programmeLedger.getProgrammeById(programmeId); + templateData = { + ...templateData, + organisationName: companyDetails.name, + serialNumber: programme.serialNo, + programmeName: programme.title, + pageLink: hostAddress + "/creditTransfers/viewAll", + }; + break; + + case "CREDIT_TRANSFER_GOV_CANCELLATION": + companyDetails = await this.companyService.findByCompanyId( + receiverCompanyId + ); + programme = await this.programmeLedger.getProgrammeById(programmeId); + templateData = { + ...templateData, + organisationName: companyDetails.name, + serialNumber: programme.serialNo, + programmeName: programme.title, + pageLink: hostAddress + "/creditTransfers/viewAll", + }; + break; + + case "CREDIT_TRANSFER_GOV_ACCEPTED_TO_RECEIVER": + companyDetails = await this.companyService.findByCompanyId( + receiverCompanyId + ); + programme = await this.programmeLedger.getProgrammeById(programmeId); + templateData = { + ...templateData, + organisationName: companyDetails.name, + serialNumber: programme.serialNo, + programmeName: programme.title, + pageLink: hostAddress + "/creditTransfers/viewAll", + }; + break; + + case "PROGRAMME_CERTIFICATION_REVOKE_BY_GOVT_TO_CERT": + companyDetails = await this.companyService.findByCompanyId( + receiverCompanyId + ); + programme = await this.programmeLedger.getProgrammeById(programmeId); + templateData = { + ...templateData, + government: companyDetails.name, + serialNumber: programme.serialNo, + programmeName: programme.title, + credits: programme.creditBalance, + pageLink: hostAddress + `/programmeManagement/view?id=${programmeId}`, + }; + break; + + case "CREDIT_RETIREMENT_RECOGNITION": + programme = await this.programmeLedger.getProgrammeById(programmeId); + templateData = { + ...templateData, + serialNumber: programme.serialNo, + programmeName: programme.title, + pageLink: hostAddress + "/creditTransfers/viewAll", + }; + break; + + case "CREDIT_RETIREMENT_NOT_RECOGNITION": + programme = await this.programmeLedger.getProgrammeById(programmeId); + templateData = { + ...templateData, + serialNumber: programme.serialNo, + programmeName: programme.title, + pageLink: hostAddress + "/creditTransfers/viewAll", + }; + break; + + default: + break; + } + + users.forEach(async (user: any) => { + templateData = { + ...templateData, + name: user.user_name, + countryName: systemCountryName, + }; + await this.emailService.sendEmail( + user.user_email, + template, + templateData + ); + }); + } + + public async sendEmailToGovernmentAdmins( + template, + templateData: any, + programmeId?: string, + companyId?: number + ) { + const systemCountryName = this.configService.get("systemCountryName"); + const hostAddress = this.configService.get("host"); + const users = await this.userService.getGovAdminAndManagerUsers(); + let programme: Programme; + let companyDetails: Company; + if (programmeId) + programme = await this.programmeLedger.getProgrammeById(programmeId); + + switch (template.id) { + case "CREDIT_TRANSFER_GOV_ACCEPTED_TO_INITIATOR": + companyDetails = await this.companyService.findByCompanyId(companyId); + templateData = { + ...templateData, + organisationName: companyDetails.name, + serialNumber: programme.serialNo, + programmeName: programme.title, + pageLink: hostAddress + "/creditTransfers/viewAll", + }; + break; + + case "CREDIT_TRANSFER_GOV_REJECTED": + companyDetails = await this.companyService.findByCompanyId(companyId); + templateData = { + ...templateData, + organisationName: companyDetails.name, + serialNumber: programme.serialNo, + programmeName: programme.title, + pageLink: hostAddress + "/creditTransfers/viewAll", + }; + break; + + case "CREDIT_RETIREMENT_CANCEL": + companyDetails = await this.companyService.findByCompanyId(companyId); + templateData = { + ...templateData, + serialNumber: programme.serialNo, + programmeName: programme.title, + pageLink: hostAddress + "/creditTransfers/viewAll", + }; + break; + + default: + break; + } + + users.forEach(async (user: any) => { + templateData = { + ...templateData, + name: user.user_name, + countryName: systemCountryName, + }; + await this.emailService.sendEmail( + user.user_email, + template, + templateData + ); + }); + } + + public async sendEmail( + sender: string, + template, + templateData: any, + companyId: number + ) { + const companyDetails = await this.companyService.findByCompanyId(companyId); + const systemCountryName = this.configService.get("systemCountryName"); + templateData = { + ...templateData, + countryName: systemCountryName, + government: companyDetails.name, + }; + await this.emailService.sendEmail(sender, template, templateData); + } +} diff --git a/lambda/services/src/shared/email/email.service.ts b/lambda/services/src/shared/email/email.service.ts index 6f9ce3200..e19daf4c8 100644 --- a/lambda/services/src/shared/email/email.service.ts +++ b/lambda/services/src/shared/email/email.service.ts @@ -1,7 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import nodemailer = require('nodemailer'); -import { EmailTemplates } from './email.template'; @Injectable() export class EmailService { @@ -30,22 +29,39 @@ export class EmailService { } for (const key in data) { if (data.hasOwnProperty(key)) { - template = template.replace(`{{${key}}}`, data[key]) + var find = `{{${key}}}`; + var re = new RegExp(find, 'g'); + template = template.replace(re, data[key]); } } return template; } + private getSubjectMessage(template: string, data) :string{ + if (template == undefined) { + return template; + } + for (const key in data) { + if (data.hasOwnProperty(key)) { + var find = `{{${key}}}`; + var re = new RegExp(find, 'g'); + template = template.replace(re, data[key]); + } + } + + return `🏭📋 🇦🇶 Carbon Registry: ${template}`; + } + public async sendEmail(sendToEmail: string, template, templateData: any): Promise { this.logger.log('Sending email', JSON.stringify(sendToEmail)) // this.configService.get('stage') != 'local' && !sendToEmail.endsWith(this.configService.get('email.skipSuffix')) - if (sendToEmail && sendToEmail.endsWith('@undp.org')) { + if (sendToEmail && !sendToEmail.endsWith('@xeptagon.com')) { return new Promise((resolve, reject) => { this.transporter.sendMail({ from: this.sourceEmail, to: sendToEmail, - subject: template["subject"], + subject: this.getSubjectMessage(template["subject"], templateData), text: this.getTemplateMessage(template["text"], templateData), // plain text body html: this.getTemplateMessage(template["html"], templateData), // html body }, function(error, info) { diff --git a/lambda/services/src/shared/email/email.template.ts b/lambda/services/src/shared/email/email.template.ts index f3cd71df6..e6d256739 100644 --- a/lambda/services/src/shared/email/email.template.ts +++ b/lambda/services/src/shared/email/email.template.ts @@ -1,16 +1,48 @@ export const EmailTemplates = { - REGISTER_EMAIL: { - subject: 'Welcome to Carbon Credit Registry', + ORGANISATION_CREATE:{ + id: 'ORGANISATION_CREATE', + subject: 'Welcome!', html: ` - Hi {{name}},

+ Welcome {{organisationName}},

+ Your Organisation has been registered with the {{countryName}} Carbon Registry as a {{organisationRole}} Organisation.

+ Explore the Registry here {{home}}.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + USER_CREATE: { + id: 'USER_CREATE', + subject: 'Welcome!', + html: ` + Welcome {{name}},

+ + Your account has been created for the {{countryName}} Carbon Credit + Registry. You can access your account using the temporary Homepage: {{home}}

+ + User: {{email}}
+ Password (temporary): {{tempPassword}}

- Welcome to Carbon Credit Registry - {{countryName}}.
Your account has been created. -
You can access using your temporary password: {{password}} - {{apiKeyText}} + If you have any questions, feel free to email our customer success + team customer success team + (We’re lightning quick at replying.) We also offer live chat {{liveChat}}.

- Sincerely,
- The Carbon Credit Registry Team - `, + + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team

+ +
+ P.S.Need immediate help getting started? Check out our help + documentation {{helpDoc}}. Or, just reply to this email, the + {{countryName}} Carbon Credit Registry Team is always ready to help! +
+
+
+ United Nations Development Programme
+ 1 United Nations Plaza
+ New York, NY USA 10001 +
+ `, text: '' }, API_KEY_EMAIL: { @@ -50,5 +82,388 @@ export const EmailTemplates = { The Carbon Credit Registry Team `, text: '' + }, + CHANGE_PASSOWRD: { + id: 'CHANGE_PASSOWRD', + subject: 'Your password was changed', + html: ` + Hi {{name}},

+ The password of your Carbon Registry account was changed successfully.

+ If you do not use {{countryName}} Carbon Credit Registry or did not request a password reset, please ignore this email or + contact support + if you have questions. + +

+ Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + `, + text: '' + }, + PROGRAMME_CREATE: { + id: 'PROGRAMME_CREATE', + subject: 'New Programme Received for Authorisation', + html:` + Hi {{name}},

+ + A new programme owned by {{organisationName}} is awaiting authorisation.

+ + Click here to access all the programmes that require authorisation. +

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + PROGRAMME_AUTHORISATION: { + id: 'PROGRAMME_AUTHORISATION', + subject: 'Programme authorised', + html:` + Hi {{name}},

+ + {{programmeName}} of your organisation has been authorised on {{authorisedDate}} with the serial number {{serialNumber}}. +

+ + Click here for more details of the programme. +

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + PROGRAMME_REJECTION: { + id: 'PROGRAMME_REJECTION', + subject: 'Programme Rejected', + html: ` + Hi {{name}},

+ + {{programmeName}} of your Organisation has been rejected on {{date}} due to the following reason/s:
+ {{reason}}

+ + Click here {{pageLink}} for more details of the programme.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_ISSUANCE: { + id: 'CREDIT_ISSUANCE', + subject: 'Credits Issued', + html: ` + Hi {{name}},

+ + {{programmeName}} of your Organisation with the serial number {{serialNumber}} has been issued with {{credits}} credits.

+ + Click here {{pageLink}} for more details of the programme.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_TRANSFER_REQUISITIONS: { + id: 'CREDIT_TRANSFER_REQUISITIONS', + subject: 'Transfer Request Received', + html: ` + Hi {{name}},

+ + {{organisationName}} has requested to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}}.

+ Click here {{pageLink}} for more details of the transfer request.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_TRANSFER_CANCELLATION: { + id: 'CREDIT_TRANSFER_CANCELLATION', + subject: 'Transfer Request Cancelled', + html: ` + Hi {{name}},

+ + Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} made by {{organisationName}} has been cancelled.

+ Click here {{pageLink}} for more details of the transfer request.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_TRANSFER_ACCEPTED: { + id: 'CREDIT_TRANSFER_ACCEPTED', + subject: 'Transfer Request Accepted', + html: ` + Hi {{name}},

+ + Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} made by your organisation has been accepted by {{organisationName}}.

+ Click here {{pageLink}} for more details of the transfer request.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_TRANSFER_REJECTED: { + id: 'CREDIT_TRANSFER_REJECTED', + subject: 'Transfer Request Rejected', + html: ` + Hi {{name}},

+ + Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} + made by your organisation has been rejected by {{organisationName}}.

+ Click here {{pageLink}} for more details of the transfer request.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team` + }, + CREDIT_TRANSFER_GOV:{ + id: 'CREDIT_TRANSFER_GOV', + subject: 'Transfer Request Received', + html: ` + Hi {{name}},

+ + {{government}} has requested your organisation to transfer {{credits}} credits with the serial number {{serialNumber}} + from {{programmeName}} to {{organisationName}}.

+ + Click here {{pageLink}} for more details of the transfer request.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_TRANSFER_GOV_CANCELLATION: { + id: 'CREDIT_TRANSFER_GOV_CANCELLATION', + subject: 'Transfer Request Cancelled', + html: ` + Hi {{name}},

+ + Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} + to {{organisationName}} made by {{government}} has been cancelled.

+ + Click here {{pageLink}} for more details of the transfer request.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_TRANSFER_GOV_ACCEPTED_TO_INITIATOR: { + id: 'CREDIT_TRANSFER_GOV_ACCEPTED_TO_INITIATOR', + subject: 'Transfer Request Accepted', + html: ` + Hi {{name}},

+ + Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} made by your organisation has been accepted by {{organisationName}}.

+ Click here {{pageLink}} for more details of the transfer request.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_TRANSFER_GOV_ACCEPTED_TO_RECEIVER: { + id: 'CREDIT_TRANSFER_GOV_ACCEPTED_TO_RECEIVER', + subject: 'Credits Received', + html: ` + Hi {{name}},

+ + {{organisationName}} has transferred {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to your organisation by accepting the request made by the {{government}}.

+ Click here {{pageLink}} for more details of the transfer request

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_TRANSFER_GOV_REJECTED: { + id: 'CREDIT_TRANSFER_GOV_REJECTED', + subject: 'Transfer Request Rejected', + html: ` + Hi {{name}},

+ + Request to transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} made by your organisation has been rejected by {{organisationName}}.

+ Click here {{pageLink}} for more details of the transfer request

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_SEND_DEVELOPER:{ + id: 'CREDIT_SEND_DEVELOPER', + subject: 'Credits Received', + html: ` + Hi {{name}},

+ + {{organisationName}} has transferred {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to your organisation.

+ + Click here {{pageLink}} for more details of the transfer request.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + PROGRAMME_CERTIFICATION:{ + id: 'PROGRAMME_CERTIFICATION', + subject: 'Programme Certified by {{organisationName}}', + html: ` + Hi {{name}},

+ + The {{programmeName}} containing {{credits}} credits with the serial number {{serialNumber}} of your Organisation has been certified by {{organisationName}}.

+ Click here {{pageLink}} for more details of the certification.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + PROGRAMME_CERTIFICATION_REVOKE_BY_CERT:{ + id: 'PROGRAMME_CERTIFICATION_REVOKE_BY_CERT', + subject: 'Programme Certificate Revoked by {{organisationName}}', + html: ` + Hi {{name}},

+ + The certification of the programme {{programmeName}} containing {{credits}} credits with the serial number {{serialNumber}} has been revoked by {{organisationName}}.

+ Click here {{pageLink}} for more details of the certification.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + PROGRAMME_CERTIFICATION_REVOKE_BY_GOVT_TO_PROGRAMME:{ + id: 'PROGRAMME_CERTIFICATION_REVOKE_BY_GOVT_TO_PROGRAMME', + subject: 'Programme Certificate Revoked by {{government}}', + html: ` + Hi {{name}},

+ + The certification given by {{organisationName}} for the programme {{programmeName}} containing {{credits}} credits with the serial number {{serialNumber}} has been revoked by the {{government}}.

+ Click here {{pageLink}} for more details of the certification.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + PROGRAMME_CERTIFICATION_REVOKE_BY_GOVT_TO_CERT:{ + id: 'PROGRAMME_CERTIFICATION_REVOKE_BY_GOVT_TO_CERT', + subject: 'Programme Certificate Revoked by {{government}}', + html: ` + Hi {{name}},

+ + The certification given by your organisation for the programme {{programmeName}} containing {{credits}} credits with the serial number {{serialNumber}} has been revoked by the {{government}}.

+ Click here {{pageLink}} for more details of the certification.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + PROGRAMME_CERTIFICATION_REVOKE_BY_SYSTEM:{ + id: 'PROGRAMME_CERTIFICATION_REVOKE_BY_SYSTEM', + subject: 'Programme Certificate Revoked by the System', + html: ` + Hi {{name}},

+ + The certification given by {{organisationName}} for the programme {{programmeName}} containing {{credits}} credits with the serial number {{serialNumber}} has been revoked by the system as {{organisationName}} was deactivated.

+ Click here {{pageLink}} for more details of the certification.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + PROGRAMME_DEVELOPER_ORG_DEACTIVATION:{ + id: 'PROGRAMME_DEVELOPER_ORG_DEACTIVATION', + subject: 'Organisation Deactivated', + html: ` + Hi,

+ + Your organisation has been deactivated by the {{government}}. Your organisation will still be visible but not other will be able to take place. Following are the effects of deactivation:

+ · All the users of the organisation were deactivated.
+ · All the credits owned by your organisation were frozen.
+ · All credit transfer requests sent and received by your organisation were cancelled.
+ · All the international transfer retire requests sent by your organisation were cancelled.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CERTIFIER_ORG_DEACTIVATION: { + id: 'CERTIFIER_ORG_DEACTIVATION', + subject: 'Organisation Deactivated', + html: ` + Hi,

+ + Your organisation has been deactivated by the {{government}}. Your organisation will still be visible but not other will be able to take place. Following are the effects of deactivation:

+ · All the users of the organisation were deactivated.
+ · All the certificates given by your organisation were revoked.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_RETIREMENT_BY_GOV:{ + id: 'CREDIT_RETIREMENT_BY_GOV', + subject: 'Credits Retired', + html: ` + Hi {{name}},

+ + {{credits}} credits of the programme {{programmeName}} with the serial number {{serialNumber}} has been retired by the {{government}} as {{reason}}.

+ Click here {{pageLink}} for more details of the retirement.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_RETIREMENT_BY_DEV:{ + id: 'CREDIT_RETIREMENT_BY_DEV', + subject: 'International Transfer Retire Request Received', + html: ` + Hi {{name}},

+ + {{organisationName}} has requested an international transfer retirement of {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}}.

+ Click here {{pageLink}} for more details of the international transfer retire request.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_RETIREMENT_CANCEL:{ + id: 'CREDIT_RETIREMENT_CANCEL', + subject: 'International Transfer Retire Request Cancelled', + html: ` + Hi {{name}},

+ + Request to internationally transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to {{country}} made by {{organisationName}} has been cancelled.

+ Click here {{pageLink}} for more details of the international transfer retire request.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_RETIREMENT_RECOGNITION:{ + id: 'CREDIT_RETIREMENT_RECOGNITION', + subject: 'International Transfer Retire Request Recognised', + html: ` + Hi {{name}},

+ + Request to internationally transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to {{country}} made by your organisation has been recognised.

+ Click here {{pageLink}} for more details of the international transfer retire request.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + CREDIT_RETIREMENT_NOT_RECOGNITION:{ + id: 'CREDIT_RETIREMENT_NOT_RECOGNITION', + subject: 'International Transfer Retire Request Not Recognised', + html: ` + Hi {{name}},

+ + Request to internationally transfer {{credits}} credits with the serial number {{serialNumber}} from {{programmeName}} to {{country}} made by your organisation has not been recognised.

+ Click here {{pageLink}} for more details of the international transfer retire request.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` + }, + ORG_REACTIVATION:{ + id: 'ORG_REACTIVATION', + subject: 'Organisation Reactivated', + html: ` + Hi

+ + Your organisation has been reactivated by the {{government}}. Your organisation will be able to perform actions as before and all the users of the organisation will be reactivated.

+ + Sincerely,
+ The {{countryName}} Carbon Credit Registry Team + ` } }; diff --git a/lambda/services/src/shared/entities/company.entity.ts b/lambda/services/src/shared/entities/company.entity.ts index 8f75f22ec..5a0fded3a 100644 --- a/lambda/services/src/shared/entities/company.entity.ts +++ b/lambda/services/src/shared/entities/company.entity.ts @@ -64,6 +64,9 @@ export class Company implements EntitySubject{ @Column("bigint", { nullable: true }) lastUpdateVersion: number; + @Column("bigint", { nullable: true }) + creditTxTime: number; + @Column({nullable:true}) remarks: string; diff --git a/lambda/services/src/shared/entities/programme.entity.ts b/lambda/services/src/shared/entities/programme.entity.ts index e58c28abc..6e71829b7 100644 --- a/lambda/services/src/shared/entities/programme.entity.ts +++ b/lambda/services/src/shared/entities/programme.entity.ts @@ -73,14 +73,14 @@ export class Programme implements EntitySubject { @Column({ type: "decimal", precision: 10, scale: PRECISION, nullable: true }) creditBalance: number; - @Column({ type: "decimal", precision: 10, scale: PRECISION, nullable: true }) - creditRetired: number; + @Column("real", { array: true, nullable: true }) + creditRetired: number[]; @Column("real", { array: true, nullable: true }) creditFrozen: number[]; - @Column({ type: "decimal", precision: 10, scale: PRECISION, nullable: true }) - creditTransferred: number; + @Column("real", { array: true, nullable: true }) + creditTransferred: number[]; @Column({ nullable: true }) constantVersion: string; @@ -132,6 +132,18 @@ export class Programme implements EntitySubject { @Column({ type: "bigint" }) createdTime: number; + @Column({ type: "bigint", nullable: true }) + authTime: number; + + @Column({ type: "bigint", nullable: true }) + creditUpdateTime: number; + + @Column({ type: "bigint", nullable: true }) + statusUpdateTime: number; + + @Column({ type: "bigint", nullable: true }) + certifiedTime: number; + @Column({ nullable: true }) txRef: string; diff --git a/lambda/services/src/shared/entities/programme.transfer.ts b/lambda/services/src/shared/entities/programme.transfer.ts index d1bb7b380..b9b969171 100644 --- a/lambda/services/src/shared/entities/programme.transfer.ts +++ b/lambda/services/src/shared/entities/programme.transfer.ts @@ -61,9 +61,15 @@ export class ProgrammeTransfer implements EntitySubject { @Column({nullable: true}) comment: string; + @Column({nullable: true}) + txRef: string; + @Column({type: "bigint"}) txTime: number; - + + @Column({type: "bigint", nullable: true}) + createdTime: number; + @Column({ type: "enum", enum: TransferStatus, diff --git a/lambda/services/src/shared/entities/programmeTransfer.view.entity.ts b/lambda/services/src/shared/entities/programmeTransfer.view.entity.ts index 39ea5ec40..8f27a3b83 100644 --- a/lambda/services/src/shared/entities/programmeTransfer.view.entity.ts +++ b/lambda/services/src/shared/entities/programmeTransfer.view.entity.ts @@ -5,7 +5,7 @@ import { ProgrammeTransfer } from "./programme.transfer"; @ViewEntity({ expression: ` SELECT programme_transfer.*, JSON_AGG(distinct "requester".*) as "requester", JSON_AGG(distinct "receiver".*) as "receiver", - "prog"."creditBalance" as "creditBalance", "prog"."title" as "programmeTitle", + "prog"."creditBalance" as "creditBalance", "prog"."title" as "programmeTitle", "prog"."certifierId" as "programmeCertifierId", "prog"."sector" as "programmeSector", JSON_AGG(distinct "certifier".*) as "certifier", JSON_AGG(distinct "sender".*) as "sender", "prog"."proponentTaxVatId" as "proponentTaxVatId", "prog"."proponentPercentage" as "proponentPercentage", "prog"."creditOwnerPercentage" as "creditOwnerPercentage", @@ -26,6 +26,9 @@ export class ProgrammeTransferViewEntityQuery extends ProgrammeTransfer { @ViewColumn() programmeTitle: string; + @ViewColumn() + programmeCertifierId: number[] + @ViewColumn() serialNo: string; diff --git a/lambda/services/src/shared/enum/programme-status.enum.ts b/lambda/services/src/shared/enum/programme-status.enum.ts index 8e235880f..64c843a25 100644 --- a/lambda/services/src/shared/enum/programme-status.enum.ts +++ b/lambda/services/src/shared/enum/programme-status.enum.ts @@ -5,4 +5,7 @@ export enum ProgrammeStage { AWAITING_AUTHORIZATION = "AwaitingAuthorization", AUTHORISED = "Authorised", REJECTED = "Rejected", +// ISSUED = "Issued", +// RETIRED = "Retired", +// TRANSFERRED = "Transferred", } diff --git a/lambda/services/src/shared/enum/stat.type.enum.ts b/lambda/services/src/shared/enum/stat.type.enum.ts index b5e53111f..9af40b9ce 100644 --- a/lambda/services/src/shared/enum/stat.type.enum.ts +++ b/lambda/services/src/shared/enum/stat.type.enum.ts @@ -31,28 +31,32 @@ export enum StatType { PROGRAMS_CERTIFIED = "PROGRAMS_CERTIFIED", PROGRAMS_UNCERTIFIED = "PROGRAMS_UNCERTIFIED", - AGG_PROGRAMME_BY_STATUS = 'AGG_PROGRAMME_BY_STATUS', - AGG_PROGRAMME_BY_SECTOR = 'AGG_PROGRAMME_BY_SECTOR', - MY_CREDIT = 'MY_CREDIT', - PENDING_TRANSFER_INIT = 'PENDING_TRANSFER_INIT', - PENDING_TRANSFER_RECV = 'PENDING_TRANSFER_RECV', - CERTIFIED_BY_ME = 'CERTIFIED_BY_ME', - REVOKED_BY_ME = 'REVOKED_BY_ME', - CERTIFIED_REVOKED_BY_ME = 'CERTIFIED_REVOKED_BY_ME', - ALL_AUTH_PROGRAMMES = 'ALL_AUTH_PROGRAMMES', - ALL_AUTH_PROGRAMME_MINE = 'ALL_AUTH_PROGRAMME_MINE', - CERTIFIED_PROGRAMMES = 'CERTIFIED_PROGRAMMES', - REVOKED_PROGRAMMES = 'REVOKED_PROGRAMMES', - CERTIFIED_REVOKED_PROGRAMMES = 'CERTIFIED_REVOKED_PROGRAMMES', - CERTIFIED_BY_ME_BY_STATE = 'CERTIFIED_BY_ME_BY_STATE', - CERTIFIED_BY_ME_BY_SECTOR = 'CERTIFIED_BY_ME_BY_SECTOR', - MY_AGG_PROGRAMME_BY_STATUS = 'MY_AGG_PROGRAMME_BY_STATUS', - MY_AGG_PROGRAMME_BY_SECTOR = 'MY_AGG_PROGRAMME_BY_SECTOR', - MY_CERTIFIED_REVOKED_PROGRAMMES = 'MY_CERTIFIED_REVOKED_PROGRAMMES', - ALL_PROGRAMME_LOCATION = 'ALL_PROGRAMME_LOCATION', - MY_PROGRAMME_LOCATION = 'MY_PROGRAMME_LOCATION', - MY_CERTIFIED_PROGRAMME_LOCATION = 'MY_CERTIFIED_PROGRAMME_LOCATION', - ALL_TRANSFER_LOCATION = 'ALL_TRANSFER_LOCATION', - MY_TRANSFER_LOCATION = 'MY_TRANSFER_LOCATION', - MY_CERTIFIED_TRANSFER_LOCATION = 'MY_CERTIFIED_TRANSFER_LOCATION', + AGG_PROGRAMME_BY_STATUS = "AGG_PROGRAMME_BY_STATUS", + AGG_PROGRAMME_BY_SECTOR = "AGG_PROGRAMME_BY_SECTOR", + MY_CREDIT = "MY_CREDIT", + PENDING_TRANSFER_INIT = "PENDING_TRANSFER_INIT", + PENDING_TRANSFER_RECV = "PENDING_TRANSFER_RECV", + CERTIFIED_BY_ME = "CERTIFIED_BY_ME", + REVOKED_BY_ME = "REVOKED_BY_ME", + CERTIFIED_REVOKED_BY_ME = "CERTIFIED_REVOKED_BY_ME", + UNCERTIFIED_BY_ME = "UNCERTIFIED_BY_ME", + ALL_AUTH_PROGRAMMES = "ALL_AUTH_PROGRAMMES", + ALL_AUTH_PROGRAMME_MINE = "ALL_AUTH_PROGRAMME_MINE", + CERTIFIED_PROGRAMMES = "CERTIFIED_PROGRAMMES", + REVOKED_PROGRAMMES = "REVOKED_PROGRAMMES", + CERTIFIED_REVOKED_PROGRAMMES = "CERTIFIED_REVOKED_PROGRAMMES", + CERTIFIED_BY_ME_BY_STATE = "CERTIFIED_BY_ME_BY_STATE", + CERTIFIED_BY_ME_BY_SECTOR = "CERTIFIED_BY_ME_BY_SECTOR", + MY_AGG_PROGRAMME_BY_STATUS = "MY_AGG_PROGRAMME_BY_STATUS", + MY_AGG_PROGRAMME_BY_SECTOR = "MY_AGG_PROGRAMME_BY_SECTOR", + MY_CERTIFIED_REVOKED_PROGRAMMES = "MY_CERTIFIED_REVOKED_PROGRAMMES", + ALL_PROGRAMME_LOCATION = "ALL_PROGRAMME_LOCATION", + MY_PROGRAMME_LOCATION = "MY_PROGRAMME_LOCATION", + // MY_CERTIFIED_PROGRAMME_LOCATION = 'MY_CERTIFIED_PROGRAMME_LOCATION', + ALL_TRANSFER_LOCATION = "ALL_TRANSFER_LOCATION", + MY_TRANSFER_LOCATION = "MY_TRANSFER_LOCATION", + MY_CERTIFIED_TRANSFER_LOCATION = "MY_CERTIFIED_TRANSFER_LOCATION", + AGG_AUTH_PROGRAMME_BY_STATUS = "AGG_AUTH_PROGRAMME_BY_STATUS", + MY_AGG_AUTH_PROGRAMME_BY_STATUS = "MY_AGG_AUTH_PROGRAMME_BY_STATUS", + AUTH_CERTIFIED_BY_ME_BY_STATE = "AUTH_CERTIFIED_BY_ME_BY_STATE", } diff --git a/lambda/services/src/shared/enum/system.action.type.ts b/lambda/services/src/shared/enum/system.action.type.ts new file mode 100644 index 000000000..0d44f41bc --- /dev/null +++ b/lambda/services/src/shared/enum/system.action.type.ts @@ -0,0 +1,4 @@ +export enum SystemActionType { + SUSPEND_AUTO_CANCEL = 'SUSPEND_AUTO_CANCEL', + SUSPEND_REVOKE = 'SUSPEND_REVOKE' +} \ No newline at end of file diff --git a/lambda/services/src/shared/enum/txtype.enum.ts b/lambda/services/src/shared/enum/txtype.enum.ts index 7fddeda67..66cd0b934 100644 --- a/lambda/services/src/shared/enum/txtype.enum.ts +++ b/lambda/services/src/shared/enum/txtype.enum.ts @@ -8,4 +8,5 @@ export enum TxType { REVOKE = '6', FREEZE = '7', AUTH = '8', + UNFREEZE = '9', } \ No newline at end of file diff --git a/lambda/services/src/shared/programme-ledger/programme-ledger.service.ts b/lambda/services/src/shared/programme-ledger/programme-ledger.service.ts index 4b8b8b70c..1f288eee3 100644 --- a/lambda/services/src/shared/programme-ledger/programme-ledger.service.ts +++ b/lambda/services/src/shared/programme-ledger/programme-ledger.service.ts @@ -272,21 +272,33 @@ export class ProgrammeLedgerService { programme.txTime = new Date().getTime(); programme.txRef = `${name}#${transfer.requestId}#${transfer.retirementType}#${reason}`; + const compIndex = programme.companyId.indexOf(transfer.fromCompanyId); + if (compIndex < 0) { + throw new HttpException( + `Company ${transfer.fromCompanyId} does not own the programme`, + HttpStatus.BAD_REQUEST + ); + } if (isRetirement) { // if (programme.creditBalance == transfer.creditAmount) { // programme.currentStage = ProgrammeStage.RETIRED; // } programme.txType = TxType.RETIRE; if (!programme.creditRetired) { - programme.creditRetired = 0; + programme.creditRetired = new Array( + programme.creditOwnerPercentage.length + ).fill(0); } - programme.creditRetired += transfer.creditAmount; + programme.creditRetired[compIndex] += transfer.creditAmount; + } else { programme.txType = TxType.TRANSFER; if (!programme.creditTransferred) { - programme.creditTransferred = 0; + programme.creditTransferred = new Array( + programme.creditOwnerPercentage.length + ).fill(0); } - programme.creditTransferred += transfer.creditAmount; + programme.creditTransferred[compIndex] += transfer.creditAmount; } programme.creditChange = transfer.creditAmount; programme.creditBalance -= transfer.creditAmount; @@ -504,11 +516,11 @@ export class ProgrammeLedgerService { public async revokeCompanyCertifications( companyId: number, - reason: string, - user: string + user: string, + sendRevokeEmail: Function ): Promise { this.logger.log( - `Freezing programme credits reason:${reason} companyId:${companyId} user:${user}` + `Freezing programme credits companyId:${companyId} user:${user}` ); const getQueries = {}; companyId = Number(companyId); @@ -540,7 +552,7 @@ export class ProgrammeLedgerService { const prvTxTime = programme.txTime; programme.txTime = new Date().getTime(); - programme.txRef = `${user}#${reason}`; + programme.txRef = `${user}`; programme.txType = TxType.REVOKE; programme.certifierId.splice(index, 1); @@ -556,6 +568,8 @@ export class ProgrammeLedgerService { }; programmesId.push(programme.programmeId); + + sendRevokeEmail(programme); } // updatedProgramme = programme; return [updateMap, updateWhere, {}]; @@ -567,12 +581,11 @@ export class ProgrammeLedgerService { public async freezeCompany( companyId: number, - reason: string, user: any, - companyName: string + isFreeze: boolean ): Promise { this.logger.log( - `Freezing programme credits reason:${reason} companyId:${companyId} user:${user.id}` + `Freezing programme credits companyId:${companyId} user:${user}` ); const getQueries = {}; companyId = Number(companyId); @@ -605,31 +618,43 @@ export class ProgrammeLedgerService { ); } - if (programme.companyId.length > 1) { - if (!programme.creditOwnerPercentage) { - throw new HttpException( - "Not ownership percentage for the company", - HttpStatus.BAD_REQUEST - ); + if(isFreeze){ + if (programme.companyId.length > 1) { + if (!programme.creditOwnerPercentage) { + throw new HttpException( + "Not ownership percentage for the company", + HttpStatus.BAD_REQUEST + ); + } + } else { + programme.creditOwnerPercentage = [100]; } - } else { - programme.creditOwnerPercentage = [100]; - } - const freezeCredit = - (programme.creditBalance * programme.creditOwnerPercentage[index]) / - 100; - if (!programme.creditFrozen) { - programme.creditFrozen = new Array( - programme.creditOwnerPercentage.length - ).fill(0); + const freezeCredit =this.round2Precision( + (programme.creditBalance * programme.creditOwnerPercentage[index]) / + 100); + if (!programme.creditFrozen) { + programme.creditFrozen = new Array( + programme.creditOwnerPercentage.length + ).fill(0); + } + if(freezeCredit === 0) + continue; + programme.creditFrozen[index] = freezeCredit; + }else{ + if(programme.creditFrozen === undefined || programme.creditFrozen[index] === null) + continue; + const unFrozenCredit = this.round2Precision(programme.creditFrozen[index]); + if(unFrozenCredit === 0) + continue; + programme.creditChange = unFrozenCredit; + programme.creditFrozen[index] = 0; } const prvTxTime = programme.txTime; (programme.txTime = new Date().getTime()), - (programme.txRef = `${user.companyId}#${user.companyName}#${user.id}#${user.name}#${companyName}`), - (programme.txType = TxType.FREEZE); - programme.creditFrozen[index] = freezeCredit; + (programme.txRef = user), + (programme.txType = isFreeze ? TxType.FREEZE : TxType.UNFREEZE); updateMap[this.ledger.tableName + "#" + programme.programmeId] = { currentStage: programme.currentStage, @@ -842,8 +867,12 @@ export class ProgrammeLedgerService { } private round2Precision(val) { - return parseFloat(val.toFixed(PRECISION)); + if(val) + return parseFloat(val.toFixed(PRECISION)); + else + return 0; } + public async authProgrammeStatus( programmeId: string, countryCodeA2: string, diff --git a/lambda/services/src/shared/programme/programme.module.ts b/lambda/services/src/shared/programme/programme.module.ts index 4c90927bd..fd76b7b0a 100644 --- a/lambda/services/src/shared/programme/programme.module.ts +++ b/lambda/services/src/shared/programme/programme.module.ts @@ -13,6 +13,7 @@ import { Company } from '../entities/company.entity'; import { ProgrammeQueryEntity } from '../entities/programme.view.entity'; import { ProgrammeTransferViewEntityQuery } from '../entities/programmeTransfer.view.entity'; import { UserModule } from '../user/user.module'; +import { EmailHelperModule } from '../email-helper/email-helper.module'; @Module({ imports: [ @@ -22,7 +23,8 @@ import { UserModule } from '../user/user.module'; UtilModule, CompanyModule, EmailModule, - UserModule + UserModule, + EmailHelperModule ], providers: [Logger, ProgrammeService], exports: [ProgrammeService] diff --git a/lambda/services/src/shared/programme/programme.service.ts b/lambda/services/src/shared/programme/programme.service.ts index 15d6f634b..97082945a 100644 --- a/lambda/services/src/shared/programme/programme.service.ts +++ b/lambda/services/src/shared/programme/programme.service.ts @@ -1,813 +1,1690 @@ -import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; -import { ProgrammeDto } from '../dto/programme.dto'; -import { Programme } from '../entities/programme.entity'; -import { ProgrammeLedgerService } from '../programme-ledger/programme-ledger.service'; -import { instanceToPlain, plainToClass } from 'class-transformer'; -import { ProgrammeStage } from '../enum/programme-status.enum'; -import { AgricultureConstants, AgricultureCreationRequest, calculateCredit, SolarConstants, SolarCreationRequest } from 'carbon-credit-calculator'; -import { QueryDto } from '../dto/query.dto'; -import { InjectRepository } from '@nestjs/typeorm'; -import { In, Repository } from 'typeorm'; -import { PrimaryGeneratedColumnType } from 'typeorm/driver/types/ColumnTypes'; -import { CounterService } from '../util/counter.service'; -import { CounterType } from '../util/counter.type.enum'; -import { ConstantEntity } from '../entities/constants.entity'; -import { DataResponseDto } from '../dto/data.response.dto'; -import { ConstantUpdateDto } from '../dto/constants.update.dto'; -import { ProgrammeApprove } from '../dto/programme.approve'; -import { DataListResponseDto } from '../dto/data.list.response'; -import { BasicResponseDto } from '../dto/basic.response.dto'; -import { ConfigService } from '@nestjs/config'; -import { TypeOfMitigation } from '../enum/typeofmitigation.enum'; -import { CompanyService } from '../company/company.service'; -import { ProgrammeTransferRequest } from '../dto/programme.transfer.request'; -import { EmailService } from '../email/email.service'; -import { EmailTemplates } from '../email/email.template'; -import { User } from '../entities/user.entity'; -import { ProgrammeTransfer } from '../entities/programme.transfer'; -import { TransferStatus } from '../enum/transform.status.enum'; -import { ProgrammeTransferApprove } from '../dto/programme.transfer.approve'; -import { ProgrammeTransferReject } from '../dto/programme.transfer.reject'; -import { Company } from '../entities/company.entity'; -import { HelperService } from '../util/helpers.service'; -import { CompanyRole } from '../enum/company.role.enum'; -import { ProgrammeCertify } from '../dto/programme.certify'; -import { ProgrammeQueryEntity } from '../entities/programme.view.entity'; -import { ProgrammeTransferViewEntityQuery } from '../entities/programmeTransfer.view.entity'; -import { ProgrammeRetire } from '../dto/programme.retire'; -import { ProgrammeTransferCancel } from '../dto/programme.transfer.cancel'; -import { CompanyState } from '../enum/company.state.enum'; -import { ProgrammeReject } from '../dto/programme.reject'; -import { ProgrammeIssue } from '../dto/programme.issue'; -import { RetireType } from '../enum/retire.type.enum'; -import { UserService } from '../user/user.service'; -import { Role } from '../casl/role.enum'; - -export declare function PrimaryGeneratedColumn(options: PrimaryGeneratedColumnType): Function; +import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; +import { ProgrammeDto } from "../dto/programme.dto"; +import { Programme } from "../entities/programme.entity"; +import { ProgrammeLedgerService } from "../programme-ledger/programme-ledger.service"; +import { instanceToPlain, plainToClass } from "class-transformer"; +import { ProgrammeStage } from "../enum/programme-status.enum"; +import { + AgricultureConstants, + AgricultureCreationRequest, + calculateCredit, + SolarConstants, + SolarCreationRequest, +} from "carbon-credit-calculator"; +import { QueryDto } from "../dto/query.dto"; +import { InjectRepository } from "@nestjs/typeorm"; +import { In, Repository } from "typeorm"; +import { PrimaryGeneratedColumnType } from "typeorm/driver/types/ColumnTypes"; +import { CounterService } from "../util/counter.service"; +import { CounterType } from "../util/counter.type.enum"; +import { ConstantEntity } from "../entities/constants.entity"; +import { DataResponseDto } from "../dto/data.response.dto"; +import { ConstantUpdateDto } from "../dto/constants.update.dto"; +import { ProgrammeApprove } from "../dto/programme.approve"; +import { DataListResponseDto } from "../dto/data.list.response"; +import { BasicResponseDto } from "../dto/basic.response.dto"; +import { ConfigService } from "@nestjs/config"; +import { TypeOfMitigation } from "../enum/typeofmitigation.enum"; +import { CompanyService } from "../company/company.service"; +import { ProgrammeTransferRequest } from "../dto/programme.transfer.request"; +import { EmailService } from "../email/email.service"; +import { EmailTemplates } from "../email/email.template"; +import { User } from "../entities/user.entity"; +import { ProgrammeTransfer } from "../entities/programme.transfer"; +import { TransferStatus } from "../enum/transform.status.enum"; +import { ProgrammeTransferApprove } from "../dto/programme.transfer.approve"; +import { ProgrammeTransferReject } from "../dto/programme.transfer.reject"; +import { Company } from "../entities/company.entity"; +import { HelperService } from "../util/helpers.service"; +import { CompanyRole } from "../enum/company.role.enum"; +import { ProgrammeCertify } from "../dto/programme.certify"; +import { ProgrammeQueryEntity } from "../entities/programme.view.entity"; +import { ProgrammeTransferViewEntityQuery } from "../entities/programmeTransfer.view.entity"; +import { ProgrammeRetire } from "../dto/programme.retire"; +import { ProgrammeTransferCancel } from "../dto/programme.transfer.cancel"; +import { CompanyState } from "../enum/company.state.enum"; +import { ProgrammeReject } from "../dto/programme.reject"; +import { ProgrammeIssue } from "../dto/programme.issue"; +import { RetireType } from "../enum/retire.type.enum"; +import { EmailHelperService } from "../email-helper/email-helper.service"; +import { UserService } from "../user/user.service"; +import { use } from "passport"; +import { SystemActionType } from "../enum/system.action.type"; +import { CountryService } from "../util/country.service"; + +export declare function PrimaryGeneratedColumn( + options: PrimaryGeneratedColumnType +): Function; @Injectable() export class ProgrammeService { - - constructor( - private programmeLedger: ProgrammeLedgerService, - private counterService: CounterService, - private configService: ConfigService, - private companyService: CompanyService, - private userService: UserService, - private emailService: EmailService, - private helperService: HelperService, - @InjectRepository(Programme) private programmeRepo: Repository, - @InjectRepository(ProgrammeQueryEntity) private programmeViewRepo: Repository, - @InjectRepository(ProgrammeTransferViewEntityQuery) private programmeTransferViewRepo: Repository, - @InjectRepository(Company) private companyRepo: Repository, - @InjectRepository(ProgrammeTransfer) private programmeTransferRepo: Repository, - @InjectRepository(ConstantEntity) private constantRepo: Repository, - private logger: Logger) { } - - private toProgramme(programmeDto: ProgrammeDto): Programme { - const data = instanceToPlain(programmeDto); - this.logger.verbose('Converted programme', JSON.stringify(data)) - return plainToClass(Programme, data); - } - - private async getCreditRequest(programmeDto: ProgrammeDto, constants: ConstantEntity) { - switch (programmeDto.typeOfMitigation) { - case TypeOfMitigation.AGRICULTURE: - const ar = new AgricultureCreationRequest() - ar.duration = (programmeDto.endTime - programmeDto.startTime) - ar.durationUnit = "s" - ar.landArea = programmeDto.agricultureProperties.landArea; - ar.landAreaUnit = programmeDto.agricultureProperties.landAreaUnit - if (constants) { - ar.agricultureConstants = constants.data as AgricultureConstants - } - return ar; - case TypeOfMitigation.SOLAR: - const sr = new SolarCreationRequest() - sr.buildingType = programmeDto.solarProperties.consumerGroup; - sr.energyGeneration = programmeDto.solarProperties.energyGeneration; - sr.energyGenerationUnit = programmeDto.solarProperties.energyGenerationUnit - if (constants) { - sr.solarConstants = constants.data as SolarConstants - } - return sr; - } - throw Error("Not implemented for mitigation type " + programmeDto.typeOfMitigation) + private userNameCache: any = {}; + + constructor( + private programmeLedger: ProgrammeLedgerService, + private counterService: CounterService, + private configService: ConfigService, + private companyService: CompanyService, + private userService: UserService, + private emailService: EmailService, + private helperService: HelperService, + private emailHelperService: EmailHelperService, + private readonly countryService: CountryService, + @InjectRepository(Programme) private programmeRepo: Repository, + @InjectRepository(ProgrammeQueryEntity) + private programmeViewRepo: Repository, + @InjectRepository(ProgrammeTransferViewEntityQuery) + private programmeTransferViewRepo: Repository, + @InjectRepository(Company) private companyRepo: Repository, + @InjectRepository(ProgrammeTransfer) + private programmeTransferRepo: Repository, + @InjectRepository(ConstantEntity) + private constantRepo: Repository, + private logger: Logger + ) {} + + private toProgramme(programmeDto: ProgrammeDto): Programme { + const data = instanceToPlain(programmeDto); + this.logger.verbose("Converted programme", JSON.stringify(data)); + return plainToClass(Programme, data); + } + + private async getCreditRequest( + programmeDto: ProgrammeDto, + constants: ConstantEntity + ) { + switch (programmeDto.typeOfMitigation) { + case TypeOfMitigation.AGRICULTURE: + const ar = new AgricultureCreationRequest(); + ar.duration = programmeDto.endTime - programmeDto.startTime; + ar.durationUnit = "s"; + ar.landArea = programmeDto.agricultureProperties.landArea; + ar.landAreaUnit = programmeDto.agricultureProperties.landAreaUnit; + if (constants) { + ar.agricultureConstants = constants.data as AgricultureConstants; + } + return ar; + case TypeOfMitigation.SOLAR: + const sr = new SolarCreationRequest(); + sr.buildingType = programmeDto.solarProperties.consumerGroup; + sr.energyGeneration = programmeDto.solarProperties.energyGeneration; + sr.energyGenerationUnit = + programmeDto.solarProperties.energyGenerationUnit; + if (constants) { + sr.solarConstants = constants.data as SolarConstants; + } + return sr; + } + throw Error( + "Not implemented for mitigation type " + programmeDto.typeOfMitigation + ); + } + + async transferReject(req: ProgrammeTransferReject, approver: User) { + this.logger.log( + `Programme reject ${JSON.stringify(req)} ${approver.companyId}` + ); + + const pTransfer = await this.programmeTransferRepo.findOneBy({ + requestId: req.requestId, + }); + + if (!pTransfer) { + throw new HttpException( + "Transfer request does not exist", + HttpStatus.BAD_REQUEST + ); } - async transferReject(req: ProgrammeTransferReject, approverCompanyId: number) { - - this.logger.log(`Programme reject ${JSON.stringify(req)} ${approverCompanyId}`); + if (pTransfer.status == TransferStatus.CANCELLED) { + throw new HttpException( + "Transfer request already cancelled", + HttpStatus.BAD_REQUEST + ); + } - const pTransfer = await this.programmeTransferRepo.findOneBy({ - requestId: req.requestId, - }) + if ( + !pTransfer.isRetirement && + pTransfer.fromCompanyId != approver.companyId + ) { + throw new HttpException( + "Invalid approver for the transfer request", + HttpStatus.FORBIDDEN + ); + } + if (pTransfer.isRetirement && pTransfer.toCompanyId != approver.companyId) { + throw new HttpException( + "Invalid approver for the retirement request", + HttpStatus.FORBIDDEN + ); + } - if (!pTransfer) { - throw new HttpException("Transfer request does not exist", HttpStatus.BAD_REQUEST) - } + const result = await this.programmeTransferRepo + .update( + { + requestId: req.requestId, + status: TransferStatus.PENDING, + }, + { + status: pTransfer.isRetirement + ? TransferStatus.NOTRECOGNISED + : TransferStatus.REJECTED, + txTime: new Date().getTime(), + txRef: `${req.comment}#${approver.companyId}#${approver.id}`, + } + ) + .catch((err) => { + this.logger.error(err); + return err; + }); + + const initiatorCompanyDetails = await this.companyService.findByCompanyId( + pTransfer.initiatorCompanyId + ); + + if (result.affected > 0) { + if (pTransfer.isRetirement) { + await this.emailHelperService.sendEmailToOrganisationAdmins( + pTransfer.fromCompanyId, + EmailTemplates.CREDIT_RETIREMENT_NOT_RECOGNITION, + { + credits: pTransfer.creditAmount, + country: pTransfer.toCompanyMeta.country, + }, + 0, + pTransfer.programmeId + ); + } else if ( + initiatorCompanyDetails.companyRole === CompanyRole.GOVERNMENT + ) { + await this.emailHelperService.sendEmailToGovernmentAdmins( + EmailTemplates.CREDIT_TRANSFER_GOV_REJECTED, + { credits: pTransfer.creditAmount }, + pTransfer.programmeId, + pTransfer.fromCompanyId + ); + } else { + await this.emailHelperService.sendEmailToOrganisationAdmins( + pTransfer.initiatorCompanyId, + EmailTemplates.CREDIT_TRANSFER_REJECTED, + { credits: pTransfer.creditAmount }, + pTransfer.fromCompanyId, + pTransfer.programmeId + ); + } + return new BasicResponseDto(HttpStatus.OK, "Successfully rejected"); + } - if (pTransfer.status == TransferStatus.CANCELLED) { - throw new HttpException("Transfer request already cancelled", HttpStatus.BAD_REQUEST) + throw new HttpException( + "No pending transfer request found", + HttpStatus.BAD_REQUEST + ); + } + + async getTransferByProgrammeId( + programmeId: string, + abilityCondition: string, + user: User + ): Promise { + const query: QueryDto = { + page: 1, + size: 30, + filterAnd: [ + { + key: "programmeId", + operation: "=", + value: String(programmeId), + }, + ], + filterOr: undefined, + sort: undefined, + }; + + const resp = await this.programmeTransferViewRepo + .createQueryBuilder("programme_transfer") + .where( + this.helperService.generateWhereSQL( + query, + this.helperService.parseMongoQueryToSQLWithTable( + "programme_transfer", + abilityCondition + ) + ) + ) + .orderBy( + query?.sort?.key && + this.helperService.generateSortCol(query?.sort?.key), + query?.sort?.order + ) + .offset(query.size * query.page - query.size) + .limit(query.size) + .getManyAndCount(); + + if (resp && resp.length > 0) { + for (const e of resp[0]) { + console.log(e); + e.certifier = + e.certifier.length > 0 && e.certifier[0] === null ? [] : e.certifier; + if ( + e.isRetirement && + e.retirementType == RetireType.CROSS_BORDER && + e.toCompanyMeta.country + ) { + e.toCompanyMeta["countryName"] = + await this.countryService.getCountryName(e.toCompanyMeta.country); + } + + let usrId = undefined; + let userCompany = undefined; + if (e["txRef"] != undefined && e["txRef"] != null) { + const parts = e["txRef"]?.split("#"); + if (parts.length > 2) { + usrId = parts[2]; + userCompany = parts[1]; + } + } else { + usrId = e["initiator"]; + userCompany = e["initiatorCompanyId"]; } - if (!pTransfer.isRetirement && pTransfer.fromCompanyId != approverCompanyId) { - throw new HttpException("Invalid approver for the transfer request", HttpStatus.FORBIDDEN) + if ( + user.companyRole === CompanyRole.GOVERNMENT || + Number(userCompany) === Number(user.companyId) + ) { + e["userName"] = await this.getUserName(usrId); } - if (pTransfer.isRetirement && pTransfer.toCompanyId != approverCompanyId) { - throw new HttpException("Invalid approver for the retirement request", HttpStatus.FORBIDDEN) + } + } + return new DataListResponseDto( + resp.length > 0 ? resp[0] : undefined, + resp.length > 1 ? resp[1] : undefined + ); + } + + async queryProgrammeTransfers( + query: QueryDto, + abilityCondition: string, + user: User + ): Promise { + const resp = await this.programmeTransferViewRepo + .createQueryBuilder("programme_transfer") + .where( + this.helperService.generateWhereSQL( + query, + this.helperService.parseMongoQueryToSQLWithTable( + "programme_transfer", + abilityCondition + ) + ) + ) + .orderBy( + query?.sort?.key && + this.helperService.generateSortCol(query?.sort?.key), + query?.sort?.order, + query?.sort?.nullFirst !== undefined + ? query?.sort?.nullFirst === true + ? "NULLS FIRST" + : "NULLS LAST" + : undefined + ) + .offset(query.size * query.page - query.size) + .limit(query.size) + .getManyAndCount(); + + if (resp && resp.length > 0) { + for (const e of resp[0]) { + e.certifier = + e.certifier.length > 0 && e.certifier[0] === null ? [] : e.certifier; + + if (e.toCompanyMeta && e.toCompanyMeta.country) { + e.toCompanyMeta['countryName'] = await this.countryService.getCountryName(e.toCompanyMeta.country) } + } + } + return new DataListResponseDto( + resp.length > 0 ? resp[0] : undefined, + resp.length > 1 ? resp[1] : undefined + ); + } + + async transferApprove(req: ProgrammeTransferApprove, approver: User) { + // TODO: Handle transaction, can happen + console.log("Approver", approver); + const transfer = await this.programmeTransferRepo.findOneBy({ + requestId: req.requestId, + }); + + if (!transfer) { + throw new HttpException( + "Transfer request does not exist", + HttpStatus.BAD_REQUEST + ); + } - const result = await this.programmeTransferRepo.update({ - requestId: req.requestId, - status: TransferStatus.PENDING - }, { - status: pTransfer.isRetirement ? TransferStatus.NOTRECOGNISED : TransferStatus.REJECTED - }).catch((err) => { - this.logger.error(err); - return err; - }); + if (transfer.status == TransferStatus.CANCELLED) { + throw new HttpException( + "Transfer request already cancelled", + HttpStatus.BAD_REQUEST + ); + } - if (result.affected > 0) { - return new BasicResponseDto(HttpStatus.OK, "Successfully rejected"); - } + if ( + transfer.status == TransferStatus.APPROVED || + transfer.status == TransferStatus.RECOGNISED + ) { + throw new HttpException( + "Transfer already approved", + HttpStatus.BAD_REQUEST + ); + } - throw new HttpException("No pending transfer request found", HttpStatus.BAD_REQUEST) + if ( + !transfer.isRetirement && + transfer.fromCompanyId != approver.companyId + ) { + throw new HttpException( + "Invalid approver for the transfer request", + HttpStatus.FORBIDDEN + ); + } + if (transfer.isRetirement && transfer.toCompanyId != approver.companyId) { + throw new HttpException( + "Invalid approver for the retirement request", + HttpStatus.FORBIDDEN + ); } - async queryProgrammeTransfers(query: QueryDto, abilityCondition: string): Promise { - const resp = await this.programmeTransferViewRepo - .createQueryBuilder('programme_transfer') - .where( - this.helperService.generateWhereSQL( - query, - this.helperService.parseMongoQueryToSQLWithTable("programme_transfer", abilityCondition) - ) - ) - .orderBy(query?.sort?.key && `"${query?.sort?.key}"`, query?.sort?.order) - .offset(query.size * query.page - query.size) - .limit(query.size) - .getManyAndCount(); - - if (resp.length > 0) { - resp[0] = resp[0].map( e => { - e.certifier = e.certifier.length > 0 && e.certifier[0] === null ? []: e.certifier - return e; - }) - } - return new DataListResponseDto( - resp.length > 0 ? resp[0] : undefined, - resp.length > 1 ? resp[1] : undefined - ); - } + const receiver = await this.companyService.findByCompanyId( + transfer.toCompanyId + ); + const giver = await this.companyService.findByCompanyId( + transfer.fromCompanyId + ); + + if (receiver.state === CompanyState.SUSPENDED) { + await this.companyService.companyTransferCancel( + transfer.toCompanyId, + `${transfer.comment}#${approver.companyId}#${approver.id}#${SystemActionType.SUSPEND_AUTO_CANCEL}#${receiver.name}` + ); + throw new HttpException( + "Receive company suspended", + HttpStatus.BAD_REQUEST + ); + } + + if (giver.state === CompanyState.SUSPENDED) { + await this.companyService.companyTransferCancel( + transfer.fromCompanyId, + `${transfer.comment}#${approver.companyId}#${approver.id}#${SystemActionType.SUSPEND_AUTO_CANCEL}#${receiver.name}` + ); + throw new HttpException( + "Credit sending company suspended", + HttpStatus.BAD_REQUEST + ); + } - async transferApprove(req: ProgrammeTransferApprove, approver: User) { - // TODO: Handle transaction, can happen - console.log('Approver', approver) - const transfer = await this.programmeTransferRepo.findOneBy({ + if (transfer.status != TransferStatus.PROCESSING) { + const trq = await this.programmeTransferRepo + .update( + { requestId: req.requestId, + status: TransferStatus.PENDING, + }, + { + status: TransferStatus.PROCESSING, + txTime: new Date().getTime(), + } + ) + .catch((err) => { + this.logger.error(err); + return err; }); - if (!transfer) { - throw new HttpException("Transfer request does not exist", HttpStatus.BAD_REQUEST) - } + if (trq.affected <= 0) { + throw new HttpException( + "No pending transfer request found", + HttpStatus.BAD_REQUEST + ); + } + } - if (transfer.status == TransferStatus.CANCELLED) { - throw new HttpException("Transfer request already cancelled", HttpStatus.BAD_REQUEST) - } + const initiatorCompanyDetails = await this.companyService.findByCompanyId( + transfer.initiatorCompanyId + ); + + const transferResult = await this.doTransfer( + transfer, + `${this.getUserRef(approver)}#${receiver.companyId}#${receiver.name}#${ + giver.companyId + }#${giver.name}`, + req.comment, + transfer.isRetirement + ); + + if(transferResult.statusCode === 200){ + if (transfer.isRetirement) { + await this.emailHelperService.sendEmailToOrganisationAdmins( + transfer.fromCompanyId, + EmailTemplates.CREDIT_RETIREMENT_RECOGNITION, + { + credits: transfer.creditAmount, + country: transfer.toCompanyMeta.country, + }, + 0, + transfer.programmeId + ); + } else if (initiatorCompanyDetails.companyRole === CompanyRole.GOVERNMENT) { + await this.emailHelperService.sendEmailToGovernmentAdmins( + EmailTemplates.CREDIT_TRANSFER_GOV_ACCEPTED_TO_INITIATOR, + { credits: transfer.creditAmount }, + transfer.programmeId, + approver.companyId + ); + await this.emailHelperService.sendEmailToOrganisationAdmins( + transfer.toCompanyId, + EmailTemplates.CREDIT_TRANSFER_GOV_ACCEPTED_TO_RECEIVER, + { + credits: transfer.creditAmount, + government: initiatorCompanyDetails.name, + }, + transfer.fromCompanyId, + transfer.programmeId + ); + } else { + await this.emailHelperService.sendEmailToOrganisationAdmins( + transfer.toCompanyId, + EmailTemplates.CREDIT_TRANSFER_ACCEPTED, + { credits: transfer.creditAmount }, + approver.companyId, + transfer.programmeId + ); + } + } - if (transfer.status == TransferStatus.APPROVED || transfer.status == TransferStatus.RECOGNISED) { - throw new HttpException("Transfer already approved", HttpStatus.BAD_REQUEST) - } + return transferResult; + } + + private async doTransfer( + transfer: ProgrammeTransfer, + user: string, + reason: string, + isRetirement: boolean + ) { + const programme = await this.programmeLedger.transferProgramme( + transfer, + user, + reason, + isRetirement + ); + + this.logger.log("Programme updated"); + const result = await this.programmeTransferRepo + .update( + { + requestId: transfer.requestId, + }, + { + status: transfer.isRetirement + ? TransferStatus.RECOGNISED + : TransferStatus.APPROVED, + txTime: new Date().getTime(), + } + ) + .catch((err) => { + this.logger.error(err); + return err; + }); + + if (result.affected > 0) { + return new DataResponseDto(HttpStatus.OK, programme); + } - if (!transfer.isRetirement && transfer.fromCompanyId != approver.companyId) { - throw new HttpException("Invalid approver for the transfer request", HttpStatus.FORBIDDEN) - } - if (transfer.isRetirement && transfer.toCompanyId != approver.companyId) { - throw new HttpException("Invalid approver for the retirement request", HttpStatus.FORBIDDEN) - } + throw new HttpException( + "Internal error on status updating", + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + async transferCancel(req: ProgrammeTransferCancel, requester: User) { + this.logger.log( + `Programme transfer cancel by ${requester.companyId}-${ + requester.id + } received ${JSON.stringify(req)}` + ); + + const transfer = await this.programmeTransferRepo.findOneBy({ + requestId: req.requestId, + }); + + if (!transfer) { + throw new HttpException( + "Transfer request does not exist", + HttpStatus.BAD_REQUEST + ); + } - const receiver = await this.companyService.findByCompanyId( - transfer.toCompanyId + if (transfer.status != TransferStatus.PENDING) { + throw new HttpException( + "Transfer already processed", + HttpStatus.BAD_REQUEST + ); + } + + const result = await this.programmeTransferRepo + .update( + { + requestId: req.requestId, + status: TransferStatus.PENDING, + }, + { + status: TransferStatus.CANCELLED, + txTime: new Date().getTime(), + txRef: `${req.comment}#${requester.companyId}#${requester.id}`, + } + ) + .catch((err) => { + this.logger.error(err); + return err; + }); + + if (result.affected > 0) { + const initiatorCompanyDetails = await this.companyService.findByCompanyId( + transfer.initiatorCompanyId + ); + if (transfer.isRetirement) { + await this.emailHelperService.sendEmailToGovernmentAdmins( + EmailTemplates.CREDIT_RETIREMENT_CANCEL, + { + credits: transfer.creditAmount, + organisationName: initiatorCompanyDetails.name, + country: transfer.toCompanyMeta.country, + }, + transfer.programmeId + ); + } else if ( + initiatorCompanyDetails.companyRole === CompanyRole.GOVERNMENT + ) { + await this.emailHelperService.sendEmailToOrganisationAdmins( + transfer.fromCompanyId, + EmailTemplates.CREDIT_TRANSFER_GOV_CANCELLATION, + { + credits: transfer.creditAmount, + government: initiatorCompanyDetails.name, + }, + transfer.toCompanyId, + transfer.programmeId ); - const giver = await this.companyService.findByCompanyId( - transfer.fromCompanyId + } else { + await this.emailHelperService.sendEmailToOrganisationAdmins( + transfer.fromCompanyId, + EmailTemplates.CREDIT_TRANSFER_CANCELLATION, + { credits: transfer.creditAmount }, + transfer.initiatorCompanyId, + transfer.programmeId ); + } + return new BasicResponseDto(HttpStatus.OK, "Successfully cancelled"); + } + return new BasicResponseDto( + HttpStatus.BAD_REQUEST, + "Transfer request does not exist in the giv" + ); + } + + async transferRequest(req: ProgrammeTransferRequest, requester: User) { + this.logger.log( + `Programme transfer request by ${requester.companyId}-${ + requester.id + } received ${JSON.stringify(req)}` + ); + + // TODO: Move this to casl factory + // if (requester.role == Role.ViewOnly) { + // throw new HttpException("View only user cannot create requests", HttpStatus.FORBIDDEN) + // } + + // if (![CompanyRole.GOVERNMENT, CompanyRole.PROGRAMME_DEVELOPER].includes(requester.companyRole)) { + // throw new HttpException("Unsupported company role", HttpStatus.FORBIDDEN) + // } + + if ( + req.companyCredit && + req.companyCredit.reduce((a, b) => a + b, 0) <= 0 + ) { + throw new HttpException( + "Total Amount should be greater than 0", + HttpStatus.BAD_REQUEST + ); + } - if (receiver.state === CompanyState.SUSPENDED) { - await this.companyService.companyTransferCancel(transfer.toCompanyId); - throw new HttpException( - "Receive company suspended", + if (req.fromCompanyIds.length > 1) { + if (!req.companyCredit) { + throw new HttpException( + "Company credit needs to define for multiple companies", + HttpStatus.BAD_REQUEST + ); + } else if (req.fromCompanyIds.length != req.companyCredit.length) { + throw new HttpException( + "Invalid company credit for given companies", + HttpStatus.BAD_REQUEST + ); + } + } + + if (req.fromCompanyIds && req.companyCredit && req.fromCompanyIds.length != req.companyCredit.length) { + throw new HttpException( + "Invalid company credit for given from companies", HttpStatus.BAD_REQUEST ); - } + } - if (giver.state === CompanyState.SUSPENDED) { - await this.companyService.companyTransferCancel( - transfer.fromCompanyId - ); - throw new HttpException( - "Credit sending company suspended", - HttpStatus.BAD_REQUEST - ); - } + const indexTo = req.fromCompanyIds.indexOf(req.toCompanyId); + if (indexTo >= 0 && req.companyCredit[indexTo] > 0) { + throw new HttpException( + "Cannot transfer credit within the same company", + HttpStatus.BAD_REQUEST + ); + } - if (transfer.status != TransferStatus.PROCESSING) { - const trq = await this.programmeTransferRepo.update({ - requestId: req.requestId, - status: TransferStatus.PENDING - }, { - status: TransferStatus.PROCESSING - }).catch((err) => { - this.logger.error(err); - return err; - }); - - if (trq.affected <= 0) { - throw new HttpException("No pending transfer request found", HttpStatus.BAD_REQUEST) - } - } + const programme = await this.programmeLedger.getProgrammeById( + req.programmeId + ); - return await this.doTransfer(transfer, `${this.getUserRef(approver)}#${receiver.companyId}#${receiver.name}`, req.comment, transfer.isRetirement) + if (!programme) { + throw new HttpException( + "Programme does not exist", + HttpStatus.BAD_REQUEST + ); } + this.logger.verbose(`Transfer programme ${JSON.stringify(programme)}`); - private async doTransfer(transfer: ProgrammeTransfer, user: string, reason: string, isRetirement: boolean) { - const programme = await this.programmeLedger.transferProgramme(transfer, user, reason, isRetirement); + if (programme.currentStage != ProgrammeStage.AUTHORISED) { + throw new HttpException( + "Programme is not in credit issued state", + HttpStatus.BAD_REQUEST + ); + } + // if (programme.creditBalance - (programme.creditFrozen ? programme.creditFrozen.reduce((a, b) => a + b, 0) : 0) < req.creditAmount) { + // throw new HttpException("Not enough balance for the transfer", HttpStatus.BAD_REQUEST) + // } + if ( + requester.companyRole != CompanyRole.GOVERNMENT && + ![...req.fromCompanyIds, req.toCompanyId].includes(requester.companyId) + ) { + throw new HttpException( + "Cannot initiate transfers for other companies", + HttpStatus.BAD_REQUEST + ); + } - this.logger.log('Programme updated'); - const result = await this.programmeTransferRepo.update({ - requestId: transfer.requestId - }, { - status: transfer.isRetirement ? TransferStatus.RECOGNISED : TransferStatus.APPROVED - }).catch((err) => { - this.logger.error(err); - return err; - }); + if (!req.fromCompanyIds) { + req.fromCompanyIds = programme.companyId; + } + if (!programme.creditOwnerPercentage) { + programme.creditOwnerPercentage = [100]; + } + if (!req.companyCredit) { + req.companyCredit = programme.creditOwnerPercentage.map( + (p, i) => + (programme.creditBalance * p) / 100 - + (programme.creditFrozen ? programme.creditFrozen[i] : 0) + ); + } - if (result.affected > 0) { - return new DataResponseDto(HttpStatus.OK, programme); - } + const requestedCompany = await this.companyService.findByCompanyId( + requester.companyId + ); + + const allTransferList: ProgrammeTransfer[] = []; + const autoApproveTransferList: ProgrammeTransfer[] = []; + const ownershipMap = {}; + const frozenCredit = {}; - throw new HttpException("Internal error on status updating", HttpStatus.INTERNAL_SERVER_ERROR) + for (const i in programme.companyId) { + ownershipMap[programme.companyId[i]] = programme.creditOwnerPercentage[i]; + if (programme.creditFrozen) { + frozenCredit[programme.companyId[i]] = programme.creditFrozen[i]; + } } - async transferCancel(req: ProgrammeTransferCancel, requester: User) { - this.logger.log(`Programme transfer cancel by ${requester.companyId}-${requester.id} received ${JSON.stringify(req)}`) + const hostAddress = this.configService.get("host"); + + const fromCompanyListMap = {}; + for (const j in req.fromCompanyIds) { + const fromCompanyId = req.fromCompanyIds[j]; + this.logger.log( + `Transfer request from ${fromCompanyId} to programme owned by ${programme.companyId}` + ); + const fromCompany = await this.companyService.findByCompanyId( + fromCompanyId + ); + fromCompanyListMap[fromCompanyId] = fromCompany; + + if (!programme.companyId.includes(fromCompanyId)) { + throw new HttpException( + "From company mentioned in the request does own the programme", + HttpStatus.BAD_REQUEST + ); + } - const transfer = await this.programmeTransferRepo.findOneBy({ - requestId: req.requestId, - }); - - if (!transfer) { - throw new HttpException("Transfer request does not exist", HttpStatus.BAD_REQUEST) - } + console.log( + programme.creditBalance, + ownershipMap[fromCompanyId], + frozenCredit[fromCompanyId] + ); + const companyAvailableCredit = + (programme.creditBalance * ownershipMap[fromCompanyId]) / 100 - + (frozenCredit[fromCompanyId] ? frozenCredit[fromCompanyId] : 0); + + let transferCompanyCredit; + if (req.fromCompanyIds.length == 1 && !req.companyCredit) { + transferCompanyCredit = companyAvailableCredit; + } else { + transferCompanyCredit = req.companyCredit[j]; + } - if (transfer.status != TransferStatus.PENDING) { - throw new HttpException("Transfer already processed", HttpStatus.BAD_REQUEST) - } + if (companyAvailableCredit < transferCompanyCredit) { + throw new HttpException( + `Company ${fromCompany.name} does not have enough balance for the transfer. Available: ${companyAvailableCredit}`, + HttpStatus.BAD_REQUEST + ); + } - const result = await this.programmeTransferRepo.update({ - requestId: req.requestId, - status: TransferStatus.PENDING - }, { - status: TransferStatus.CANCELLED - }).catch((err) => { - this.logger.error(err); - return err; - }); + if (transferCompanyCredit == 0) { + continue; + } - if (result.affected > 0) { - return new BasicResponseDto(HttpStatus.OK, "Successfully cancelled"); - } - return new BasicResponseDto(HttpStatus.BAD_REQUEST, "Transfer request does not exist in the giv"); + const transfer = new ProgrammeTransfer(); + transfer.programmeId = req.programmeId; + transfer.fromCompanyId = fromCompanyId; + transfer.toCompanyId = req.toCompanyId; + transfer.initiator = requester.id; + transfer.initiatorCompanyId = requester.companyId; + transfer.txTime = new Date().getTime(); + transfer.createdTime = transfer.txTime; + transfer.comment = req.comment; + transfer.creditAmount = transferCompanyCredit; + transfer.toAccount = req.toAccount; + transfer.isRetirement = false; + + if (requester.companyId != fromCompanyId) { + transfer.status = TransferStatus.PENDING; + } else { + transfer.status = TransferStatus.PROCESSING; + autoApproveTransferList.push(transfer); + } + allTransferList.push(transfer); + } + const results = await this.programmeTransferRepo.insert(allTransferList); + console.log(results); + for (const i in allTransferList) { + allTransferList[i].requestId = results.identifiers[i].requestId; } - async transferRequest(req: ProgrammeTransferRequest, requester: User) { - this.logger.log(`Programme transfer request by ${requester.companyId}-${requester.id} received ${JSON.stringify(req)}`) - - // TODO: Move this to casl factory - // if (requester.role == Role.ViewOnly) { - // throw new HttpException("View only user cannot create requests", HttpStatus.FORBIDDEN) - // } + let updateProgramme = undefined; + for (const trf of autoApproveTransferList) { + this.logger.log(`Credit send received ${trf}`); + const toCompany = await this.companyService.findByCompanyId( + trf.toCompanyId + ); + console.log("To Company", toCompany); + updateProgramme = ( + await this.doTransfer( + trf, + `${this.getUserRef(requester)}#${toCompany.companyId}#${ + toCompany.name + }#${fromCompanyListMap[trf.fromCompanyId].companyId}#${ + fromCompanyListMap[trf.fromCompanyId].name + }`, + req.comment, + false + ) + ).data; + await this.emailHelperService.sendEmailToOrganisationAdmins( + trf.toCompanyId, + EmailTemplates.CREDIT_SEND_DEVELOPER, + { + organisationName: requestedCompany.name, + credits: trf.creditAmount, + programmeName: programme.title, + serialNumber: programme.serialNo, + pageLink: hostAddress + "/creditTransfers/viewAll", + } + ); + } + if (updateProgramme) { + return new DataResponseDto(HttpStatus.OK, updateProgramme); + } - // if (![CompanyRole.GOVERNMENT, CompanyRole.PROGRAMME_DEVELOPER].includes(requester.companyRole)) { - // throw new HttpException("Unsupported company role", HttpStatus.FORBIDDEN) - // } + allTransferList.forEach(async (transfer) => { + if (requester.companyRole === CompanyRole.GOVERNMENT) { + await this.emailHelperService.sendEmailToOrganisationAdmins( + transfer.fromCompanyId, + EmailTemplates.CREDIT_TRANSFER_GOV, + { + credits: transfer.creditAmount, + programmeName: programme.title, + serialNumber: programme.serialNo, + pageLink: hostAddress + "/creditTransfers/viewAll", + government: requestedCompany.name, + }, + transfer.toCompanyId + ); + } else if (requester.companyId != transfer.fromCompanyId) { + await this.emailHelperService.sendEmailToOrganisationAdmins( + transfer.fromCompanyId, + EmailTemplates.CREDIT_TRANSFER_REQUISITIONS, + { + organisationName: requestedCompany.name, + credits: transfer.creditAmount, + programmeName: programme.title, + serialNumber: programme.serialNo, + pageLink: hostAddress + "/creditTransfers/viewAll", + } + ); + } + }); + + return new DataListResponseDto(allTransferList, allTransferList.length); + } + + async create(programmeDto: ProgrammeDto): Promise { + this.logger.verbose("ProgrammeDTO received", programmeDto); + const programme: Programme = this.toProgramme(programmeDto); + this.logger.verbose("Programme create", programme); + + if ( + programmeDto.proponentTaxVatId.length > 1 && + (!programmeDto.proponentPercentage || + programmeDto.proponentPercentage.length != + programmeDto.proponentTaxVatId.length) + ) { + throw new HttpException( + "Proponent percentage must defined for each proponent tax id", + HttpStatus.BAD_REQUEST + ); + } - if (req.companyCredit && req.companyCredit.reduce((a, b) => a + b, 0) <= 0) { - throw new HttpException("Total Amount should be greater than 0", HttpStatus.BAD_REQUEST) - } + if ( + programmeDto.proponentPercentage && + programmeDto.proponentTaxVatId.length != + programmeDto.proponentPercentage.length + ) { + throw new HttpException( + "Proponent percentage and number of tax ids does not match", + HttpStatus.BAD_REQUEST + ); + } - if (req.fromCompanyIds.length > 1 ) { - if (!req.companyCredit) { - throw new HttpException("Company credit needs to define for multiple companies", HttpStatus.BAD_REQUEST) - } else if (req.fromCompanyIds.length != req.companyCredit.length){ - throw new HttpException("Invalid company credit for given companies", HttpStatus.BAD_REQUEST) - } - } + if ( + programmeDto.proponentPercentage && + programmeDto.proponentPercentage.reduce((a, b) => a + b, 0) != 100 + ) { + throw new HttpException( + "Proponent percentage sum must be equals to 100", + HttpStatus.BAD_REQUEST + ); + } - const indexTo = req.fromCompanyIds.indexOf(req.toCompanyId); - if (indexTo >= 0 && req.companyCredit[indexTo] > 0) { - throw new HttpException("Cannot transfer credit within the same company", HttpStatus.BAD_REQUEST) - } + if ( + programmeDto.proponentTaxVatId.length !== + new Set(programmeDto.proponentTaxVatId).size + ) { + throw new HttpException( + "Proponent tax id cannot be duplicated", + HttpStatus.BAD_REQUEST + ); + } - const programme = await this.programmeLedger.getProgrammeById(req.programmeId); + const companyIds = []; + const companyNames = []; + for (const taxId of programmeDto.proponentTaxVatId) { + const projectCompany = await this.companyService.findByTaxId(taxId); + if (!projectCompany) { + throw new HttpException( + "Proponent tax id does not exist in the system", + HttpStatus.BAD_REQUEST + ); + } - if (!programme) { - throw new HttpException("Programme does not exist", HttpStatus.BAD_REQUEST) - } - this.logger.verbose(`Transfer programme ${JSON.stringify(programme)}`) + if (projectCompany.companyRole != CompanyRole.PROGRAMME_DEVELOPER) { + throw new HttpException( + "Proponent is not a programme developer", + HttpStatus.BAD_REQUEST + ); + } - if (programme.currentStage != ProgrammeStage.AUTHORISED) { - throw new HttpException("Programme is not in credit issued state", HttpStatus.BAD_REQUEST) - } - // if (programme.creditBalance - (programme.creditFrozen ? programme.creditFrozen.reduce((a, b) => a + b, 0) : 0) < req.creditAmount) { - // throw new HttpException("Not enough balance for the transfer", HttpStatus.BAD_REQUEST) - // } - // if (requester.companyRole != CompanyRole.GOVERNMENT && programme.companyId.includes(requester.companyId)) { - // throw new HttpException("Cannot initiate transfers for already owned programmes", HttpStatus.BAD_REQUEST) - // } - - if (!req.fromCompanyIds) { - req.fromCompanyIds = programme.companyId; - } - if (!programme.creditOwnerPercentage) { - programme.creditOwnerPercentage = [100] - } - if (!req.companyCredit) { - req.companyCredit = programme.creditOwnerPercentage.map((p, i) => (programme.creditBalance*p/100 - (programme.creditFrozen ? programme.creditFrozen[i]: 0))); - } + companyIds.push(projectCompany.companyId); + companyNames.push(projectCompany.name); + } - const requestedCompany = await this.companyService.findByCompanyId(requester.companyId); - - const allTransferList: ProgrammeTransfer[] = [] - const autoApproveTransferList: ProgrammeTransfer[] = [] - const ownershipMap = {} - const frozenCredit = {} - - for (const i in programme.companyId) { - ownershipMap[programme.companyId[i]] = programme.creditOwnerPercentage[i] - if (programme.creditFrozen) { - frozenCredit[programme.companyId[i]] = programme.creditFrozen[i] - } - } - - for (const j in req.fromCompanyIds) { - const fromCompanyId = req.fromCompanyIds[j] - this.logger.log(`Transfer request from ${fromCompanyId} to programme owned by ${programme.companyId}`) - const fromCompany = await this.companyService.findByCompanyId(fromCompanyId); - - if (!programme.companyId.includes(fromCompanyId)) { - throw new HttpException("From company mentioned in the request does own the programme", HttpStatus.BAD_REQUEST) - } - - console.log(programme.creditBalance, ownershipMap[fromCompanyId], frozenCredit[fromCompanyId]) - const companyAvailableCredit = (programme.creditBalance * ownershipMap[fromCompanyId] / 100) - (frozenCredit[fromCompanyId] ? frozenCredit[fromCompanyId] : 0); - - let transferCompanyCredit; - if (req.fromCompanyIds.length == 1 && !req.companyCredit) { - transferCompanyCredit = companyAvailableCredit; - } else { - transferCompanyCredit = req.companyCredit[j]; - } - - if (companyAvailableCredit < transferCompanyCredit) { - throw new HttpException(`Company ${fromCompany.name} does not have enough balance for the transfer. Available: ${companyAvailableCredit}`, HttpStatus.BAD_REQUEST) - } - - if (transferCompanyCredit == 0) { - continue; - } - - const transfer = new ProgrammeTransfer(); - transfer.programmeId = req.programmeId; - transfer.fromCompanyId = fromCompanyId; - transfer.toCompanyId = req.toCompanyId; - transfer.initiator = requester.id; - transfer.initiatorCompanyId = requester.companyId; - transfer.txTime = new Date().getTime() - transfer.comment = req.comment; - transfer.creditAmount = transferCompanyCredit; - transfer.toAccount = req.toAccount; - transfer.isRetirement = false; - - if (requester.companyId != fromCompanyId) { - transfer.status = TransferStatus.PENDING; - await this.emailService.sendEmail( - fromCompany.email, - EmailTemplates.TRANSFER_REQUEST, - { - "name": fromCompany.name, - "requestedCompany": requestedCompany.name, - "credits": transfer.creditAmount, - "serialNo": programme.serialNo, - "programmeName": programme.title - }); - } else { - transfer.status = TransferStatus.PROCESSING; - autoApproveTransferList.push(transfer); - } - allTransferList.push(transfer); - } - const results = await this.programmeTransferRepo.insert(allTransferList) - console.log(results) - for (const i in allTransferList) { - allTransferList[i].requestId = results.identifiers[i].requestId; - } + programme.programmeId = await this.counterService.incrementCount( + CounterType.PROGRAMME, + 3 + ); + programme.countryCodeA2 = this.configService.get("systemCountry"); + const constants = await this.getLatestConstant( + programmeDto.typeOfMitigation + ); + + const req = await this.getCreditRequest(programmeDto, constants); + try { + programme.creditEst = Math.round(await calculateCredit(req)); + } catch (err) { + this.logger.log(`Credit calculate failed ${err.message}`); + throw new HttpException(err.message, HttpStatus.BAD_REQUEST); + } - let updateProgramme = undefined; - for (const trf of autoApproveTransferList) { - this.logger.log(`Credit send received ${trf}`) - const toCompany = await this.companyService.findByCompanyId(trf.toCompanyId); - console.log('To Company', toCompany) - updateProgramme = (await this.doTransfer(trf, `${this.getUserRef(requester)}#${toCompany.companyId}#${toCompany.name}`, req.comment, false)).data; - } - if (updateProgramme) { - return new DataResponseDto(HttpStatus.OK, updateProgramme) - } - return new DataListResponseDto(allTransferList, allTransferList.length) + if (programme.creditEst <= 0) { + throw new HttpException( + "Not enough credits to create the programme", + HttpStatus.BAD_REQUEST + ); + } + // programme.creditBalance = programme.creditIssued; + // programme.creditChange = programme.creditIssued; + programme.programmeProperties.creditYear = new Date( + programme.startTime * 1000 + ).getFullYear(); + programme.constantVersion = constants + ? String(constants.version) + : "default"; + programme.currentStage = ProgrammeStage.AWAITING_AUTHORIZATION; + programme.companyId = companyIds; + programme.txTime = new Date().getTime(); + if (programme.proponentPercentage) { + programme.creditOwnerPercentage = programme.proponentPercentage; + } + programme.createdTime = programme.txTime; + if (!programme.creditUnit) { + programme.creditUnit = this.configService.get("defaultCreditUnit"); } - async create(programmeDto: ProgrammeDto): Promise { - this.logger.verbose('ProgrammeDTO received', programmeDto) - const programme: Programme = this.toProgramme(programmeDto); - this.logger.verbose('Programme create', programme) + let orgNamesList = ""; + if (companyNames.length > 1) { + const lastItem = companyNames.pop(); + orgNamesList = companyNames.join(",") + " and " + lastItem; + } else { + orgNamesList = companyNames[0]; + } + + if (programme.companyId.length === 1 && !programme.proponentPercentage) { + programme.proponentPercentage = [100]; + programme.creditOwnerPercentage = [100]; + } + const savedProgramme = await this.programmeLedger.createProgramme(programme); + if(savedProgramme){ + const hostAddress = this.configService.get("host"); + await this.emailHelperService.sendEmailToGovernmentAdmins( + EmailTemplates.PROGRAMME_CREATE, + { + organisationName: orgNamesList, + programmePageLink: + hostAddress + `/programmeManagement/view?id=${programme.programmeId}`, + } + ); + } - if (programmeDto.proponentTaxVatId.length > 1 && (!programmeDto.proponentPercentage || programmeDto.proponentPercentage.length != programmeDto.proponentTaxVatId.length)) { - throw new HttpException("Proponent percentage must defined for each proponent tax id", HttpStatus.BAD_REQUEST) - } + return savedProgramme; + } + + async query( + query: QueryDto, + abilityCondition: string + ): Promise { + const skip = query.size * query.page - query.size; + let resp = await this.programmeViewRepo + .createQueryBuilder("programme") + .where( + this.helperService.generateWhereSQL( + query, + this.helperService.parseMongoQueryToSQLWithTable( + "programme", + abilityCondition + ), + "programme" + ) + ) + .orderBy( + query?.sort?.key && `"programme"."${query?.sort?.key}"`, + query?.sort?.order, + query?.sort?.nullFirst !== undefined + ? query?.sort?.nullFirst === true + ? "NULLS FIRST" + : "NULLS LAST" + : undefined + ) + .offset(skip) + .limit(query.size) + .getManyAndCount(); + + if (resp.length > 0) { + resp[0] = resp[0].map((e) => { + e.certifier = + e.certifier.length > 0 && e.certifier[0] === null ? [] : e.certifier; + e.company = + e.company.length > 0 && e.company[0] === null ? [] : e.company; + return e; + }); + } - if (programmeDto.proponentPercentage && programmeDto.proponentTaxVatId.length != programmeDto.proponentPercentage.length) { - throw new HttpException("Proponent percentage and number of tax ids does not match", HttpStatus.BAD_REQUEST) - } + return new DataListResponseDto( + resp.length > 0 ? resp[0] : undefined, + resp.length > 1 ? resp[1] : undefined + ); + } - if (programmeDto.proponentPercentage && programmeDto.proponentPercentage.reduce((a, b) => a + b, 0) != 100) { - throw new HttpException("Proponent percentage sum must be equals to 100", HttpStatus.BAD_REQUEST) + async getProgrammeEvents(programmeId: string, user: User): Promise { + const resp = await this.programmeLedger.getProgrammeHistory(programmeId); + if (resp == null) { + return []; + } + for (const el of resp) { + const refs = this.getCompanyIdAndUserIdFromRef(el.data.txRef); + if ( + refs && + (user.companyRole === CompanyRole.GOVERNMENT || + Number(refs?.companyId) === Number(user.companyId)) + ) { + el.data["userName"] = await this.getUserName(refs.id); + } + } + return resp; + } + + async updateCustomConstants( + customConstantType: TypeOfMitigation, + constants: ConstantUpdateDto + ) { + let config; + if (customConstantType == TypeOfMitigation.AGRICULTURE) { + config = new AgricultureConstants(); + const recv = instanceToPlain(constants.agricultureConstants); + for (const key in recv) { + if (recv.hasOwnProperty(key) && recv[key] != undefined) { + config[key] = recv[key]; } - - if (programmeDto.proponentTaxVatId.length !== new Set(programmeDto.proponentTaxVatId).size) { - throw new HttpException("Proponent tax id cannot be duplicated", HttpStatus.BAD_REQUEST) + } + } else if (customConstantType == TypeOfMitigation.SOLAR) { + config = new SolarConstants(); + const recv = instanceToPlain(constants.solarConstants); + for (const key in recv) { + if (recv.hasOwnProperty(key) && recv[key] != undefined) { + config[key] = recv[key]; } + } + } - const companyIds = [] - for (const taxId of programmeDto.proponentTaxVatId) { - const projectCompany = await this.companyService.findByTaxId(taxId); - if (!projectCompany) { - throw new HttpException("Proponent tax id does not exist in the system", HttpStatus.BAD_REQUEST) - } - - if (projectCompany.companyRole != CompanyRole.PROGRAMME_DEVELOPER) { - throw new HttpException("Proponent is not a programme developer", HttpStatus.BAD_REQUEST) - } - - companyIds.push(projectCompany.companyId) - } + const existing = await this.getLatestConstant(customConstantType); + if (existing && JSON.stringify(existing.data) == JSON.stringify(config)) { + throw new HttpException( + "Not difference in the config from the previous version", + HttpStatus.BAD_REQUEST + ); + } + const resp = await this.constantRepo.save({ + id: customConstantType, + data: config, + }); + return new DataResponseDto(HttpStatus.OK, resp); + } + + async getLatestConstant(customConstantType: TypeOfMitigation) { + return await this.constantRepo.findOne({ + where: [{ id: customConstantType }], + order: { version: "DESC" }, + }); + } + + async certify(req: ProgrammeCertify, add: boolean, user: User) { + this.logger.log( + `Programme ${req.programmeId} certification received by ${user.id}` + ); + + if (add && user.companyRole != CompanyRole.CERTIFIER) { + throw new HttpException( + "Programme certification can perform only by certifier", + HttpStatus.FORBIDDEN + ); + } + if ( + !add && + ![CompanyRole.CERTIFIER, CompanyRole.GOVERNMENT].includes( + user.companyRole + ) + ) { + throw new HttpException( + "Programme certification revoke can perform only by certifier or government", + HttpStatus.FORBIDDEN + ); + } - programme.programmeId = (await this.counterService.incrementCount(CounterType.PROGRAMME, 3)) - programme.countryCodeA2 = this.configService.get('systemCountry'); - const constants = await this.getLatestConstant(programmeDto.typeOfMitigation) + let certifierId; + if (user.companyRole === CompanyRole.GOVERNMENT) { + if (!req.certifierId) { + throw new HttpException( + "certifierId required for government user", + HttpStatus.FORBIDDEN + ); + } + certifierId = req.certifierId; + } else { + certifierId = user.companyId; + } - const req = await this.getCreditRequest(programmeDto, constants); - try { - programme.creditEst = Math.round(await calculateCredit(req)); - } catch (err) { - this.logger.log(`Credit calculate failed ${err.message}`) - throw new HttpException(err.message, HttpStatus.BAD_REQUEST) - } + const updated = await this.programmeLedger.updateCertifier( + req.programmeId, + certifierId, + add, + this.getUserRefWithRemarks(user, req.comment) + ); + updated.company = await this.companyRepo.find({ + where: { companyId: In(updated.companyId) }, + }); + if (updated && updated.certifierId && updated.certifierId.length > 0) { + updated.certifier = await this.companyRepo.find({ + where: { companyId: In(updated.certifierId) }, + }); + } - if (programme.creditEst <= 0) { - throw new HttpException("Not enough credits to create the programme", HttpStatus.BAD_REQUEST) - } - // programme.creditBalance = programme.creditIssued; - // programme.creditChange = programme.creditIssued; - programme.programmeProperties.creditYear = new Date(programme.startTime * 1000).getFullYear() - programme.constantVersion = constants ? String(constants.version) : "default" - programme.currentStage = ProgrammeStage.AWAITING_AUTHORIZATION; - programme.companyId = companyIds; - programme.txTime = new Date().getTime(); - if (programme.proponentPercentage){ - programme.creditOwnerPercentage = programme.proponentPercentage - } - programme.createdTime = programme.txTime; - if (!programme.creditUnit) { - programme.creditUnit = this.configService.get('defaultCreditUnit') - } + if (add) { + await this.emailHelperService.sendEmailToProgrammeOwnerAdmins( + req.programmeId, + EmailTemplates.PROGRAMME_CERTIFICATION, + {}, + user.companyId + ); + } else { + if (user.companyRole === CompanyRole.GOVERNMENT) { + await this.emailHelperService.sendEmailToProgrammeOwnerAdmins( + req.programmeId, + EmailTemplates.PROGRAMME_CERTIFICATION_REVOKE_BY_GOVT_TO_PROGRAMME, + {}, + req.certifierId, + user.companyId + ); + await this.emailHelperService.sendEmailToOrganisationAdmins( + req.certifierId, + EmailTemplates.PROGRAMME_CERTIFICATION_REVOKE_BY_GOVT_TO_CERT, + {}, + user.companyId, + req.programmeId + ); + } else { + await this.emailHelperService.sendEmailToProgrammeOwnerAdmins( + req.programmeId, + EmailTemplates.PROGRAMME_CERTIFICATION_REVOKE_BY_CERT, + {}, + user.companyId + ); + } + } - return await this.programmeLedger.createProgramme(programme); - } - - async query(query: QueryDto, abilityCondition: string): Promise { - const skip = (query.size * query.page) - query.size; - let resp = (await this.programmeViewRepo.createQueryBuilder("programme") - .where(this.helperService.generateWhereSQL(query, this.helperService.parseMongoQueryToSQLWithTable("programme", abilityCondition), "programme")) - .orderBy( - query?.sort?.key && `"programme"."${query?.sort?.key}"`, - query?.sort?.order - ) - .offset(skip) - .limit(query.size) - .getManyAndCount()) - - if (resp.length > 0) { - resp[0] = resp[0].map( e => { - e.certifier = e.certifier.length > 0 && e.certifier[0] === null ? []: e.certifier - e.company = e.company.length > 0 && e.company[0] === null ? []: e.company - return e; - }) - } + return new DataResponseDto(HttpStatus.OK, updated); + } + + async retireProgramme(req: ProgrammeRetire, requester: User) { + this.logger.log( + `Programme ${req.programmeId} retiring Comment: ${req.comment} type: ${req.type}` + ); + + if ( + req.companyCredit && + req.companyCredit.reduce((a, b) => a + b, 0) <= 0 + ) { + throw new HttpException( + "Total Amount should be greater than 0", + HttpStatus.BAD_REQUEST + ); + } - return new DataListResponseDto( - resp.length > 0 ? resp[0] : undefined, - resp.length > 1 ? resp[1] : undefined + if (req.fromCompanyIds && req.fromCompanyIds.length > 1) { + if ( + req.companyCredit && + req.fromCompanyIds.length != req.companyCredit.length + ) { + throw new HttpException( + "Invalid company credit for given companies", + HttpStatus.BAD_REQUEST ); + } } - async getProgrammeEvents(programmeId: string, companyId: number): Promise { - const resp = await this.programmeLedger.getProgrammeHistory(programmeId); - // const comp = await this.companyService.findByCompanyId(companyId) - // if (resp.length > 0 && comp.state == CompanyState.SUSPENDED) { - - // } - return resp == null ? [] : resp; - } + // if (req.type === RetireType.CROSS_BORDER && !req.toCompanyMeta.country) { + // throw new HttpException("Country is required for cross border retirement", HttpStatus.BAD_REQUEST) + // } - async updateCustomConstants(customConstantType: TypeOfMitigation, constants: ConstantUpdateDto) { - let config; - if (customConstantType == TypeOfMitigation.AGRICULTURE) { - config = new AgricultureConstants() - const recv = instanceToPlain(constants.agricultureConstants) - for (const key in recv) { - if (recv.hasOwnProperty(key) && recv[key] != undefined) { - config[key] = recv[key] - } - } - } - else if (customConstantType == TypeOfMitigation.SOLAR) { - config = new SolarConstants() - const recv = instanceToPlain(constants.solarConstants) - for (const key in recv) { - if (recv.hasOwnProperty(key) && recv[key] != undefined) { - config[key] = recv[key] - } - } - } + const programme = await this.programmeLedger.getProgrammeById( + req.programmeId + ); - const existing = await this.getLatestConstant(customConstantType); - if (existing && JSON.stringify(existing.data) == JSON.stringify(config)) { - throw new HttpException("Not difference in the config from the previous version", HttpStatus.BAD_REQUEST) - } - const resp = await this.constantRepo.save({ - id: customConstantType, - data: config - }) - return new DataResponseDto(HttpStatus.OK, resp); + if (!programme) { + throw new HttpException( + "Programme does not exist", + HttpStatus.BAD_REQUEST + ); } + this.logger.verbose(`Transfer programme ${JSON.stringify(programme)}`); - async getLatestConstant(customConstantType: TypeOfMitigation) { - return await this.constantRepo.findOne({ - where: [{ id: customConstantType }], - order: { version: 'DESC' } - }); + if (programme.currentStage != ProgrammeStage.AUTHORISED) { + throw new HttpException( + "Programme is not in credit issued state", + HttpStatus.BAD_REQUEST + ); + } + + if (!programme.creditOwnerPercentage) { + programme.creditOwnerPercentage = [100]; } + const requestedCompany = await this.companyService.findByCompanyId( + requester.companyId + ); + const toCompany = await this.companyService.findGovByCountry( + this.configService.get("systemCountry") + ); + + if (requestedCompany.companyRole != CompanyRole.GOVERNMENT) { + if (!req.fromCompanyIds) { + req.fromCompanyIds = [requester.companyId]; + } - async certify(req: ProgrammeCertify, add: boolean, user: User) { - this.logger.log(`Programme ${req.programmeId} certification received by ${user.id}`) + if (!programme.companyId.includes(requester.companyId)) { + throw new HttpException( + "Credit retirement can initiate only the government or programme owner", + HttpStatus.BAD_REQUEST + ); + } - if (add && user.companyRole != CompanyRole.CERTIFIER) { - throw new HttpException("Programme certification can perform only by certifier", HttpStatus.FORBIDDEN) - } + if (!req.fromCompanyIds.includes(requester.companyId)) { + throw new HttpException( + "Requester does not included in the from company id", + HttpStatus.BAD_REQUEST + ); + } - if (!add && ![CompanyRole.CERTIFIER, CompanyRole.GOVERNMENT].includes(user.companyRole)) { - throw new HttpException("Programme certification revoke can perform only by certifier or government", HttpStatus.FORBIDDEN) - } + if (req.fromCompanyIds.length > 1) { + throw new HttpException( + "Does not allow to retire other company credits", + HttpStatus.BAD_REQUEST + ); + } - let certifierId; - if (user.companyRole === CompanyRole.GOVERNMENT) { - if (!req.certifierId) { - throw new HttpException("certifierId required for government user", HttpStatus.FORBIDDEN) - } - certifierId = req.certifierId - } else { - certifierId = user.companyId; - } + if (req.type !== RetireType.CROSS_BORDER) { + throw new HttpException( + "Programme developer allowed to initiate only cross border transfers", + HttpStatus.BAD_REQUEST + ); + } - const updated = await this.programmeLedger.updateCertifier(req.programmeId, certifierId, add, this.getUserRef(user)) - updated.company = await this.companyRepo.find({ - where: { companyId: In(updated.companyId) }, - }) - if (updated && updated.certifierId && updated.certifierId.length > 0) { - updated.certifier = await this.companyRepo.find({ - where: { companyId: In(updated.certifierId) }, - }) - } - return new DataResponseDto(HttpStatus.OK, updated) + if (!req.companyCredit) { + const reqIndex = programme.companyId.indexOf(requester.companyId); + req.companyCredit = [ + (programme.creditBalance * + programme.creditOwnerPercentage[reqIndex]) / + 100 - + (programme.creditFrozen ? programme.creditFrozen[reqIndex] : 0), + ]; + } + } else { + if (!req.fromCompanyIds) { + req.fromCompanyIds = programme.companyId; + } + if (!req.companyCredit) { + req.companyCredit = programme.creditOwnerPercentage.map( + (p, i) => + (programme.creditBalance * p) / 100 - + (programme.creditFrozen ? programme.creditFrozen[i] : 0) + ); + } } - async retireProgramme(req: ProgrammeRetire, requester: User) { - this.logger.log(`Programme ${req.programmeId} retiring Comment: ${req.comment} type: ${req.type}`) - - if (req.fromCompanyIds && req.fromCompanyIds.length > 1 ) { - if (req.companyCredit && req.fromCompanyIds.length != req.companyCredit.length){ - throw new HttpException("Invalid company credit for given companies", HttpStatus.BAD_REQUEST) - } - } + const allTransferList: ProgrammeTransfer[] = []; + const autoApproveTransferList: ProgrammeTransfer[] = []; + const ownershipMap = {}; + const frozenCredit = {}; - // if (req.type === RetireType.CROSS_BORDER && !req.toCompanyMeta.country) { - // throw new HttpException("Country is required for cross border retirement", HttpStatus.BAD_REQUEST) - // } + for (const i in programme.companyId) { + ownershipMap[programme.companyId[i]] = programme.creditOwnerPercentage[i]; + if (programme.creditFrozen) { + frozenCredit[programme.companyId[i]] = programme.creditFrozen[i]; + } + } - const programme = await this.programmeLedger.getProgrammeById(req.programmeId); + const fromCompanyMap = {}; + for (const j in req.fromCompanyIds) { + const fromCompanyId = req.fromCompanyIds[j]; + this.logger.log( + `Retire request from ${fromCompanyId} to programme owned by ${programme.companyId}` + ); + const fromCompany = await this.companyService.findByCompanyId( + fromCompanyId + ); + fromCompanyMap[fromCompanyId] = fromCompany; + if (!programme.companyId.includes(fromCompanyId)) { + throw new HttpException( + "Retire request from company does own the programme", + HttpStatus.BAD_REQUEST + ); + } + const companyAvailableCredit = + (programme.creditBalance * ownershipMap[fromCompanyId]) / 100 - + (frozenCredit[fromCompanyId] ? frozenCredit[fromCompanyId] : 0); + + let transferCompanyCredit; + if (req.fromCompanyIds.length == 1 && !req.companyCredit) { + transferCompanyCredit = companyAvailableCredit; + } else { + transferCompanyCredit = req.companyCredit[j]; + } - if (!programme) { - throw new HttpException("Programme does not exist", HttpStatus.BAD_REQUEST) - } - this.logger.verbose(`Transfer programme ${JSON.stringify(programme)}`) + if ( + req.type != RetireType.CROSS_BORDER && + transferCompanyCredit < companyAvailableCredit + ) { + throw new HttpException( + `Required to retire the full credit amount for the given retirement type`, + HttpStatus.BAD_REQUEST + ); + } - if (programme.currentStage != ProgrammeStage.AUTHORISED) { - throw new HttpException("Programme is not in credit issued state", HttpStatus.BAD_REQUEST) - } - + if (companyAvailableCredit < transferCompanyCredit) { + throw new HttpException( + `Company ${fromCompany.name} does not have enough balance for the transfer. Available: ${companyAvailableCredit}`, + HttpStatus.BAD_REQUEST + ); + } - if (!req.fromCompanyIds) { - req.fromCompanyIds = programme.companyId; - } - if (!programme.creditOwnerPercentage) { - programme.creditOwnerPercentage = [100] - } - if (!req.companyCredit) { - req.companyCredit = programme.creditOwnerPercentage.map((p, i) => (programme.creditBalance*p/100 - (programme.creditFrozen ? programme.creditFrozen[i]: 0))); - } + if (transferCompanyCredit == 0) { + continue; + } - const requestedCompany = await this.companyService.findByCompanyId(requester.companyId); - const toCompany = await this.companyService.findGovByCountry(this.configService.get('systemCountry')) + const transfer = new ProgrammeTransfer(); + transfer.programmeId = req.programmeId; + transfer.fromCompanyId = fromCompanyId; + transfer.toCompanyId = toCompany.companyId; + transfer.initiator = requester.id; + transfer.initiatorCompanyId = requester.companyId; + transfer.txTime = new Date().getTime(); + transfer.createdTime = transfer.txTime; + transfer.comment = req.comment; + transfer.creditAmount = transferCompanyCredit; + transfer.toAccount = + req.type == RetireType.CROSS_BORDER ? "international" : "local"; + transfer.isRetirement = true; + transfer.toCompanyMeta = req.toCompanyMeta; + transfer.retirementType = req.type; + // await this.programmeTransferRepo.save(transfer); + + const hostAddress = this.configService.get("host"); + if (requester.companyId != toCompany.companyId) { + transfer.status = TransferStatus.PENDING; + await this.emailHelperService.sendEmailToGovernmentAdmins( + EmailTemplates.CREDIT_RETIREMENT_BY_DEV, + { + credits: transfer.creditAmount, + programmeName: programme.title, + serialNumber: programme.serialNo, + organisationName: fromCompany.name, + pageLink: hostAddress + "/creditTransfers/viewAll", + } + ); + } else { + transfer.status = TransferStatus.PROCESSING; + autoApproveTransferList.push(transfer); + await this.emailHelperService.sendEmailToOrganisationAdmins( + fromCompany.companyId, + EmailTemplates.CREDIT_RETIREMENT_BY_GOV, + { + credits: transfer.creditAmount, + programmeName: programme.title, + serialNumber: programme.serialNo, + government: toCompany.name, + reason: req.comment, + pageLink: hostAddress + "/creditTransfers/viewAll", + } + ); + } + allTransferList.push(transfer); + } + const results = await this.programmeTransferRepo.insert(allTransferList); + console.log(results); + for (const i in allTransferList) { + allTransferList[i].requestId = results.identifiers[i].requestId; + } - if (requestedCompany.companyRole != CompanyRole.GOVERNMENT) { - if (!programme.companyId.includes(requester.companyId)) { - throw new HttpException("Credit retirement can initiate only the government or programme owner", HttpStatus.BAD_REQUEST) - } - if (req.type !== RetireType.CROSS_BORDER) { - throw new HttpException("Programme developer allowed to initiate only cross border transfers", HttpStatus.BAD_REQUEST) - } - } - - - const allTransferList: ProgrammeTransfer[] = [] - const autoApproveTransferList: ProgrammeTransfer[] = [] - const ownershipMap = {} - const frozenCredit = {} - - for (const i in programme.companyId) { - ownershipMap[programme.companyId[i]] = programme.creditOwnerPercentage[i] - if (programme.creditFrozen) { - frozenCredit[programme.companyId[i]] = programme.creditFrozen[i] - } - } - - for (const j in req.fromCompanyIds) { - const fromCompanyId = req.fromCompanyIds[j] - this.logger.log(`Retire request from ${fromCompanyId} to programme owned by ${programme.companyId}`) - const fromCompany = await this.companyService.findByCompanyId(fromCompanyId); - - if (!programme.companyId.includes(fromCompanyId)) { - throw new HttpException("Retire request from company does own the programme", HttpStatus.BAD_REQUEST) - } - const companyAvailableCredit = (programme.creditBalance * ownershipMap[fromCompanyId] / 100) - (frozenCredit[fromCompanyId] ? frozenCredit[fromCompanyId] : 0); - - let transferCompanyCredit; - if (req.fromCompanyIds.length == 1 && !req.companyCredit) { - transferCompanyCredit = companyAvailableCredit; - } else { - transferCompanyCredit = req.companyCredit[j]; - } - - if (companyAvailableCredit < transferCompanyCredit) { - throw new HttpException(`Company ${fromCompany.name} does not have enough balance for the transfer. Available: ${companyAvailableCredit}`, HttpStatus.BAD_REQUEST) - } - - if (transferCompanyCredit == 0) { - continue; - } - - const transfer = new ProgrammeTransfer(); - transfer.programmeId = req.programmeId; - transfer.fromCompanyId = fromCompanyId; - transfer.toCompanyId = toCompany.companyId; - transfer.initiator = requester.id; - transfer.initiatorCompanyId = requester.companyId; - transfer.txTime = new Date().getTime() - transfer.comment = req.comment; - transfer.creditAmount = transferCompanyCredit; - transfer.toAccount = req.type == RetireType.CROSS_BORDER ? 'international': 'local'; - transfer.isRetirement = true; - transfer.toCompanyMeta = req.toCompanyMeta; - transfer.retirementType = req.type; - // await this.programmeTransferRepo.save(transfer); - - if (requester.companyId != toCompany.companyId) { - transfer.status = TransferStatus.PENDING; - await this.emailService.sendEmail( - toCompany.email, - EmailTemplates.RETIRE_REQUEST, - { - "name": fromCompany.name, - "requestedCompany": requestedCompany.name, - "credits": transfer.creditAmount, - "serialNo": programme.serialNo, - "programmeName": programme.title - }); - } else { - transfer.status = TransferStatus.PROCESSING; - autoApproveTransferList.push(transfer); - } - allTransferList.push(transfer); - } - const results = await this.programmeTransferRepo.insert(allTransferList) - console.log(results) - for (const i in allTransferList) { - allTransferList[i].requestId = results.identifiers[i].requestId; - } - - let updateProgramme = undefined; - for (const trf of autoApproveTransferList) { - this.logger.log(`Retire auto approve received ${trf}`) - updateProgramme = (await this.doTransfer(trf, this.getUserRef(requester), req.comment, true)).data; - } - if (updateProgramme) { - return new DataResponseDto(HttpStatus.OK, updateProgramme) - } - return new DataListResponseDto(allTransferList, allTransferList.length) + let updateProgramme = undefined; + for (const trf of autoApproveTransferList) { + this.logger.log(`Retire auto approve received ${trf}`); + updateProgramme = ( + await this.doTransfer( + trf, + `${this.getUserRef(requester)}#${toCompany.companyId}#${ + toCompany.name + }#${fromCompanyMap[trf.fromCompanyId].companyId}#${ + fromCompanyMap[trf.fromCompanyId].name + }`, + req.comment, + true + ) + ).data; + } + if (updateProgramme) { + return new DataResponseDto(HttpStatus.OK, updateProgramme); + } + return new DataListResponseDto(allTransferList, allTransferList.length); + } + + async issueProgrammeCredit(req: ProgrammeIssue, user: User) { + this.logger.log( + `Programme ${req.programmeId} approve. Comment: ${req.comment}` + ); + const program = await this.programmeLedger.getProgrammeById( + req.programmeId + ); + if (!program) { + throw new HttpException( + "Programme does not exist", + HttpStatus.BAD_REQUEST + ); } - async issueProgrammeCredit(req: ProgrammeIssue, user: User) { - this.logger.log(`Programme ${req.programmeId} approve. Comment: ${req.comment}`) - const program = await this.programmeLedger.getProgrammeById(req.programmeId); - if (!program) { - throw new HttpException("Programme does not exist", HttpStatus.BAD_REQUEST); - } + if (program.currentStage != ProgrammeStage.AUTHORISED) { + throw new HttpException( + "Programme is not in authorised state", + HttpStatus.BAD_REQUEST + ); + } + if (program.creditEst - program.creditIssued < req.issueAmount) { + throw new HttpException( + "Programme issue credit amount can not exceed pending credit amount", + HttpStatus.BAD_REQUEST + ); + } + const updated: any = await this.programmeLedger.issueProgrammeStatus( + req.programmeId, + this.configService.get("systemCountry"), + program.companyId, + req.issueAmount, + this.getUserRefWithRemarks(user, req.comment) + ); + if (!updated) { + return new BasicResponseDto( + HttpStatus.BAD_REQUEST, + `Does not found a pending programme for the given programme id ${req.programmeId}` + ); + } - if (program.currentStage != ProgrammeStage.AUTHORISED) { - throw new HttpException("Programme is not in authorised state", HttpStatus.BAD_REQUEST); - } - if (program.creditEst - program.creditIssued < req.issueAmount) { - throw new HttpException("Programme issue credit amount can not exceed pending credit amount", HttpStatus.BAD_REQUEST); - } - const updated: any = await this.programmeLedger.issueProgrammeStatus(req.programmeId, this.configService.get('systemCountry'), program.companyId, req.issueAmount, this.getUserRef(user)) - if (!updated) { - return new BasicResponseDto(HttpStatus.BAD_REQUEST, `Does not found a pending programme for the given programme id ${req.programmeId}`) - } + updated.company = await this.companyRepo.find({ + where: { companyId: In(updated.companyId) }, + }); + if (updated.certifierId && updated.certifierId.length > 0) { + updated.certifier = await this.companyRepo.find({ + where: { companyId: In(updated.certifierId) }, + }); + } - updated.company = await this.companyRepo.find({ - where: { companyId: In(updated.companyId) }, - }) - if (updated.certifierId && updated.certifierId.length > 0) { - updated.certifier = await this.companyRepo.find({ - where: { companyId: In(updated.certifierId) }, - }) - } - return new DataResponseDto(HttpStatus.OK, updated) + const hostAddress = this.configService.get("host"); + updated.company.forEach(async (company) => { + await this.emailHelperService.sendEmailToOrganisationAdmins( + company.companyId, + EmailTemplates.CREDIT_ISSUANCE, + { + programmeName: updated.title, + credits: updated.creditIssued, + serialNumber: updated.serialNo, + pageLink: + hostAddress + `/programmeManagement/view?id=${updated.programmeId}`, + } + ); + }); + + return new DataResponseDto(HttpStatus.OK, updated); + } + + async approveProgramme(req: ProgrammeApprove, user: User) { + this.logger.log( + `Programme ${req.programmeId} approve. Comment: ${req.comment}` + ); + const program = await this.programmeLedger.getProgrammeById( + req.programmeId + ); + if (!program) { + throw new HttpException( + "Programme does not exist", + HttpStatus.BAD_REQUEST + ); } - async approveProgramme(req: ProgrammeApprove, user: User) { - this.logger.log(`Programme ${req.programmeId} approve. Comment: ${req.comment}`) - const program = await this.programmeLedger.getProgrammeById(req.programmeId); - if (!program) { - throw new HttpException("Programme does not exist", HttpStatus.BAD_REQUEST); - } + if (program.currentStage != ProgrammeStage.AWAITING_AUTHORIZATION) { + throw new HttpException( + "Programme is not in pending state", + HttpStatus.BAD_REQUEST + ); + } + if (program.creditEst < req.issueAmount) { + throw new HttpException( + "Programme issue credit amount can not exceed estimated credit amount", + HttpStatus.BAD_REQUEST + ); + } + const updated: any = await this.programmeLedger.authProgrammeStatus( + req.programmeId, + this.configService.get("systemCountry"), + program.companyId, + req.issueAmount, + this.getUserRefWithRemarks(user, req.comment) + ); + if (!updated) { + return new BasicResponseDto( + HttpStatus.BAD_REQUEST, + `Does not found a pending programme for the given programme id ${req.programmeId}` + ); + } - if (program.currentStage != ProgrammeStage.AWAITING_AUTHORIZATION) { - throw new HttpException("Programme is not in pending state", HttpStatus.BAD_REQUEST); - } - if (program.creditEst < req.issueAmount) { - throw new HttpException("Programme issue credit amount can not exceed estimated credit amount", HttpStatus.BAD_REQUEST); - } - const updated: any = await this.programmeLedger.authProgrammeStatus(req.programmeId, this.configService.get('systemCountry'), program.companyId, req.issueAmount, this.getUserRef(user)) - if (!updated) { - return new BasicResponseDto(HttpStatus.BAD_REQUEST, `Does not found a pending programme for the given programme id ${req.programmeId}`) - } + updated.company = await this.companyRepo.find({ + where: { companyId: In(updated.companyId) }, + }); + if (updated.certifierId && updated.certifierId.length > 0) { + updated.certifier = await this.companyRepo.find({ + where: { companyId: In(updated.certifierId) }, + }); + } - updated.company = await this.companyRepo.find({ - where: { companyId: In(updated.companyId) }, - }) - if (updated.certifierId && updated.certifierId.length > 0) { - updated.certifier = await this.companyRepo.find({ - where: { companyId: In(updated.certifierId) }, - }) - } - return new DataResponseDto(HttpStatus.OK, updated) + const hostAddress = this.configService.get("host"); + updated.company.forEach(async (company) => { + await this.emailHelperService.sendEmailToOrganisationAdmins( + company.companyId, + EmailTemplates.PROGRAMME_AUTHORISATION, + { + programmeName: updated.title, + authorisedDate: new Date(updated.txTime), + serialNumber: updated.serialNo, + programmePageLink: + hostAddress + `/programmeManagement/view?id=${updated.programmeId}`, + } + ); + }); + + return new DataResponseDto(HttpStatus.OK, updated); + } + + async rejectProgramme(req: ProgrammeReject, user: User) { + this.logger.log( + `Programme ${req.programmeId} reject. Comment: ${req.comment}` + ); + + const updated = await this.programmeLedger.updateProgrammeStatus( + req.programmeId, + ProgrammeStage.REJECTED, + ProgrammeStage.AWAITING_AUTHORIZATION, + this.getUserRefWithRemarks(user, req.comment) + ); + if (!updated) { + throw new HttpException( + "Programme does not exist", + HttpStatus.BAD_REQUEST + ); } - async rejectProgramme(req: ProgrammeReject, user: User) { - this.logger.log(`Programme ${req.programmeId} reject. Comment: ${req.comment}`) + await this.emailHelperService.sendEmailToProgrammeOwnerAdmins( + req.programmeId, + EmailTemplates.PROGRAMME_REJECTION, + { reason: req.comment } + ); - const updated = await this.programmeLedger.updateProgrammeStatus(req.programmeId, ProgrammeStage.REJECTED, ProgrammeStage.AWAITING_AUTHORIZATION, this.getUserRef(user)) - if (!updated) { - throw new HttpException("Programme does not exist", HttpStatus.BAD_REQUEST); - } - return new BasicResponseDto(HttpStatus.OK, "Successfully updated") + return new BasicResponseDto(HttpStatus.OK, "Successfully updated"); + } + + private getUserName = async (usrId: string) => { + this.logger.debug(`Getting user [${usrId}]`); + if (usrId == "undefined" || usrId == "null") { + return null; } + const userId = Number(usrId); + if (userId == undefined || userId == null) { + return null; + } + if (this.userNameCache[userId]) { + this.logger.debug( + `Getting user - cached ${userId} ${this.userNameCache[userId]}` + ); + return this.userNameCache[userId]; + } + const user = await this.userService.findById(Number(userId)); + this.logger.debug(`Getting user - user ${user}`); + if (user) { + this.logger.debug(`Getting user - user ${user.name}`); + this.userNameCache[userId] = user.name; + return user.name; + } + return null; + }; - private getUserRef = (user: any) => { - return `${user.companyId}#${user.companyName}#${user.id}#${user.name}`; + private getCompanyIdAndUserIdFromRef = (ref: string) => { + if (!ref) { + return null; + } + const parts = ref.split("#"); + if (parts.length > 2) { + return { + id: parts[2], + companyId: Number(parts[0]), + }; } -} \ No newline at end of file + if (parts.length > 0) { + return { + companyId: Number(parts[0]), + }; + } + return null; + }; + + private getUserRef = (user: any) => { + return `${user.companyId}#${user.companyName}#${user.id}`; + }; + + private getUserRefWithRemarks = (user: any, remarks: string) => { + return `${user.companyId}#${user.companyName}#${user.id}#${remarks}`; + }; +} diff --git a/lambda/services/src/shared/server.ts b/lambda/services/src/shared/server.ts index a7fb85609..9352b1221 100644 --- a/lambda/services/src/shared/server.ts +++ b/lambda/services/src/shared/server.ts @@ -33,12 +33,12 @@ function setupSwagger(nestApp: INestApplication, name: string, httpBase: String) .setVersion('0.0.1') .addBearerAuth() .addApiKey() - .addServer(`/${process.env.NODE_ENV}`) + .addServer(`${process.env.NODE_ENV === 'local' ? '/local': '/'}`) .build(); - + // ${process.env.NODE_ENV} const document = SwaggerModule.createDocument(nestApp, config); - SwaggerModule.setup(`${httpBase}/docs`, nestApp, document, { + SwaggerModule.setup(`${httpBase}`, nestApp, document, { customSiteTitle: 'API Documentation', swaggerOptions: { docExpansion: 'none', diff --git a/lambda/services/src/shared/user/user.module.ts b/lambda/services/src/shared/user/user.module.ts index ca6cbbcbc..5a08d4aa3 100644 --- a/lambda/services/src/shared/user/user.module.ts +++ b/lambda/services/src/shared/user/user.module.ts @@ -1,4 +1,4 @@ -import { Logger, Module } from '@nestjs/common'; +import { forwardRef, Logger, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '../../shared/entities/user.entity'; import { UserService } from './user.service'; @@ -9,6 +9,7 @@ import configuration from '../../shared/configuration'; import { ConfigModule } from '@nestjs/config'; import { CompanyModule } from '../company/company.module'; import { UtilModule } from '../util/util.module'; +import { EmailHelperModule } from '../email-helper/email-helper.module'; @Module({ imports: [ @@ -24,7 +25,8 @@ import { UtilModule } from '../util/util.module'; TypeOrmModule.forFeature([User]), CaslModule, EmailModule, - CompanyModule, + forwardRef(() => CompanyModule), + forwardRef(() => EmailHelperModule), UtilModule ], providers: [UserService, Logger], diff --git a/lambda/services/src/shared/user/user.service.ts b/lambda/services/src/shared/user/user.service.ts index 2a91db8b2..740447141 100644 --- a/lambda/services/src/shared/user/user.service.ts +++ b/lambda/services/src/shared/user/user.service.ts @@ -1,6 +1,8 @@ import { + forwardRef, HttpException, HttpStatus, + Inject, Injectable, Logger, UseFilters, @@ -48,6 +50,7 @@ export class UserService { private configService: ConfigService, private helperService: HelperService, @InjectEntityManager() private entityManger: EntityManager, + @Inject(forwardRef(() => CompanyService)) private companyService: CompanyService, private counterService: CounterService ) {} @@ -201,6 +204,14 @@ export class UserService { return err; }); if (result.affected > 0) { + await this.emailService.sendEmail( + user.email, + EmailTemplates.CHANGE_PASSOWRD, + { + name: user.name, + countryName: this.configService.get("systemCountryName"), + } + ); return new BasicResponseDto(HttpStatus.OK, "Successfully updated"); } throw new HttpException( @@ -257,6 +268,7 @@ export class UserService { HttpStatus.INTERNAL_SERVER_ERROR ); } + async create( userDto: UserDto, companyId: number, @@ -343,6 +355,8 @@ export class UserService { u.apiKey = await this.generateApiKey(userDto.email); } + const hostAddress = this.configService.get("host"); + if (company) { company.companyId = parseInt( await this.counterService.incrementCount(CounterType.COMPANY, 3) @@ -359,13 +373,25 @@ export class UserService { HttpStatus.INTERNAL_SERVER_ERROR ); } + + if(company.email){ + await this.emailService.sendEmail(company.email, EmailTemplates.ORGANISATION_CREATE, { + organisationName: company.name, + countryName: this.configService.get("systemCountryName"), + organisationRole: company.companyRole, + home: hostAddress, + }); + } } - await this.emailService.sendEmail(u.email, EmailTemplates.REGISTER_EMAIL, { + await this.emailService.sendEmail(u.email, EmailTemplates.USER_CREATE, { name: u.name, - countryName: this.configService.get("systemCountry"), - password: u.password, - apiKeyText: u.apiKey ? `
Api Key: ${u.apiKey}` : "", + countryName: this.configService.get("systemCountryName"), + tempPassword: u.password, + home: hostAddress, + email: u.email, + liveChat: this.configService.get("liveChat"), + helpDoc: this.configService.get("helpDocumentation"), }); u.createdTime = new Date().getTime(); @@ -412,6 +438,7 @@ export class UserService { }); const { apiKey, password, ...resp } = usr; + return resp; } @@ -494,4 +521,28 @@ export class UserService { HttpStatus.INTERNAL_SERVER_ERROR ); } + + async getGovAdminAndManagerUsers() { + const result = await this.userRepo + .createQueryBuilder("user") + .where("user.role in (:admin, :manager)",{admin:Role.Admin, manager:Role.Manager}) + .andWhere("user.companyRole= :companyRole",{companyRole:CompanyRole.GOVERNMENT}) + .select(['user.name','user.email']) + .getRawMany(); + + return result; + + //return result.map((item) => {return item.user_email}); + } + + async getOrganisationAdminAndManagerUsers(organisationId) { + const result = await this.userRepo + .createQueryBuilder("user") + .where("user.role in (:admin,:manager)",{admin:Role.Admin, manager:Role.Manager}) + .andWhere("user.companyId= :companyId",{companyId:organisationId}) + .select(['user.name','user.email']) + .getRawMany(); + + return result; + } } diff --git a/lambda/services/src/shared/util/country.service.ts b/lambda/services/src/shared/util/country.service.ts index 5a448e510..6675eea84 100644 --- a/lambda/services/src/shared/util/country.service.ts +++ b/lambda/services/src/shared/util/country.service.ts @@ -21,6 +21,12 @@ export class CountryService { })) != null; } + async getCountryName(alpha2: string) { + return (await this.countryRepo.findOneBy({ + alpha2: alpha2 + }))?.name; + } + async getCountryList(query: QueryDto) { const resp = await this.countryRepo diff --git a/lambda/services/src/shared/util/helpers.service.ts b/lambda/services/src/shared/util/helpers.service.ts index 18d67624a..550564c07 100644 --- a/lambda/services/src/shared/util/helpers.service.ts +++ b/lambda/services/src/shared/util/helpers.service.ts @@ -27,11 +27,31 @@ export class HelperService { return value; } - private isNotLower(key: string) { - if (["txType", "typeOfMitigation", "currentStage", "sectoralScope", "companyRole", "state", "status"].includes(key)) + private prepareKey(col: string, table?: string) { + let key; + if (col.includes('->>')) { + const parts = col.split('->>'); + key = `"${parts[0]}"->>'${parts[1]}'` + } else { + key = `"${col}"` + } + return `${table ? table + "." : ""}${key}` + } + + private isLower(key: string) { + if (["email", "name", "companyName", "taxId", "country", "title", "externalId", "serialNo", "programmeTitle"].includes(key)) return true; } + public generateSortCol(col: string) { + if (col.includes('->>')) { + const parts = col.split('->>'); + return `"${parts[0]}"->>'${parts[1]}'` + } else { + return `"${col}"` + } + } + public generateWhereSQLChartStastics( data: chartStatsRequestDto, extraSQL: string, @@ -212,17 +232,17 @@ export class HelperService { if (this.isQueryDto(e.value)) { return `(${this.prepareValue(e.value, table)})`; } else if (e.operation === 'ANY') { - return `${this.prepareValue(e.value, table)} = ANY(${table ? table + "." : ""}"${e.key}")`; + return `${this.prepareValue(e.value, table)} = ANY(${this.prepareKey(e.key, table)})`; } else if (e.keyOperation) { - return `${e.keyOperation}(${table ? table + "." : ""}"${e.key}") ${ + return `${e.keyOperation}(${this.prepareKey(e.key, table)}) ${ e.operation } ${this.prepareValue(e.value, table, true)}`; - } else if (!this.isNotLower(e.key) && typeof e.value === "string") { - return `LOWER(${table ? table + "." : ""}"${e.key}") ${ + } else if (this.isLower(e.key) && typeof e.value === "string") { + return `LOWER(${this.prepareKey(e.key, table)}) ${ e.operation } ${this.prepareValue(e.value, table, true)}`; } else { - return `${table ? table + "." : ""}"${e.key}" ${ + return `${this.prepareKey(e.key, table)} ${ e.operation } ${this.prepareValue(e.value, table)}`; } @@ -235,15 +255,15 @@ export class HelperService { if (this.isQueryDto(e.value)) { return `(${this.prepareValue(e.value, table)})`; } else if (e.operation === 'ANY') { - return `${this.prepareValue(e.value, table)} = ANY(${table ? table + "." : ""}"${e.key}")`; + return `${this.prepareValue(e.value, table)} = ANY(${this.prepareKey(e.key, table)})`; } else if (e.keyOperation) { - return `${e.keyOperation}(${table ? table + "." : ""}"${e.key}") ${ + return `${e.keyOperation}(${this.prepareKey(e.key, table)}) ${ e.operation } ${this.prepareValue(e.value, table, true)}`; - } else if (!this.isNotLower(e.key) && typeof e.value === "string") { - return `LOWER(${table ? table + "." : ""}"${e.key}") ${e.operation} ${this.prepareValue(e.value, table, true)}`; + } else if (this.isLower(e.key) && typeof e.value === "string") { + return `LOWER(${this.prepareKey(e.key, table)}) ${e.operation} ${this.prepareValue(e.value, table, true)}`; } else { - return `${table ? table + "." : ""}"${e.key}" ${e.operation} ${this.prepareValue(e.value, table)}`; + return `${this.prepareKey(e.key, table)} ${e.operation} ${this.prepareValue(e.value, table)}`; } }) .join(" or "); diff --git a/web/.env-cmdrc b/web/.env-cmdrc index 865b6471f..15ae73e47 100644 --- a/web/.env-cmdrc +++ b/web/.env-cmdrc @@ -1,5 +1,5 @@ { "development": { - "REACT_APP_BACKEND": "https://ck5kt5uaw1.execute-api.us-east-1.amazonaws.com/dev/api" + "REACT_APP_BACKEND": "https://ck5kt5uaw1.execute-api.us-east-1.amazonaws.com/dev" } } diff --git a/web/package.json b/web/package.json index ca7c68d7b..5ef7e8657 100644 --- a/web/package.json +++ b/web/package.json @@ -8,9 +8,6 @@ "@casl/react": "^3.1.0", "@craco/craco": "^7.0.0", "@mapbox/mapbox-sdk": "^0.14.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", "@types/node": "^16.18.3", "@types/react": "^18.0.25", @@ -19,7 +16,6 @@ "antd": "^4.24.1", "apexcharts": "^3.36.3", "axios": "^1.1.3", - "bcrypt": "^5.1.0", "bootstrap-icons": "^1.10.2", "buffer": "^6.0.3", "class-transformer": "^0.5.1", diff --git a/web/public/Assets/i18n/companyProfile/en.json b/web/public/Assets/i18n/companyProfile/en.json index bb83d9191..acb7f25dd 100644 --- a/web/public/Assets/i18n/companyProfile/en.json +++ b/web/public/Assets/i18n/companyProfile/en.json @@ -2,6 +2,7 @@ "title": "Organisation Details", "subTitle": "View the details of the selected organisation", "deauthorise": "DEACTIVATE", + "reActivate": "REACTIVATE", "organisationDetailsHeading": "Organisation Details", "name" : "Name", "taxId" : "Tax ID", @@ -17,5 +18,8 @@ "activeStatus": "Active", "deauthorisedStatus": "Deactivated", "remarks": "Remarks", - "deauthorisationSuccess": "Company deactivated successfully!" + "deauthorisationSuccess": "Company deactivated successfully!", + "reActivateConfirmHeaderText": "Are you sure you want to reactivate this organisation?", + "reActivateConfirmText": "Note: all users associated with the organisation will also be reactivated.", + "reactivationSuccess": "Company reactivated successfully!" } \ No newline at end of file diff --git a/web/public/Assets/i18n/dashboard/en.json b/web/public/Assets/i18n/dashboard/en.json index e69de29bb..18de9ae71 100644 --- a/web/public/Assets/i18n/dashboard/en.json +++ b/web/public/Assets/i18n/dashboard/en.json @@ -0,0 +1,71 @@ +{ + "today": "Today", + "last7": "Last 7 days", + "last14": "Last 14 days", + "overall": "OVERALL", + "mine": "MINE", + "auth": "Authorised", + "rejected": "Rejected", + "pending": "Pending", + "programmesPending": "Programmes Pending", + "trasnferReqReceived": "Pending Transfers Received", + "trasnferReqInit": "Pending Transfers Sent", + "programmesUnCertified": "Programmes Certifiable", + "programmesCertified": "Programmes Certified", + "creditBal": "Credit Balance", + "creditCertified": "Credit Certified", + "programmes": "Programmes", + "credits": "Credits", + "certifiedCredits": "Certified Credits", + "totalProgrammes": "Total Programmes", + "totalProgrammesSector": "Total Programmes: Sector", + "totalCredits": "Total Credits", + "totalCreditsCertified": "Total Credits Certified", + "programmeLocations": "Programme Locations", + "trasnferLocations": "Transfer Locations International", + "tTprogrammespendingGoverment": "Pending state programmes awaiting authorisation", + "tTTransferReqSentGovernment": "Pending credit transfer requests sent to programme owners initiated by your organisation", + "tTCreditBalanceGovernment": "Total credit balance owned by your organisation", + "tTProgrammesGoverment": "Number of programmes created during the specified period and their programme state in the carbon registry at present", + "tTCreditsGovernment": "Number of credits of authorised programmes created during the specified period and their credit state in the carbon registry at present", + "tTCertifiedCreditsGovernment": "Number of credits of programmes created during the specified period, uncertified, certified and revoked in the carbon registry at present", + "tTTotalProgrammesGovernment": "Graphical representation of the number of programmes created during the specified period in each programme state in the carbon registry at present", + "tTTotalProgrammesSectorGovernment": "Graphical representation of the number of programmes in each programme sector created during the specified time in the carbon registry", + "tTTotalCreditsGovernment": "Graphical representation of the number of credits of programmes created during the specified period in each credit state in the carbon registry at present", + "tTTotalCreditsCertifiedGovernment": "Graphical representation of the number of credits of programmes created during the specified period certified, uncertified and revoked in the carbon registry at present", + "tTProgrammeLocationsGovernment": " Locations of the programmes created during the specified period and their programme states in the carbon registry at present", + "tTTransferLocationsGovernment": "Locations of credits of international transfer requests recognised during the specified period", + "tTTransferReqRecProgrammeDev": "Pending credit transfer requests received by your organisation", + "tTTransferReqInitProgrammeDev": "Pending local credit transfer requests initiated by your organisation", + "tTCreditBalanceProgrammeDev": "Total credit balance owned by your organisation", + "tTProgrammesProgrammeDev": "Number of programmes created during the specified period and their programme state in the carbon registry at present, owned by your organisation", + "tTCreditsProgrammeDev": "Number of credits of authorised programmes created during the specified period and their credit state in the carbon registry at present, owned by your organisation", + "tTCertifiedCreditsProgrammeDev": "Number of credits of programmes created during the specified period, uncertified, certified and revoked in the carbon registry at present, owned by your organisation", + "tTTotalProgrammesProgrammeDev": "Graphical representation of the number of programmes created during the specified period, owned by your organisation, in each programme state in the carbon registry at present", + "tTTotalProgrammesSecProgrammeDev": "Graphical representation of the number of programmes owned by your organisation, in each programme sector created during the specified time in the carbon registry", + "tTTotalCreditsProgrammeDev": "Graphical representation of the number of credits of programmes created during the specified period, owned by your organisation, in each credit state in the carbon registry at present", + "tTTotalCertifiedCreditsProgrammeDev": "Graphical representation of the number of credits of programmes created during the specified period, owned by your organisation, certified, uncertified and revoked in the carbon registry at present", + "tTProgrammeLocationsProgrammeDev": "Locations of the programmes created during the specified period, owned by your organisation, and their programme states in the carbon registry at present", + "tTTrasnferLocationsProgrammeDev": "Locations of credits international transfer requests of programmes owned by your organisation recognised during the specified period", + "tTProgrammesUnCertiCertifier": "Number of programmes not yet certified including certificates revoked by your organisation", + "tTProgrammesCertiCertifier": "Number of programmes certified by your organisation", + "tTCreditCertifiedCertifier": "Number of credits certified by your organisation", + "tTProgrammesCertifierMine": "Number of programmes created during the specified period, certified by your organisation, and their programme state in the carbon registry at present", + "tTCreditsCertifierMine": "Number of credits of authorised programmes created during the specified period, certified by your organisation and their credit state in the carbon registry at present", + "tTCertifiedCreditsCertifierMine": "Number of credits of programmes created during the specified period, certified by your organisation, uncertified, certified and revoked in the carbon registry at present", + "tTTotalProgrammesCertifierMine": "Graphical representation of the number of programmes in each programme sector created during the specified time, certified by your company, in the carbon registry", + "tTTotalProgrammesSecCertifierMine": "Graphical representation of the number of programmes in each programme sector created during the specified time, certified by your company, in the carbon registry", + "tTTotalCreditsCertifierMine": "Graphical representation of the number of credits of programmes created during the specified period, certified by your organisation, in each credit state in the carbon registry at present", + "tTTotalCertifiedCreditsCertifierMine": "Graphical representation of the number of credits of programmes certified, uncertified and revoked by your organisation, spread over the specified time", + "tTProgrammeLocationsCertifierMine": "Locations of the programmes created during the specified period, certified by your organisation, and their programme states in the carbon registry at present", + "tTTrasnferLocationsCertifierMine": "Locations of credits of international transfer requests of programmes certified by your organisation recognised during the specified period", + "tTProgrammesCertifierOverall": "Number of programmes created during the specified period and their programme state in the carbon registry at present", + "tTCreditsCertifierOverall": "Number of credits of authorised programmes created during the specified period and their credit state in the carbon registry at present", + "tTCertifiedCreditsCertifierOverall": "Number of credits of programmes created during the specified period, uncertified, certified and revoked in the carbon registry at present", + "tTTotalProgrammesCertifierOverall": "Graphical representation of the number of programmes created during the specified period in each programme state in the carbon registry at present", + "tTTotalProgrammesSecCertifierOverall": "Graphical representation of the number of programmes in each programme sector created during the specified time in the carbon registry", + "tTTotalCreditsCertifierOverall": "Graphical representation of the number of credits of programmes created during the specified period in each credit state in the carbon registry at present", + "tTTotalCertifiedCreditsCertifierOverall": "Graphical representation of the number of credits of programmes created during the specified period certified, uncertified and revoked in the carbon registry at present", + "tTProgrammeLocationsCertifierOverall": "Locations of the programmes created during the specified period and their programme states in the carbon registry at present", + "tTTrasnferLocationsCertifierOverall": "Locations of credits of international transfer requests recognised during the specified period" +} \ No newline at end of file diff --git a/web/public/Assets/i18n/view/en.json b/web/public/Assets/i18n/view/en.json index 8dd15e0bf..0c9f62541 100644 --- a/web/public/Assets/i18n/view/en.json +++ b/web/public/Assets/i18n/view/en.json @@ -73,6 +73,39 @@ "company": "Company", "accept": "Accept", "certifier": "Certifier", - "seeMine": "Mine" - + "seeMine": "Mine", + "tlRejectTitle": "Transfer Request Rejected", + "tlTxRejectDesc": "The request to transfer {} {} credits from {} to {} was rejected by {}", + "tlInitTitle": "Transfer Initiated", + "tlInitDesc": "{} {} credits of this programme were requested to be transferred from {} to {} by {}", + "via": "via", + "tlDescInit": "credits of this programme were requested to be transferred from", + "tlCreate": "Programme Created", + "tlCreateDesc": "The programme was created with a valuation of {} {} credits.", + "tlAuth": "Authorised", + "tlAuthDesc": "The programme was authorised for {} {} credits until {} with the Serial Number {} by {}", + "tlIssue": "Credits Issued", + "tlIssueDesc": "The programme was issued {} {} credits by {}", + "tlReject": "Rejected", + "tlRejectDesc": "The programme was rejected by {}", + "tlTransfer": "Transferred", + "tlTransferDesc": "{} {} credits of this programme were transferred from {} to {} by {}", + "tlRevoke": "Certificate Revoked", + "tlRevokeDesc": "The certification of this programme was revoked {} by {}", + "tlCertify": "Certified", + "tlCertifyDesc": "The programme was certified by {}", + "tlRetire": "Credits Retired", + "tlRetireDesc": "{} {} credits of this programme were retired from {} {}as {} by {}", + "tlFrozen": "Credits Frozen", + "tlFrozenDesc": "{} {} credits were frozen due to the deactivation of {} by {}", + "tlRetInit": "Retirement Initiated", + "tlRetInitDesc": "{} {} credits of this programme were requested to be transferred from {} {}as {} by {}", + "tlRetRejectTitle": "Retirement Not Recognised", + "tlTxRetRejectDesc": "The request to transfer {} {} credits from {} to {} was not recognised by {}", + "tlRetCancelTitle": "Retire request cancelled", + "tlTxCancelTitle": "Transfer Request Cancelled", + "tlTxCancelDesc": "The request to transfer {} {} credits from {} to {} was cancelled by {}", + "tlTxCancelSystemDesc": "The request to transfer {} {} credits from {} to {} was cancelled by the system due to the deactivation of {} by the government", + "tlUnFrozen": "Credits Unfrozen", + "tlUnFrozenDesc": "{} frozen {} credits of the programme have been unfrozen due to the reactivation of {} by {}" } \ No newline at end of file diff --git a/web/public/index.html b/web/public/index.html index d2ef87cf5..690ad9451 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -25,7 +25,8 @@ Carbon Credit Management - + Carbon Credit Registry - Antarctic Region @@ -34,8 +35,7 @@ - + @@ -45,8 +45,7 @@ - + @@ -69,7 +68,13 @@ data-tf-tooltip="Hey 👋  How can I help you? " data-tf-chat data-tf-medium="snippet" data-tf-hidden="utm_source=xxxxx,utm_medium=xxxxx,utm_campaign=xxxxx,utm_term=xxxxx,utm_content=xxxxx" style="all:unset;"> - + \ No newline at end of file diff --git a/web/src/App.tsx b/web/src/App.tsx index 3ad2c9ccf..43330b81e 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -58,7 +58,7 @@ const App = () => { serverURL={ process.env.REACT_APP_BACKEND ? process.env.REACT_APP_BACKEND - : 'http://localhost:3000/local/api' + : 'https://ck5kt5uaw1.execute-api.us-east-1.amazonaws.com/dev' } > diff --git a/web/src/Assets/Fonts/Inter-Bold.woff b/web/src/Assets/Fonts/Inter-Bold.woff new file mode 100644 index 000000000..eaf3d4bfd Binary files /dev/null and b/web/src/Assets/Fonts/Inter-Bold.woff differ diff --git a/web/src/Assets/Fonts/Inter-Bold.woff2 b/web/src/Assets/Fonts/Inter-Bold.woff2 new file mode 100644 index 000000000..2846f29cc Binary files /dev/null and b/web/src/Assets/Fonts/Inter-Bold.woff2 differ diff --git a/web/src/Assets/Fonts/Inter-Medium.woff b/web/src/Assets/Fonts/Inter-Medium.woff new file mode 100644 index 000000000..d546843f2 Binary files /dev/null and b/web/src/Assets/Fonts/Inter-Medium.woff differ diff --git a/web/src/Assets/Fonts/Inter-Medium.woff2 b/web/src/Assets/Fonts/Inter-Medium.woff2 new file mode 100644 index 000000000..f92498a2e Binary files /dev/null and b/web/src/Assets/Fonts/Inter-Medium.woff2 differ diff --git a/web/src/Assets/Fonts/Inter-Regular.woff b/web/src/Assets/Fonts/Inter-Regular.woff new file mode 100644 index 000000000..62d3a6187 Binary files /dev/null and b/web/src/Assets/Fonts/Inter-Regular.woff differ diff --git a/web/src/Assets/Fonts/Inter-Regular.woff2 b/web/src/Assets/Fonts/Inter-Regular.woff2 new file mode 100644 index 000000000..6c2b6893d Binary files /dev/null and b/web/src/Assets/Fonts/Inter-Regular.woff2 differ diff --git a/web/src/Assets/Fonts/Inter-SemiBold.woff b/web/src/Assets/Fonts/Inter-SemiBold.woff new file mode 100644 index 000000000..a815f43a9 Binary files /dev/null and b/web/src/Assets/Fonts/Inter-SemiBold.woff differ diff --git a/web/src/Assets/Fonts/Inter-SemiBold.woff2 b/web/src/Assets/Fonts/Inter-SemiBold.woff2 new file mode 100644 index 000000000..611e90c95 Binary files /dev/null and b/web/src/Assets/Fonts/Inter-SemiBold.woff2 differ diff --git a/web/src/Assets/Fonts/fonts.scss b/web/src/Assets/Fonts/fonts.scss index 7d294ebf2..3105c08cf 100644 --- a/web/src/Assets/Fonts/fonts.scss +++ b/web/src/Assets/Fonts/fonts.scss @@ -1,6 +1,9 @@ @font-face { font-family: 'Inter'; - src: url('Inter-SemiBold.ttf') format('truetype'); + src: url('Inter-SemiBold.woff2') format('woff2'), + /* Only serve WOFF,ttf if necessary. Otherwise,WOFF 2.0 will be served. */ + url('Inter-SemiBold.woff') format('woff'), + url('Inter-SemiBold.ttf') format('truetype'); font-weight: 600; font-style: normal; font-display: swap; @@ -8,7 +11,10 @@ @font-face { font-family: 'Inter'; - src: url('Inter-Bold.ttf') format('truetype'); + src: url('Inter-Bold.woff2') format('woff2'), + /* Only serve WOFF,ttf if necessary. Otherwise,WOFF 2.0 will be served. */ + url('Inter-Bold.woff') format('woff'), + url('Inter-Bold.ttf') format('truetype'); font-weight: 700; font-style: normal; font-display: swap; @@ -22,7 +28,10 @@ } @font-face { font-family: 'Inter'; - src: url('Inter-Regular.ttf') format('truetype'); + src: url('Inter-Regular.woff2') format('woff2'), + /* Only serve WOFF,ttf if necessary. Otherwise,WOFF 2.0 will be served. */ + url('Inter-Regular.woff') format('woff'), + url('Inter-Regular.ttf') format('truetype'); font-weight: 400; font-style: normal; font-display: swap; @@ -30,7 +39,10 @@ @font-face { font-family: 'Inter'; - src: url('Inter-Medium.ttf') format('truetype'); + src: url('Inter-Medium.woff2') format('woff2'), + /* Only serve WOFF,ttf if necessary. Otherwise,WOFF 2.0 will be served. */ + url('Inter-Medium.woff') format('woff'), + url('Inter-Medium.ttf') format('truetype'); font-weight: 500; font-style: normal; font-display: swap; diff --git a/web/src/Assets/Images/factory.webp b/web/src/Assets/Images/factory.webp new file mode 100644 index 000000000..6999d2a9f Binary files /dev/null and b/web/src/Assets/Images/factory.webp differ diff --git a/web/src/Assets/Images/forest.png b/web/src/Assets/Images/forest.png index 5768efd0b..3d19ede7b 100644 Binary files a/web/src/Assets/Images/forest.png and b/web/src/Assets/Images/forest.png differ diff --git a/web/src/Assets/Images/forest.webp b/web/src/Assets/Images/forest.webp new file mode 100644 index 000000000..734449bc4 Binary files /dev/null and b/web/src/Assets/Images/forest.webp differ diff --git a/web/src/Assets/Images/logo-slider.png b/web/src/Assets/Images/logo-slider.png index c60defeb2..737c20ba5 100644 Binary files a/web/src/Assets/Images/logo-slider.png and b/web/src/Assets/Images/logo-slider.png differ diff --git a/web/src/Assets/Images/resources.webp b/web/src/Assets/Images/resources.webp new file mode 100644 index 000000000..b7d7e6a2a Binary files /dev/null and b/web/src/Assets/Images/resources.webp differ diff --git a/web/src/Assets/Images/undp.png b/web/src/Assets/Images/undp.png index d582b61f5..d7d963ba7 100644 Binary files a/web/src/Assets/Images/undp.png and b/web/src/Assets/Images/undp.png differ diff --git a/web/src/Assets/Images/undp.webp b/web/src/Assets/Images/undp.webp new file mode 100644 index 000000000..81fc30f5f Binary files /dev/null and b/web/src/Assets/Images/undp.webp differ diff --git a/web/src/Casl/entities/ProgrammeTransfer.ts b/web/src/Casl/entities/ProgrammeTransfer.ts index 6a0e9ba39..14cf1c2d9 100644 --- a/web/src/Casl/entities/ProgrammeTransfer.ts +++ b/web/src/Casl/entities/ProgrammeTransfer.ts @@ -1,4 +1,7 @@ -import { CreditTransferStage } from '../../Definitions/InterfacesAndType/programme.definitions'; +import { + CreditTransferStage, + RetireType, +} from '../../Definitions/InterfacesAndType/programme.definitions'; import { BaseEntity } from './BaseEntity'; export class ProgrammeTransfer implements BaseEntity { @@ -22,6 +25,8 @@ export class ProgrammeTransfer implements BaseEntity { comment?: string; + txRef?: string; + txTime?: number; status?: CreditTransferStage; @@ -31,4 +36,10 @@ export class ProgrammeTransfer implements BaseEntity { companyId?: number[]; creditOwnerPercentage?: number[]; + + createdTime?: number; + + retirementType?: RetireType; + + toCompanyMeta?: any; } diff --git a/web/src/Casl/enums/programme-status.enum.ts b/web/src/Casl/enums/programme-status.enum.ts index c553bc5ef..07263e16a 100644 --- a/web/src/Casl/enums/programme-status.enum.ts +++ b/web/src/Casl/enums/programme-status.enum.ts @@ -8,3 +8,9 @@ export enum ProgrammeStage { RETIRED = 'Retired', TRANSFERRED = 'Transferred', } + +export enum ProgrammeStageLegend { // Up to this not used + AUTHORISED = 'Authorised', + REJECTED = 'Rejected', + AWAITING_AUTHORIZATION = 'AwaitingAuthorization', +} diff --git a/web/src/Casl/enums/sector.enum.ts b/web/src/Casl/enums/sector.enum.ts new file mode 100644 index 000000000..67f4bd8f0 --- /dev/null +++ b/web/src/Casl/enums/sector.enum.ts @@ -0,0 +1,12 @@ +export enum Sector { + Energy = 'Energy', + Health = 'Health', + Education = 'Education', + Transport = 'Transport', + Manufacturing = 'Manufacturing', + Hospitality = 'Hospitality', + Forestry = 'Forestry', + Waste = 'Waste', + Agriculture = 'Agriculture', + Other = 'Other', +} diff --git a/web/src/Casl/enums/statsCards.type.enum.ts b/web/src/Casl/enums/statsCards.type.enum.ts new file mode 100644 index 000000000..c5476632f --- /dev/null +++ b/web/src/Casl/enums/statsCards.type.enum.ts @@ -0,0 +1,19 @@ +export enum StatsCardsTypes { + PROGRAMMES_PENDING = 'Programmes Pending', + TRANSFER_REQUEST_RECEIVED = 'Pending Transfers Received', + PROGRAMMES_UNCERTIFIED = 'Programmes Certifiable', + TRANSFER_REQUEST_SENT = 'Pending Transfers Sent', + PROGRAMMES_CERTIFIED = 'Programmes Certified', + CREDIT_BALANCE = 'Credit Balance', + CREDIT_CERTIFIED = 'Credit Certified', + + PROGRAMMES = 'Programmes', + CREDITS = 'Credits', + CERTIFIED_CREDITS = 'Certified Credits', + TOTAL_PROGRAMMES = 'Total Programmes', + TOTAL_PROGRAMMES_SECTOR = 'Total Programmes: Sector', + TOTAL_CREDITS = 'Total Credits', + TOTAL_CREDITS_CERTIFIED = 'Total Credits Certified', + PROGRAMME_LOCATIONS = 'Programme Locations', + TRANSFER_LOCATIONS_INTERNATIONAL = 'Transfer Locations International', +} diff --git a/web/src/Components/Footer/layout.footer.scss b/web/src/Components/Footer/layout.footer.scss new file mode 100644 index 000000000..3d9115eeb --- /dev/null +++ b/web/src/Components/Footer/layout.footer.scss @@ -0,0 +1,89 @@ +@import '../../Assets/Fonts/fonts.scss'; +@import '../../Styles/variables.scss'; + + +.homepage-footer-container { + + background-color: $background-color-dark; + min-height: 200px; + height: 100%; + align-items: center; + padding: 1rem 2rem 0 1rem; + cursor: pointer; + + .footertext { + color: $dark-text-color; + padding: 5px 60px 20px 60px; + font-size: 0.875rem; + + } + .footer-raw{ + display:flex; + } + .footertext-bottom { + color: $dark-text-color; + padding: 100px 60px 28px 60px; + font-size: 0.875rem; + font-family: 'MuseoSans'; + font-weight: 100; + + .cc { + margin: 5px; + } + } + .footertext-link-container { + display: flex; + color: $dark-text-color; + padding: 100px 60px 20px 60px; + font-size: 0.75rem; + text-decoration: underline; + justify-content: right; + } + .footertext-links { + padding-left: 30px; + color: $dark-text-color; + } + + .divider { + margin: 2px 60px 2px 60px; + width: calc(100% - 120px) !important; + min-width: calc(100% - 120px); + } + .logocontainer { + display: flex; + margin-top: 10px; + + .title { + font-family: 'MuseoSans'; + font-size: 2rem; + color: $dark-text-color; + font-weight: 700; + margin-right: 0.5rem; + margin-left: 0.5rem; + } + + .title-sub { + font-weight: 100 !important; + font-family: 'MuseoSans'; + font-size: 2rem; + color: $dark-text-color; + } + + .logo { + height: 97px; + width:110px; + img { + object-fit: cover; + height: auto; + height: 70%; + margin-left: 60px; + } + } + .footer-country-name { + font-size: 0.875rem; + color: $dark-text-color; + font-family: 'MuseoSans'; + margin-left: 0.6rem; + } + } + } diff --git a/web/src/Components/Footer/layout.footer.tsx b/web/src/Components/Footer/layout.footer.tsx new file mode 100644 index 000000000..ce4e9c374 --- /dev/null +++ b/web/src/Components/Footer/layout.footer.tsx @@ -0,0 +1,67 @@ +import { Col, Divider, Row } from 'antd'; +import { useTranslation } from 'react-i18next'; +import sliderLogo from '../../Assets/Images/logo-slider.png'; +import './layout.footer.scss'; +import { CcCircle } from 'react-bootstrap-icons'; + +const LayoutFooter = () => { + const { i18n, t } = useTranslation(['common', 'homepage']); + + return ( +
+ + +
+
+ slider-logo +
+
+
+
{'CARBON'}
+
{'REGISTRY'}
+
+
+ {process.env.COUNTRY_NAME || 'Antarctic Region'} +
+
+
+ +
+ + + +
{t('homepage:footertext1')}
+ +
+ + +
+ {process.env.COUNTRY_NAME || 'Antarctic Region'} + +
+ + + + +
+
+ ); +}; + +export default LayoutFooter; diff --git a/web/src/Components/Header/layout.header.scss b/web/src/Components/Header/layout.header.scss index 127f2b8c9..9f6b58984 100644 --- a/web/src/Components/Header/layout.header.scss +++ b/web/src/Components/Header/layout.header.scss @@ -11,7 +11,6 @@ .ant-checkbox-inner { border-radius: 100%; - // background-color: $primary-color; border-color: $primary-color; } @@ -19,9 +18,6 @@ background-color: $primary-color; } - // .ant-checkbox-inner::after { - // content: none; - // } } .layout-header-container { @@ -39,7 +35,6 @@ img { margin: -14px 5px 7px 5px; object-fit: contain; - // height: auto; height: 30px; width: 30px; cursor: pointer; @@ -48,6 +43,5 @@ } .header-prof { - // margin-right: 50px; float: right; } \ No newline at end of file diff --git a/web/src/Components/Header/layout.header.tsx b/web/src/Components/Header/layout.header.tsx index cd989c80f..7b5abf2fd 100644 --- a/web/src/Components/Header/layout.header.tsx +++ b/web/src/Components/Header/layout.header.tsx @@ -1,11 +1,10 @@ -import { Col, MenuProps, Row } from 'antd'; +import { MenuProps } from 'antd'; import { useState } from 'react'; import './layout.header.scss'; import { useTranslation } from 'react-i18next'; import { HeaderProps } from '../../Definitions/InterfacesAndType/layout.header'; import { useConnection } from '../../Context/ConnectionContext/connectionContext'; import { useUserContext } from '../../Context/UserInformationContext/userInformationContext'; -import { LeftOutlined, RightOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import thumbnail from '../../Assets/Images/thumbnail.png'; @@ -35,21 +34,7 @@ const LayoutHeader = (props: HeaderProps) => { return (
- {/* */} - {/* -
{ - onToggle(!collapsed); - setCollapsed(!collapsed); - }} - > - {collapsed ? : } -
- */}
- {/* */} - {/* */}
{ }} />
- - {/*
-
- - - -
-
*/} - {/* */} - {/*
*/} + thumbnail
- {/*
*/}
); }; diff --git a/web/src/Components/ImgwithFallback/ImgWithFallback.tsx b/web/src/Components/ImgwithFallback/ImgWithFallback.tsx new file mode 100644 index 000000000..6735fe5cb --- /dev/null +++ b/web/src/Components/ImgwithFallback/ImgWithFallback.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +const ImgWithFallback = ({ + src, + alt, + fallbackSrc, + mediaType, + className, + ...delegated +}: { + src: string; + alt: string; + fallbackSrc: string; + mediaType: string; + className: string; +}) => { + return ( + + + {alt} + + ); +}; + +export default ImgWithFallback; diff --git a/web/src/Components/InfoView/info.view.scss b/web/src/Components/InfoView/info.view.scss index 0da80617d..6692d2e55 100644 --- a/web/src/Components/InfoView/info.view.scss +++ b/web/src/Components/InfoView/info.view.scss @@ -27,4 +27,4 @@ margin: 15px 25px; font-size: 0.8rem; } -} \ No newline at end of file +} diff --git a/web/src/Components/Layout/layout.scss b/web/src/Components/Layout/layout.scss index 5a6b82fd2..afe4729d9 100644 --- a/web/src/Components/Layout/layout.scss +++ b/web/src/Components/Layout/layout.scss @@ -11,31 +11,3 @@ height: 100vh; overflow: auto; } -// .layout-main-container { -// overflow: hidden; -// .layout-header-container { -// top: 0; -// z-index: 10; -// padding: 0; -// padding-left: 2vw; -// } -// .ant-layout-header { -// height: 90px; -// } - -// .layout-content-container { -// margin: 20px; -// background-color: $background-color; -// border-radius: 0.5vw; -// padding-bottom: 0px; -// } -// .ant-layout { -// background-color: $background-color; -// } - -// } -// @media screen and (max-width: 1170px) { -// .layout-container { -// margin-left: unset !important; -// } -// } diff --git a/web/src/Components/LegendItem/legendItem.scss b/web/src/Components/LegendItem/legendItem.scss new file mode 100644 index 000000000..da5380489 --- /dev/null +++ b/web/src/Components/LegendItem/legendItem.scss @@ -0,0 +1,13 @@ +.legend-item-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + font-size: 14px; + font-weight: 400; + margin-left: 0.75rem; + + .text { + margin-left: 0.2rem; + } +} diff --git a/web/src/Components/LegendItem/legendItem.tsx b/web/src/Components/LegendItem/legendItem.tsx new file mode 100644 index 000000000..926c4669f --- /dev/null +++ b/web/src/Components/LegendItem/legendItem.tsx @@ -0,0 +1,21 @@ +import React, { FC } from 'react'; +import { CircleFill } from 'react-bootstrap-icons'; +import './legendItem.scss'; + +export interface LegendItemItemProps { + text: string; + color: string; +} + +const LegendItem: FC = (props: LegendItemItemProps) => { + const { text, color } = props; + + return ( +
+ +
{text}
+
+ ); +}; + +export default LegendItem; diff --git a/web/src/Components/Models/ChangePasswordModel.tsx b/web/src/Components/Models/ChangePasswordModel.tsx index 985ec9134..a5300c857 100644 --- a/web/src/Components/Models/ChangePasswordModel.tsx +++ b/web/src/Components/Models/ChangePasswordModel.tsx @@ -22,7 +22,7 @@ const ChangePasswordModel: FC = (props: ChangePasswordProps title={
- + icon
{t('passwordReset:changePassword')}
diff --git a/web/src/Components/Models/ProgrammeRetireForm.tsx b/web/src/Components/Models/ProgrammeRetireForm.tsx index b3dece042..996df5dcc 100644 --- a/web/src/Components/Models/ProgrammeRetireForm.tsx +++ b/web/src/Components/Models/ProgrammeRetireForm.tsx @@ -17,6 +17,7 @@ import { import { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useConnection } from '../../Context/ConnectionContext/connectionContext'; +import { CompanyState } from '../../Definitions/InterfacesAndType/companyManagement.definitions'; import { addCommSep, Programme } from '../../Definitions/InterfacesAndType/programme.definitions'; import { creditUnit } from '../../Pages/Common/configs'; @@ -91,7 +92,11 @@ const ProgrammeRetireForm: FC = (props: ProgrammeRetir let totalCredits = 0; let companyName = undefined; for (const index in programme.creditOwnerPercentage) { - if (hideType && Number(programme.companyId[index]) !== myCompanyId) { + if ( + (hideType && Number(programme.companyId[index]) !== myCompanyId) || + parseInt(companies[Number(programme.companyId[index])].state) === + CompanyState.SUSPENDED.valueOf() + ) { continue; } else { companyName = companies[Number(programme.companyId[index])].name; diff --git a/web/src/Components/Models/ProgrammeTransferForm.tsx b/web/src/Components/Models/ProgrammeTransferForm.tsx index b62d68548..7264cc567 100644 --- a/web/src/Components/Models/ProgrammeTransferForm.tsx +++ b/web/src/Components/Models/ProgrammeTransferForm.tsx @@ -14,6 +14,7 @@ import { import { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useConnection } from '../../Context/ConnectionContext/connectionContext'; +import { CompanyState } from '../../Definitions/InterfacesAndType/companyManagement.definitions'; import { addCommSep, CompanyRole, @@ -83,9 +84,9 @@ const ProgrammeTransferForm: FC = ( }); setCompanyList( resp.data - .map((d: any) => ({ label: d.name, value: d.companyId })) + .map((d: any) => ({ label: d.name, value: d.companyId, state: d.state })) .filter((d: any) => { - return d.value !== userCompanyId; + return d.value !== userCompanyId && parseInt(d.state) === CompanyState.ACTIVE.valueOf(); }) ); } else { @@ -118,10 +119,12 @@ const ProgrammeTransferForm: FC = ( const companyCredit = []; for (const index in programme.creditOwnerPercentage) { if ( - (toCompanyDefault && userCompanyId !== Number(programme.companyId[index])) || - (!toCompanyDefault && - (userCompanyId === Number(programme.companyId[index]) || - companyRole === CompanyRole.GOVERNMENT)) + ((toCompanyDefault && userCompanyId !== Number(programme.companyId[index])) || + (!toCompanyDefault && + (userCompanyId === Number(programme.companyId[index]) || + companyRole === CompanyRole.GOVERNMENT))) && + parseInt(companies[Number(programme.companyId[index])].state) === + CompanyState.ACTIVE.valueOf() ) { const companyAvailableTotal = ((programme.creditBalance - (programme.creditFrozen ? programme.creditFrozen[index] : 0)) * diff --git a/web/src/Components/Models/TransferActionModel.tsx b/web/src/Components/Models/TransferActionModel.tsx index f5a0a7591..f72c2a03a 100644 --- a/web/src/Components/Models/TransferActionModel.tsx +++ b/web/src/Components/Models/TransferActionModel.tsx @@ -78,6 +78,7 @@ const TransferActionModel: FC = (props: TransferAction creditAmount: transfer.creditAmount, company: transfer.toCompanyMeta ? transfer.toCompanyMeta.name : null, country: transfer.toCompanyMeta ? transfer.toCompanyMeta.country : null, + countryName: transfer.toCompanyMeta ? transfer.toCompanyMeta.countryName : null, }} onChange={() => setPopupError(undefined)} onFinish={async (d) => { @@ -140,7 +141,7 @@ const TransferActionModel: FC = (props: TransferAction diff --git a/web/src/Components/ProfileIcon/profile.icon.tsx b/web/src/Components/ProfileIcon/profile.icon.tsx index 0fc5f419e..a378fa453 100644 --- a/web/src/Components/ProfileIcon/profile.icon.tsx +++ b/web/src/Components/ProfileIcon/profile.icon.tsx @@ -25,9 +25,9 @@ const ProfileIcon: FC = (props: ProfileIconProps) => { return ( {isBase64(icon) ? ( - + profile-icon ) : icon ? ( - + profile-icon ) : name ? ( name.charAt(0).toUpperCase() ) : ( diff --git a/web/src/Components/Sider/layout.sider.scss b/web/src/Components/Sider/layout.sider.scss index 14171ec69..2dbae91aa 100644 --- a/web/src/Components/Sider/layout.sider.scss +++ b/web/src/Components/Sider/layout.sider.scss @@ -36,7 +36,6 @@ .layout-sider-menu-container { background-color: $background-color; padding-top: 12px; - // padding-left: 10px; .ant-menu-inline-collapsed { .ant-menu-item-icon { @@ -52,8 +51,6 @@ display: flex; flex-direction: row; align-items: center; - // margin-bottom: 2vw; - // padding-left: 2rem; padding: 0rem 2rem 0 1rem; cursor: pointer; @@ -70,19 +67,12 @@ font-family: 'MuseoSans'; font-size: 1.2rem; color: $logo-text-color; - // padding-bottom: 3px; } .logo { - // display: flex; - // flex-direction: row; - // justify-content: flex-start; - // align-items: center; height: 52px; - // overflow: hidden; - // margin-right: 0.8rem; + width: 80px; - // background-color: red; img { object-fit: cover; height: auto; @@ -109,7 +99,6 @@ } .ant-menu-submenu-selected { - .ant-menu-item::after { border-left: 2px solid $primary-color; } @@ -119,7 +108,7 @@ color: $dark-text-color; border-top-right-radius: 22px; border-bottom-right-radius: 22px; - + .ant-menu-submenu-arrow { color: $dark-text-color; } @@ -131,10 +120,8 @@ .ant-menu-item-selected { background: none; - // border-left: 1px solid $primary-color; border-top-right-radius: 0px; border-bottom-right-radius: 0px; - a { color: $primary-color; @@ -152,5 +139,3 @@ } } } - - diff --git a/web/src/Components/Sider/layout.sider.tsx b/web/src/Components/Sider/layout.sider.tsx index 295af469e..56cabc880 100644 --- a/web/src/Components/Sider/layout.sider.tsx +++ b/web/src/Components/Sider/layout.sider.tsx @@ -85,6 +85,7 @@ const LayoutSider = (props: LayoutSiderProps) => { {collapsed && (
country flag = (props: StasticCardItemProps) => { - const { value, title, updatedDate, icon, loading } = props; - - const cardBackgroundColor = (type: string) => { - switch (type) { - case '1': - return '#023c66'; - case '2': - return '#034b7f'; - case '3': - return '#035998'; - case '4': - return '#0468b1'; - case '5': - return '#0577ca'; - case '6': - return '#0585e3'; - default: - return '#0894f9'; - } - }; + const { value, title, updatedDate, icon, loading, companyRole } = props; return (
@@ -45,19 +29,33 @@ const StasticCard: FC = (props: StasticCardItemProps) => { ) : ( <> -
+
{title}
- {title.includes('Credit') &&
ITMOs
} -
- {title.includes('Credit') - ? value === 0 || String(value) === 'NaN' - ? 0 - : addCommSep(value) - : value} +
+ + + +
+
+
+
+ {title.includes('Credit') &&
ITMOs
} +
+ {title.includes('Credit') + ? value === 0 || String(value) === 'NaN' + ? 0 + : addCommSep(value) + : value} +
-
{moment(updatedDate * 1000).fromNow()}
+
{icon}
-
{icon}
+ {updatedDate !== '0' &&
{updatedDate}
} )}
diff --git a/web/src/Components/StasticCard/stasticCard.scss b/web/src/Components/StasticCard/stasticCard.scss index 1f2d44ccf..a1772ae33 100644 --- a/web/src/Components/StasticCard/stasticCard.scss +++ b/web/src/Components/StasticCard/stasticCard.scss @@ -2,28 +2,21 @@ .stastic-card-main-container { display: flex; - align-items: center; - justify-content: space-between; + flex-direction: column; + // align-items: center; + // justify-content: space-between; width: 100%; - height: 11rem; + height: 11.2rem; background-color: white; border-radius: 0.625rem; - padding: 0 1.875rem 0 1.875rem; + padding: 1rem 1.875rem 0 1.875rem; - .values-section { + .title-section { display: flex; - flex-direction: column; - width: 70%; + flex-direction: row; align-items: flex-start; - margin-right: 1rem; - justify-content: center; - - .details-section { - display: flex; - width: 100%; - flex-direction: row; - padding: 0.5rem 0 0.5rem 0; - } + justify-content: space-between; + height: 3.3rem; .title { display: flex; @@ -32,43 +25,67 @@ justify-content: flex-start; font-family: 'Inter'; font-size: 1.15rem; - font-weight: 500; - width: 100%; + font-weight: 600; + width: 95%; color: rgba(58, 53, 65, 0.8); } - .unit { + .info-container { display: flex; flex-direction: row; + width: 5%; + align-items: center; + justify-content: flex-end; + } + } + + .values-section { + display: flex; + flex-direction: row; + width: 100%; + align-items: flex-start; + justify-content: space-between; + height: 5rem; + // margin-right: 1rem; + + .values-and-unit { + display: flex; + flex-direction: column; align-items: flex-start; justify-content: flex-start; - margin-bottom: -0.6rem; - margin-top: 0; - font-family: 'Inter'; - font-size: 0.875rem; - font-weight: 500; - width: 100%; - color: rgba(58, 53, 65, 0.5); - } + width: 60%; - .value { - font-family: 'Inter'; - font-size: 2rem; - font-weight: 600; - width: 100%; - color: $primary-blue; - padding: 0; + .value { + font-family: 'Inter'; + font-size: 2rem; + font-weight: 600; + width: 100%; + color: $primary-blue; + padding: 0; + } + + .unit { + margin-bottom: -0.6rem; + margin-left: 0.2rem; + margin-top: -0.66rem; + font-family: 'Inter'; + font-size: 0.875rem; + font-weight: 500; + width: 100%; + color: rgba(58, 53, 65, 0.5); + } } - .updated-on { + .icon-section { display: flex; - flex-direction: row; - font-size: 0.813rem; - border-radius: 3.125rem; - padding: 0 0.5rem 0 0.5rem; - margin-top: 1rem; - color: $primary-blue; - background-color: rgba($color: #b9e2f4, $alpha: 0.4); + flex-direction: column; + align-items: flex-end; + width: 40%; + justify-content: flex-start; + + img { + height: 80px; + } } .updated-on-null { @@ -83,16 +100,15 @@ } } - .icon-section { + .updated-on { display: flex; - flex-direction: column; - align-items: flex-end; - width: 25%; - justify-content: center; - - img { - height: 80px; - } + flex-direction: row; + font-size: 0.75rem; + border-radius: 3.125rem; + padding: 0.05rem 0.8rem 0.05rem 0.8rem; + width: max-content; + color: $primary-blue; + background-color: rgba($color: #b9e2f4, $alpha: 0.4); } } diff --git a/web/src/Components/TimelineBody/TimelineBody.scss b/web/src/Components/TimelineBody/TimelineBody.scss new file mode 100644 index 000000000..db117e9f6 --- /dev/null +++ b/web/src/Components/TimelineBody/TimelineBody.scss @@ -0,0 +1,16 @@ +@import '../../Styles/variables.scss'; + +.remark { + padding-top: 7px; + font-size: 0.75rem; + .remark-title { + text-transform: capitalize; + font-weight: 500; + color: $body-text-color-light; + } + + .remark-body { + font-weight: 400; + color: $body-text-color-light; + } +} \ No newline at end of file diff --git a/web/src/Components/TimelineBody/TimelineBody.tsx b/web/src/Components/TimelineBody/TimelineBody.tsx new file mode 100644 index 000000000..6d5a48ec7 --- /dev/null +++ b/web/src/Components/TimelineBody/TimelineBody.tsx @@ -0,0 +1,30 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import './TimelineBody.scss'; + +export interface TimelineBodyProps { + text: string; + remark?: string | null; + via?: string | null; +} + +const TimelineBody: FC = (props: TimelineBodyProps) => { + const { i18n, t } = useTranslation(['view']); + const { text, remark, via } = props; + return ( +
+
+ {text} + {via && {` ${t('view:via')} ${via}`}} +
+ {remark && remark !== 'undefined' && ( +
+
{t('view:remarks')}
+
{remark}
+
+ )} +
+ ); +}; + +export default TimelineBody; diff --git a/web/src/Context/UserInformationContext/userInformationContext.tsx b/web/src/Context/UserInformationContext/userInformationContext.tsx index f87fb5ee7..6c920220c 100644 --- a/web/src/Context/UserInformationContext/userInformationContext.tsx +++ b/web/src/Context/UserInformationContext/userInformationContext.tsx @@ -31,7 +31,7 @@ export const UserInformationContextProvider = ({ children }: React.PropsWithChil : '', companyState: localStorage.getItem('companyState') ? parseInt(localStorage.getItem('companyState') as string) - : -1, + : 0, }; const [userInfoState, setUserInfoState] = useState(initialUserProps); diff --git a/web/src/Definitions/InterfacesAndType/companyManagement.definitions.tsx b/web/src/Definitions/InterfacesAndType/companyManagement.definitions.tsx index 8562a18af..4545fd2e8 100644 --- a/web/src/Definitions/InterfacesAndType/companyManagement.definitions.tsx +++ b/web/src/Definitions/InterfacesAndType/companyManagement.definitions.tsx @@ -1,15 +1,20 @@ export interface CompanyTableDataType { - name?: string; - address?: string; - companyId?: number; - companyRole?: string; - country?: string; - email?: string; - phoneNo?: string; - taxId?: string; - website?: string; - state?: string; - logo?: string; - creditBalance?: number; - programmeCount?: number; -} \ No newline at end of file + name?: string; + address?: string; + companyId?: number; + companyRole?: string; + country?: string; + email?: string; + phoneNo?: string; + taxId?: string; + website?: string; + state?: string; + logo?: string; + creditBalance?: number; + programmeCount?: number; +} + +export enum CompanyState { + SUSPENDED = 0, + ACTIVE = 1, +} diff --git a/web/src/Definitions/InterfacesAndType/programme.definitions.tsx b/web/src/Definitions/InterfacesAndType/programme.definitions.tsx index ce30e0c58..2f9f3fe3c 100644 --- a/web/src/Definitions/InterfacesAndType/programme.definitions.tsx +++ b/web/src/Definitions/InterfacesAndType/programme.definitions.tsx @@ -18,6 +18,12 @@ export enum ProgrammeStage { // // Frozen = 'Frozen', // } +export enum RetireType { + CROSS_BORDER = '0', + LEGAL_ACTION = '1', + OTHER = '2', +} + export enum CreditTransferStage { Pending = 'Pending', Approved = 'Accepted', @@ -37,6 +43,7 @@ export enum TxType { REVOKE = '6', FREEZE = '7', AUTH = '8', + UNFREEZE = '9', } export enum SectoralScope { @@ -164,8 +171,8 @@ export interface Programme { creditIssued: number; creditEst: number; creditBalance: number; - creditTransferred: number; - creditRetired: number; + creditTransferred: number[]; + creditRetired: number[]; creditFrozen: number[]; constantVersion: string; proponentTaxVatId: string[]; @@ -220,6 +227,10 @@ export const addCommSepRound = (value: any) => { .replace(/\B(?=(\d{3})+(?!\d))/g, ','); }; +export const addRoundNumber = (value: any) => { + return Number(value.toFixed(2).replace('.00', '')); +}; + export const addSpaces = (text: string) => { if (!text) { return text; @@ -262,3 +273,11 @@ export const getRetirementTypeString = (retirementType: string | null) => { return 'OTHER'; } }; + +export const sumArray = (arrList: any[]) => { + if (arrList === undefined || arrList === null) { + return 0; + } + + return arrList.reduce((a, b) => Number(a) + Number(b), 0); +}; diff --git a/web/src/Pages/AddUser/addUser.scss b/web/src/Pages/AddUser/addUser.scss index bb363a676..af0b35eb1 100644 --- a/web/src/Pages/AddUser/addUser.scss +++ b/web/src/Pages/AddUser/addUser.scss @@ -154,7 +154,6 @@ .create-user-image-upload { display: flex; flex-direction: row; - // flex: 1; align-items: center; .create-user-image-upload-label { margin-right: 2vw; diff --git a/web/src/Pages/CodeofConduct/codeofConduct.scss b/web/src/Pages/CodeofConduct/codeofConduct.scss index 3d5308e2b..427c276cc 100644 --- a/web/src/Pages/CodeofConduct/codeofConduct.scss +++ b/web/src/Pages/CodeofConduct/codeofConduct.scss @@ -7,10 +7,6 @@ overflow-x: hidden; background-color: $background-color; - .ant-row { - display: block; - } - .code-header-container { background-color: rgba(255, 255, 255, 0.8); height: 90px; @@ -59,20 +55,19 @@ .code-body-container { text-align: left; justify-content: left; - background-color: rgba(255, 255, 255, 0.8); + background-color:white; margin-top: 5px; margin-bottom: 5px; padding-bottom: 60px; - + .code-body { color: $title-text-color; - padding:20px 200px 20px 200px; + padding: 20px 200px 20px 200px; } - .code-body-contact - { + .code-body-contact { color: $title-text-color; text-align: left; - padding:20px 60px 20px 60px; + padding: 20px 60px 20px 60px; } .code-sub { padding-bottom: 30px; @@ -89,11 +84,14 @@ text-transform: uppercase; padding: 50px 50px 10px 50px; + .code-row { + display: block !important; + } + @media (max-width: $lg-size) { font-size: 2rem; margin-top: 1rem; color: $title-text-color; - // width: 100vw; line-height: 1.5em; } } @@ -101,7 +99,7 @@ display: flex; text-align: left; justify-content: left; - background-color: rgba(255, 255, 255, 0.8); + background-color: #fff; color: $title-text-color; padding-top: 30px; font-size: 20px; @@ -115,13 +113,11 @@ font-weight: 700; padding-bottom: 20px; } - .code-card-subtitle-text{ + .code-card-subtitle-text { margin: 10px 60px 10px 60px; - } .code-card-container { text-align: left; - // justify-content: center; margin: 30px 60px 30px 60px; padding: 30px; border-style: solid; diff --git a/web/src/Pages/CodeofConduct/codeofConduct.tsx b/web/src/Pages/CodeofConduct/codeofConduct.tsx index 383ad847e..346defe5a 100644 --- a/web/src/Pages/CodeofConduct/codeofConduct.tsx +++ b/web/src/Pages/CodeofConduct/codeofConduct.tsx @@ -4,7 +4,9 @@ import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import i18next from 'i18next'; import sliderLogo from '../../Assets/Images/logo-slider.png'; +import LayoutFooter from '../../Components/Footer/layout.footer'; import './codeofConduct.scss'; +import { CcCircle } from 'react-bootstrap-icons'; const CodeOfConduct = () => { const { i18n, t } = useTranslation(['common', 'homepage']); const navigate = useNavigate(); @@ -37,8 +39,8 @@ const CodeOfConduct = () => {
- - + +
CONTRIBUTOR COVENANT CODE OF CONDUCT
@@ -230,9 +232,9 @@ const CodeOfConduct = () => {
Attribution

- This Code of Conduct is adapted from the{' '} - Contributor Covenant, version - 2.0, available at + This Code of Conduct is adapted from the + Contributor Covenant, version + 2.0, available at{' '} https://www.contributor-covenant.org/version/2/0/code_of_conduct.html @@ -241,12 +243,13 @@ const CodeOfConduct = () => {

Community Impact Guidelines were inspired by + {' '} Mozilla ’s code of conduct enforcement ladder .

- For answers to common questions about this code of conduct, see the FAQ at + For answers to common questions about this code of conduct, see the FAQ at{' '} https://www.contributor-covenant.org/faq @@ -260,6 +263,55 @@ const CodeOfConduct = () => {

+
+ {/* + +
+
+ slider-logo +
+
+
+
{'CARBON'}
+
{'REGISTRY'}
+
+
+ {process.env.COUNTRY_NAME || 'Antarctic Region'} +
+
+
+ +
+ + + +
{t('homepage:footertext1')}
+ +
+ + +
+ {process.env.COUNTRY_NAME || 'Antarctic Region'} + +
+ + + + {t('homepage:Cookie')} + + + {t('homepage:codeconduct')} + + + {t('homepage:terms')} + + + {t('homepage:privacy')} + + +
*/} + +
); }; diff --git a/web/src/Pages/Common/common.form.scss b/web/src/Pages/Common/common.form.scss index fa2057561..34b4f97a0 100644 --- a/web/src/Pages/Common/common.form.scss +++ b/web/src/Pages/Common/common.form.scss @@ -37,7 +37,6 @@ display: flex; flex-direction: column; padding-left: 0.8vw; - // background-color: red !important; } .ant-form-item-explain-error { padding-left: 1.5rem; diff --git a/web/src/Pages/Company/addNewCompany.tsx b/web/src/Pages/Company/addNewCompany.tsx index d90a3dec3..b8eb37f2b 100644 --- a/web/src/Pages/Company/addNewCompany.tsx +++ b/web/src/Pages/Company/addNewCompany.tsx @@ -104,9 +104,11 @@ const AddNewCompany = () => { requestData.company.logo = logoUrls[1]; const response = await post('national/user/add', requestData); if (response.status === 200 || response.status === 201) { - setUserInfo({ - companyLogo: response.data.logo, - } as UserProps); + if (isUpdate) { + setUserInfo({ + companyLogo: response.data.logo, + } as UserProps); + } message.open({ type: 'success', content: 'Company added Successfully!', diff --git a/web/src/Pages/CompanyProfile/companyProfile.scss b/web/src/Pages/CompanyProfile/companyProfile.scss index 4a3b87f03..f409ca420 100644 --- a/web/src/Pages/CompanyProfile/companyProfile.scss +++ b/web/src/Pages/CompanyProfile/companyProfile.scss @@ -48,6 +48,11 @@ border-color: #ff8183 !important; } + .btn-activate { + color: #16b1ff !important; + border-color: #16b1ff !important; + } + .ant-btn[disabled] { border-color: #d9d9d9 !important; background: #f5f5f5 !important; diff --git a/web/src/Pages/CompanyProfile/companyProfile.tsx b/web/src/Pages/CompanyProfile/companyProfile.tsx index 2a0434aa4..5904fd674 100644 --- a/web/src/Pages/CompanyProfile/companyProfile.tsx +++ b/web/src/Pages/CompanyProfile/companyProfile.tsx @@ -23,6 +23,7 @@ const CompanyProfile = () => { const [isLoading, setIsLoading] = useState(true); const [actionInfo, setActionInfo] = useState({}); const [openDeauthorisationModal, setOpenDeauthorisationModal] = useState(false); + const [openReactivateModal, setOpenReactivateModal] = useState(false); const [errorMsg, setErrorMsg] = useState(''); const [userRole, setUserRole] = useState(''); const [companyRole, setCompanyRole] = useState(''); @@ -71,10 +72,35 @@ const CompanyProfile = () => { } }; + const onReactivateOrgConfirmed = async (remarks: string) => { + try { + const response: any = await put( + `national/organisation/activate?id=${companyDetails.companyId}`, + { + remarks: remarks, + } + ); + setOpenReactivateModal(false); + message.open({ + type: 'success', + content: t('companyProfile:reactivationSuccess'), + duration: 3, + style: { textAlign: 'right', marginRight: 15, marginTop: 10 }, + }); + getCompanyDetails(companyDetails.companyId); + } catch (exception: any) { + setErrorMsg(exception.message); + } + }; + const onDeauthoriseOrgCanceled = () => { setOpenDeauthorisationModal(false); }; + const onReactivateOrgCanceled = () => { + setOpenReactivateModal(false); + }; + const onDeauthoriseOrganisation = () => { setActionInfo({ action: `${t('companyProfile:deauthorise')}`, @@ -87,6 +113,18 @@ const CompanyProfile = () => { setOpenDeauthorisationModal(true); }; + const onReActivateOrganisation = () => { + setActionInfo({ + action: `${t('companyProfile:reActivate')}`, + headerText: `${t('companyProfile:reActivateConfirmHeaderText')}`, + text: `${t('companyProfile:reActivateConfirmText')}`, + type: 'primary', + icon: , + }); + setErrorMsg(''); + setOpenReactivateModal(true); + }; + return (
@@ -104,6 +142,17 @@ const CompanyProfile = () => { ) : ( '' )} + + {ability.can(Action.Delete, plainToClass(Company, companyDetails)) && + !isLoading && + parseInt(companyDetails.state) !== 1 ? ( + + ) : ( + '' + )} + {ability.can(Action.Update, plainToClass(Company, companyDetails)) && !isLoading && (
); }; diff --git a/web/src/Pages/CookiePolicy/cookiePolicy.scss b/web/src/Pages/CookiePolicy/cookiePolicy.scss index a5d60457c..53f1a2c31 100644 --- a/web/src/Pages/CookiePolicy/cookiePolicy.scss +++ b/web/src/Pages/CookiePolicy/cookiePolicy.scss @@ -7,10 +7,6 @@ overflow-x: hidden; background-color: $background-color; - .ant-row { - display: block; - } - .cookie-header-container { background-color: rgba(255, 255, 255, 0.8); height: 90px; @@ -59,19 +55,23 @@ .cookie-body-container { text-align: center; justify-content: center; - background-color: rgba(255, 255, 255, 0.8); + background-color: #fff; margin-top: 5px; margin-bottom: 5px; padding-bottom: 60px; + a{ + color:#004F9E !important; + } + .cookie-body { color: $title-text-color; - padding: 20px 60px 20px 60px; + padding: 20px 200px 20px 200px; } .cookie-body-contact { color: $title-text-color; text-align: left; - padding: 20px 60px 20px 60px; + padding: 20px 200px 20px 200px; } .cookie-sub { padding-bottom: 30px; @@ -92,7 +92,6 @@ font-size: 2rem; margin-top: 1rem; color: $title-text-color; - // width: 100vw; line-height: 1.5em; } } @@ -114,12 +113,11 @@ padding-bottom: 20px; } .cookie-card-subtitle-text { - margin: 10px 60px 10px 60px; + margin: 10px 200px 10px 200px; } .cookie-card-container { text-align: left; - // justify-content: center; - margin: 30px 60px 30px 60px; + margin: 30px 200px 30px 200px; padding: 30px; border-style: solid; border-color: $common-form-input-border; diff --git a/web/src/Pages/CookiePolicy/cookiePolicy.tsx b/web/src/Pages/CookiePolicy/cookiePolicy.tsx index cc6b68f73..bcd03ce21 100644 --- a/web/src/Pages/CookiePolicy/cookiePolicy.tsx +++ b/web/src/Pages/CookiePolicy/cookiePolicy.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import i18next from 'i18next'; import sliderLogo from '../../Assets/Images/logo-slider.png'; import './cookiePolicy.scss'; +import { CcCircle } from 'react-bootstrap-icons'; const CookiePolicy = () => { const { i18n, t } = useTranslation(['common', 'homepage']); const navigate = useNavigate(); @@ -38,16 +39,18 @@ const CookiePolicy = () => {
- +
COOKIE POLICY
Last updated February 02, 2023
This Cookie Policy explains how United Nations Development Programme ("Company" - , "we", "us", and "our") uses cookies and similar technologies to + , "we","us", and "our") uses cookies and similar technologies to recognize you when you visit our websites at{' '} - https://carbreg.org, ("Websites"). It explains what - these technologies are and why we use them, as well as your rights to control our use - of them. + + https://carbreg.org + + , ("Websites"). It explains what these technologies are and why we use them, as + well as your rights to control our use of them.
In some cases we may use cookies to collect personal information, or that becomes personal information if we combine it with other information. @@ -82,9 +85,9 @@ const CookiePolicy = () => { "essential" or "strictly necessary" cookies. Other cookies also enable us to track and target the interests of our users to enhance the experience on our Online Properties. Third parties serve cookies through our Websites for advertising, analytics and other - purposes. This is described in more detail below. The specific types of first and - third party cookies served through our Websites and the purposes they perform are - described below + purposes. This is described in more detail below. +
The specific types of first and third party cookies served through our Websites + and the purposes they perform are described below
(please note that the specific cookies served may vary depending on the specific Online Properties you visit):
@@ -107,8 +110,16 @@ const CookiePolicy = () => { browser-to-browser, you should visit your browser's help menu for more information.
In addition, most advertising networks offer you a way to opt out of targeted advertising. If you would like to find out more information, please visit - http://www.aboutads.info/choices/ or - http://www.youronlinechoices.com . + + {' '} + http://www.aboutads.info/choices/ + {' '} + or + + {' '} + http://www.youronlinechoices.com + + .
The specific types of first and third party cookies served through our Websites and the purposes they perform are described in the table below (please note that the specific cookies served may vary depending on the specific Online Properties you @@ -117,7 +128,7 @@ const CookiePolicy = () => { - +
Essential website cookies:
These cookies are strictly necessary to provide you with services available through @@ -133,8 +144,8 @@ const CookiePolicy = () => { Purpose: Used to maintain an anonymous user session by the server in Java™ 2 Platform - Enterprise Edition web applications. It is a necessary cookie that expires at - the end of a session. + Enterprise Edition web applications. +
It is a necessary cookie that expires at the end of a session. @@ -145,7 +156,10 @@ const CookiePolicy = () => { Service: JavaServer Pages Technologies{' '} - + View Service Privacy Policy {' '} @@ -185,7 +199,10 @@ const CookiePolicy = () => { Service: reCAPTCHA{' '} - View Service Privacy Policy{' '} + + {' '} + View Service Privacy Policy + {' '} @@ -196,9 +213,10 @@ const CookiePolicy = () => { Type: http_cookie + Expires in: - 1 year + 5 months 27 days
@@ -219,8 +237,8 @@ const CookiePolicy = () => { Service: - Termly - + Termly{' '} + View Service Privacy Policy {' '} @@ -242,7 +260,7 @@ const CookiePolicy = () => {
- +
Analytics and customization cookies:
These cookies collect information that is used either in aggregate form to help us @@ -261,7 +279,9 @@ const CookiePolicy = () => { Provider: - https://carbreg.org/ + + https://carbreg.org/ + Service: @@ -302,7 +322,7 @@ const CookiePolicy = () => { Service: Google Analytics{' '} - + View Service Privacy Policy @@ -331,8 +351,8 @@ const CookiePolicy = () => { Purpose: Used to distinguish individual users by means of designation of a randomly - generated number as client identifier, which allows calculation of visits and - sessions + generated number as client identifier, +
which allows calculation of visits and sessions @@ -342,8 +362,8 @@ const CookiePolicy = () => { Service: - Google analytics - + Google analytics{' '} + View Service Privacy Policy {' '} @@ -365,7 +385,7 @@ const CookiePolicy = () => { - +
Advertising cookies:
These cookies are used to make advertising messages more relevant to you. They perform @@ -388,13 +408,15 @@ const CookiePolicy = () => { Provider: - https://carbreg.org + + https://carbreg.org + Service: Cox Digital Solutions (Fomerly Adify){' '} - + View Service Privacy Policy {' '} @@ -433,7 +455,10 @@ const CookiePolicy = () => { Service: Facebook{' '} - + View Service Privacy Policy {' '} @@ -455,7 +480,7 @@ const CookiePolicy = () => { - +
Unclassified cookies:
These are cookies that have not yet been categorized. We are in the process of @@ -473,7 +498,9 @@ const CookiePolicy = () => { Provider: - https://carbreg.org + + https://carbreg.org + Service: @@ -496,7 +523,7 @@ const CookiePolicy = () => { - +
What about other tracking technologies, like web beacons?
@@ -525,11 +552,17 @@ const CookiePolicy = () => {
If you do not want Flash Cookies stored on your computer, you can adjust the settings of your Flash player to block Flash Cookies storage using the tools contained in the{' '} - + Website Storage Settings Panel . You can also control Flash Cookies by going to the{' '} - + Global Storage Settings Panel {' '} and following the instructions (which may include instructions that explain, for @@ -545,7 +578,7 @@ const CookiePolicy = () => {
- +
Do you serve targeted advertising?
Third parties may serve cookies on your computer or mobile device to serve advertising @@ -562,7 +595,7 @@ const CookiePolicy = () => { - +
How often will you update this Cookie Policy?
We may update this Cookie Policy from time to time in order to reflect, for example, @@ -574,14 +607,14 @@ const CookiePolicy = () => { - +
Where can I get further information?
If you have any questions about our use of cookies or other technologies, please email us at digital@undp.org or by post to:

-
United Nations Development Programme 1 United Nations Plaza New York, +
United Nations Development Programme 1 United Nations Plaza,
New York United States
Phone: +260-211-263258
This cookie policy was created using Termly's{' '} @@ -593,6 +626,54 @@ const CookiePolicy = () => {
+
+ + +
+
+ slider-logo +
+
+
+
{'CARBON'}
+
{'REGISTRY'}
+
+
+ {process.env.COUNTRY_NAME || 'Antarctic Region'} +
+
+
+ +
+ + + +
{t('homepage:footertext1')}
+ +
+ + +
+ {process.env.COUNTRY_NAME || 'Antarctic Region'} + +
+ + + + {t('homepage:Cookie')} + + + {t('homepage:codeconduct')} + + + {t('homepage:terms')} + + + {t('homepage:privacy')} + + +
+
); }; diff --git a/web/src/Pages/Dashboard/DUMMY_DATAS.ts b/web/src/Pages/Dashboard/CHART_OPTIONS.ts similarity index 69% rename from web/src/Pages/Dashboard/DUMMY_DATAS.ts rename to web/src/Pages/Dashboard/CHART_OPTIONS.ts index 8ffe1bfdf..a725be10c 100644 --- a/web/src/Pages/Dashboard/DUMMY_DATAS.ts +++ b/web/src/Pages/Dashboard/CHART_OPTIONS.ts @@ -1,54 +1,24 @@ -export const seriesX = [ - { - name: 'Land', - data: [3, 5, 2, 5, 3, 5, 3, 2], - }, - { - name: 'Agriculture', - data: [2, 2, 6, 1, 6, 2, 6, 2], - }, - { - name: 'Manufacturing', - data: [2, 4, 3, 3, 2, 3, 5, 4], - }, - { - name: 'Forest', - data: [3, 2, 3, 4, 2, 1, 3, 3], - }, - { - name: 'Energy', - data: [2, 4, 2, 4, 2, 5, 2, 2], - }, -]; - -export const seriesA = [ - { - name: 'Rice crops', - data: [8, 5, 2, 4, 9, 2, 2, 3], - }, - { - name: 'CO₂ Recycling', - data: [4, 5, 4, 5, 2, 4, 4, 6], - }, - { - name: 'Agriculture', - data: [4, 3, 2, 3, 2, 2, 2, 4], - }, - { - name: 'Iron & steel', - data: [2, 2, 2, 6, 2, 5, 4, 3], - }, - { - name: 'Solar & hydro', - data: [4, 7, 6, 3, 4, 3, 3, 7], - }, -]; +import { addCommSepRound } from '../../Definitions/InterfacesAndType/programme.definitions'; export const totalProgrammesOptions: any = { states: { - active: { + normal: { + filter: { + type: 'none', + value: 0, + }, + }, + hover: { filter: { type: 'none', + value: 0, + }, + }, + active: { + allowMultipleDataPointsSelection: true, + filter: { + type: 'darken', + value: 0.7, }, }, }, @@ -74,7 +44,7 @@ export const totalProgrammesOptions: any = { }, ], xaxis: { - categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'], + categories: [], }, yaxis: { show: true, @@ -91,6 +61,11 @@ export const totalProgrammesOptions: any = { cssClass: 'apexcharts-yaxis-title', }, }, + labels: { + formatter: (value: any) => { + return addCommSepRound(value); + }, + }, }, fill: { opacity: 1, @@ -157,11 +132,26 @@ export const totalProgrammesOptions: any = { }, }, }; + export const totalProgrammesOptionsSub: any = { states: { - active: { + normal: { filter: { type: 'none', + value: 0, + }, + }, + hover: { + filter: { + type: 'none', + value: 0, + }, + }, + active: { + allowMultipleDataPointsSelection: true, + filter: { + type: 'darken', + value: 0.7, }, }, }, @@ -187,7 +177,10 @@ export const totalProgrammesOptionsSub: any = { }, ], xaxis: { - categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'], + categories: [], + labels: { + rotatealways: true, + }, }, yaxis: { show: true, @@ -204,6 +197,11 @@ export const totalProgrammesOptionsSub: any = { cssClass: 'apexcharts-yaxis-title', }, }, + labels: { + formatter: (value: any) => { + return addCommSepRound(value); + }, + }, }, fill: { opacity: 1, @@ -305,9 +303,23 @@ export const totalProgrammesOptionsSub: any = { }; export const totalCreditsOptions: any = { states: { - active: { + normal: { filter: { type: 'none', + value: 0, + }, + }, + hover: { + filter: { + type: 'none', + value: 0, + }, + }, + active: { + allowMultipleDataPointsSelection: true, + filter: { + type: 'darken', + value: 0.7, }, }, }, @@ -333,7 +345,7 @@ export const totalCreditsOptions: any = { }, ], xaxis: { - categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'], + categories: [], }, yaxis: { show: true, @@ -350,6 +362,11 @@ export const totalCreditsOptions: any = { cssClass: 'apexcharts-yaxis-title', }, }, + labels: { + formatter: (value: any) => { + return addCommSepRound(value); + }, + }, }, fill: { opacity: 1, @@ -419,9 +436,23 @@ export const totalCreditsOptions: any = { export const totalCreditsCertifiedOptions: any = { states: { - active: { + normal: { + filter: { + type: 'none', + value: 0, + }, + }, + hover: { filter: { type: 'none', + value: 0, + }, + }, + active: { + allowMultipleDataPointsSelection: true, + filter: { + type: 'darken', + value: 0.7, }, }, }, @@ -447,7 +478,7 @@ export const totalCreditsCertifiedOptions: any = { }, ], xaxis: { - categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'], + categories: [], }, yaxis: { show: true, @@ -464,6 +495,11 @@ export const totalCreditsCertifiedOptions: any = { cssClass: 'apexcharts-yaxis-title', }, }, + labels: { + formatter: (value: any) => { + return addCommSepRound(value); + }, + }, }, fill: { opacity: 1, @@ -531,212 +567,28 @@ export const totalCreditsCertifiedOptions: any = { }, }; -export const seriesY = [ - { - name: 'Authorised', - data: [44, 55, 41, 67, 22, 43, 21, 49], - }, - { - name: 'Rejected', - data: [13, 23, 20, 8, 13, 27, 33, 12], - }, - { - name: 'Pending', - data: [11, 17, 15, 15, 21, 14, 15, 13], - }, -]; - -export const seriesZ = [ - { - name: 'Authorised', - data: [22, 31, 41, 67, 22, 40, 21, 11], - }, - { - name: 'Rejected', - data: [13, 12, 20, 15, 23, 27, 43, 12], - }, - { - name: 'Pending', - data: [11, 23, 15, 33, 21, 14, 15, 13], - }, -]; - -export const optionsP: any = { - chart: { - type: 'pie', - height: 400, - }, - title: { - text: '', - align: 'left', - margin: 10, - offsetX: 0, - offsetY: 0, - floating: false, - style: { - fontSize: '16px', - fontWeight: 'bold', - fontFamily: 'Inter-Regular', - color: '#263238', - }, - }, - fill: { - opacity: 1, - colors: ['#414487', '#2A788E', '#22A884', '#7AD151', '#FDE725'], - }, - labels: ['Energy', 'Manufacturing', 'Agriculture', 'Forest', 'Land'], - legend: { - show: true, - showForSingleSeries: false, - showForNullSeries: true, - showForZeroSeries: true, - position: 'bottom', - horizontalAlign: 'center', - floating: false, - fontSize: '14px', - fontFamily: 'Inter-Regular', - fontWeight: 400, - formatter: undefined, - inverseOrder: false, - width: undefined, - height: undefined, - tooltipHoverFormatter: undefined, - customLegendItems: [], - offsetX: 0, - offsetY: 5, - labels: { - colors: ['#414487', '#2A788E', '#22A884', '#7AD151', '#FDE725'], - useSeriesColors: false, - }, - markers: { - width: 12, - height: 12, - strokeWidth: 0, - strokeColor: '#fff', - fillColors: ['#414487', '#2A788E', '#22A884', '#7AD151', '#FDE725'], - radius: 12, - customHTML: undefined, - onClick: undefined, - offsetX: 0, - offsetY: 0, - }, - itemMargin: { - horizontal: 5, - vertical: 0, - }, - onItemClick: { - toggleDataSeries: true, - }, - onItemHover: { - highlightDataSeries: true, - }, - }, - responsive: [ - { - breakpoint: 480, - options: { - chart: { - width: 200, - }, - legend: { - position: 'bottom', - }, +export const optionDonutPieA: any = { + states: { + normal: { + filter: { + type: 'none', + value: 0, }, }, - ], - tooltip: { - fillSeriesColor: ['#414487', '#2A788E', '#22A884', '#7AD151', '#FDE725'], - }, - colors: ['#414487', '#2A788E', '#22A884', '#7AD151', '#FDE725'], -}; - -export const optionsQ: any = { - chart: { - type: 'pie', - height: 400, - }, - title: { - text: 'Overall Rejected Programmes', - align: 'left', - margin: 10, - offsetX: 0, - offsetY: 0, - floating: false, - style: { - fontSize: '16px', - fontWeight: 'bold', - fontFamily: 'Inter-Regular', - color: '#263238', - }, - }, - fill: { - opacity: 1, - colors: ['#A8006D', '#D3014C', '#FF3701', '#FFAB00', '#FDE725'], - }, - labels: ['Energy', 'Manufacturing', 'Agriculture', 'Forest', 'Land'], - legend: { - show: true, - showForSingleSeries: false, - showForNullSeries: true, - showForZeroSeries: true, - position: 'right', - horizontalAlign: 'center', - floating: false, - fontSize: '14px', - fontFamily: 'Inter-Regular', - fontWeight: 400, - formatter: undefined, - inverseOrder: false, - width: undefined, - height: undefined, - tooltipHoverFormatter: undefined, - customLegendItems: [], - offsetX: 0, - offsetY: 5, - labels: { - colors: ['#A8006D', '#D3014C', '#FF3701', '#FFAB00', '#FDE725'], - useSeriesColors: false, - }, - markers: { - width: 12, - height: 12, - strokeWidth: 0, - strokeColor: '#fff', - fillColors: ['#A8006D', '#D3014C', '#FF3701', '#FFAB00', '#FDE725'], - radius: 12, - customHTML: undefined, - onClick: undefined, - offsetX: 0, - offsetY: 0, - }, - itemMargin: { - horizontal: 5, - vertical: 0, - }, - onItemClick: { - toggleDataSeries: true, - }, - onItemHover: { - highlightDataSeries: true, + hover: { + filter: { + type: 'none', + value: 0, + }, }, - }, - responsive: [ - { - breakpoint: 480, - options: { - chart: { - width: 200, - }, - legend: { - position: 'bottom', - }, + active: { + allowMultipleDataPointsSelection: true, + filter: { + type: 'darken', + value: 0.7, }, }, - ], - colors: ['#A8006D', '#D3014C', '#FF3701', '#FFAB00', '#FDE725'], -}; - -export const optionDonutPieA: any = { + }, chart: { type: 'donut', }, @@ -744,7 +596,7 @@ export const optionDonutPieA: any = { enabled: false, }, colors: ['#6ACDFF', '#7FEABF', '#CDCDCD', '#FF8183'], - labels: ['Authorised', 'Issued', 'Transfered', 'Retired'], + labels: ['Authorised', 'Issued', 'Transferred', 'Retired'], plotOptions: { pie: { expandOnClick: false, @@ -797,8 +649,8 @@ export const optionDonutPieA: any = { offsetY: 0, }, itemMargin: { - horizontal: 5, - vertical: 0, + horizontal: 10, + vertical: 3, }, onItemClick: { toggleDataSeries: true, @@ -823,6 +675,27 @@ export const optionDonutPieA: any = { }; export const optionDonutPieB: any = { + states: { + normal: { + filter: { + type: 'none', + value: 0, + }, + }, + hover: { + filter: { + type: 'none', + value: 0, + }, + }, + active: { + allowMultipleDataPointsSelection: true, + filter: { + type: 'darken', + value: 0.7, + }, + }, + }, chart: { type: 'donut', }, @@ -883,8 +756,8 @@ export const optionDonutPieB: any = { offsetY: 0, }, itemMargin: { - horizontal: 5, - vertical: 0, + horizontal: 10, + vertical: 3, }, onItemClick: { toggleDataSeries: true, diff --git a/web/src/Pages/Dashboard/ProgrammeRejectAndTransfer.tsx b/web/src/Pages/Dashboard/ProgrammeRejectAndTransfer.tsx index bd6d88dae..135e3ea3a 100644 --- a/web/src/Pages/Dashboard/ProgrammeRejectAndTransfer.tsx +++ b/web/src/Pages/Dashboard/ProgrammeRejectAndTransfer.tsx @@ -1,12 +1,18 @@ -import React, { FC } from 'react'; -import { DatePicker, Progress, Skeleton } from 'antd'; +import React, { FC, useEffect } from 'react'; +import { DatePicker, Progress, Skeleton, Tooltip } from 'antd'; import Chart from 'react-apexcharts'; import ReactMapboxGl, { Layer, Feature } from 'react-mapbox-gl'; import 'mapbox-gl/dist/mapbox-gl.css'; import fileText from '../../Assets/Images/fileText.svg'; import { CarOutlined, CloseCircleOutlined } from '@ant-design/icons'; import moment from 'moment'; -import { ClockHistory, HandThumbsUp, XCircle, Clipboard2Pulse } from 'react-bootstrap-icons'; +import { + ClockHistory, + HandThumbsUp, + XCircle, + Clipboard2Pulse, + InfoCircle, +} from 'react-bootstrap-icons'; const { RangePicker } = DatePicker; @@ -22,12 +28,18 @@ export interface ProgrammeRejectAndTransferCardItemProps { rejected: number; updatedDate: any; loading: boolean; + toolTipText: string; } const ProgrammeRejectAndTransfer: FC = ( props: ProgrammeRejectAndTransferCardItemProps ) => { - const { totalPrgrammes, pending, rejected, authorized, updatedDate, loading } = props; + const { totalPrgrammes, pending, rejected, authorized, updatedDate, loading, toolTipText } = + props; + + useEffect(() => { + console.log({ pending, totalPrgrammes }); + }); return (
{loading ? ( @@ -37,9 +49,21 @@ const ProgrammeRejectAndTransfer: FC =
) : ( <> +
+
Programmes
+
+ + + +
+
-
Programmes
Total
{totalPrgrammes}
@@ -74,7 +98,7 @@ const ProgrammeRejectAndTransfer: FC =
@@ -90,7 +114,7 @@ const ProgrammeRejectAndTransfer: FC = showInfo={false} percent={(rejected / totalPrgrammes) * 100} status="active" - strokeColor={{ from: '#F0F0F0', to: '#D8D8D8' }} + strokeColor={{ from: '#FFA6A6', to: '#FF8183' }} />
@@ -98,7 +122,7 @@ const ProgrammeRejectAndTransfer: FC =
- +
@@ -111,14 +135,14 @@ const ProgrammeRejectAndTransfer: FC = showInfo={false} percent={(pending / totalPrgrammes) * 100} status="active" - strokeColor={{ from: '#FFA6A6', to: '#FF8183' }} + strokeColor={{ from: '#F0F0F0', to: '#D8D8D8' }} />
-
{moment(updatedDate).fromNow()}
+ {updatedDate !== '0' &&
{updatedDate}
}
)} diff --git a/web/src/Pages/Dashboard/barChartStats.tsx b/web/src/Pages/Dashboard/barChartStats.tsx index 2345db7c8..ca24b93f0 100644 --- a/web/src/Pages/Dashboard/barChartStats.tsx +++ b/web/src/Pages/Dashboard/barChartStats.tsx @@ -1,21 +1,31 @@ import React, { FC } from 'react'; -import { Skeleton } from 'antd'; +import { Skeleton, Tooltip } from 'antd'; import Chart from 'react-apexcharts'; +import { InfoCircle } from 'react-bootstrap-icons'; import moment from 'moment'; export interface BarChartStatsProps { + id: string; title: string; options: any; series: any; - lastUpdate: number; + lastUpdate: any; loading: boolean; + toolTipText: string; } const BarChartsStat: FC = (props: BarChartStatsProps) => { - const { title, options, series, lastUpdate, loading } = props; + const { id, title, options, series, lastUpdate, loading, toolTipText } = props; return (
-
{title}
+
+
{title}
+
+ + + +
+
{loading ? (
@@ -24,10 +34,17 @@ const BarChartsStat: FC = (props: BarChartStatsProps) => { ) : ( <>
- +
-
{moment(lastUpdate).fromNow()}
+ {lastUpdate !== '0' &&
{lastUpdate}
}
)} diff --git a/web/src/Pages/Dashboard/dashboard.scss b/web/src/Pages/Dashboard/dashboard.scss index f2a66267c..17c6e480b 100644 --- a/web/src/Pages/Dashboard/dashboard.scss +++ b/web/src/Pages/Dashboard/dashboard.scss @@ -1,7 +1,28 @@ @import '../Common/colors.dashboard.scss'; +.mapboxgl-popup-content { + background-color: $white !important; + color: $card-title !important; +} + +.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip { + border-top-color: $white !important; +} + +.mapboxgl-popup-close-button, +.mapboxgl-popup-close-button:hover { + display: none; + // z-index: -1 !important; +} + +.total-container { + display: flex; + flex-direction: column; + border: 1px solid gray; +} + .margin-top-6 { - margin-top: 6rem; + margin-top: 7.5rem; } .margin-top-2 { @@ -12,6 +33,18 @@ margin-top: 1rem; } +.cards-title { + font-family: 'Inter'; + align-items: flex-start; + justify-content: flex-start; + font-style: normal; + font-weight: 600; + height: 50px; + font-size: 1.15rem; + margin-top: 1rem; + color: $card-title; +} + .dashboard-main-container { width: 100%; display: flex; @@ -87,15 +120,27 @@ } .height-pie-rem { - height: 25.75rem; + height: 26.2rem; + + .apexcharts-legend { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between !important; + } + + .apexcharts-legend-series { + display: flex !important; + width: 38%; + } } .height-bar-rem { - height: 30rem; + height: 29rem; } .height-map-rem { - height: 470px; + height: 510px; } .map-container { @@ -118,7 +163,7 @@ flex-direction: column; background-color: $white; border-radius: 10px; - padding: 0 1rem 1rem 1rem; + padding: 0 1rem 0.7rem 1rem; width: 100%; .margin-top-2 { @@ -126,12 +171,40 @@ margin-top: 2rem; } + .title-section { + display: flex; + flex-direction: row; + align-items: flex-end; + justify-content: space-between; + width: 100%; + padding: 1rem 1rem 0 1rem; + + .title { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + width: 90%; + font-size: 1.15rem; + color: $card-title; + } + + .info-container { + display: flex; + flex-direction: row; + width: 10%; + height: 30px; + align-items: center; + justify-content: flex-end; + } + } + .total-programme-details { display: flex; flex-direction: row; align-items: center; justify-content: space-between; height: 150px; + margin-top: -0.5rem; .details { display: flex; @@ -139,18 +212,6 @@ width: 70%; padding: 0 1rem 0 0.8rem; - .title { - font-family: 'Inter'; - align-items: flex-start; - justify-content: flex-start; - font-style: normal; - font-weight: 600; - height: 50px; - font-size: 1.15rem; - padding-left: 0.125rem; - color: $card-title; - } - .detail { font-family: 'Inter'; font-style: normal; @@ -193,6 +254,8 @@ justify-content: flex-start; height: 100px; width: 100%; + margin-top: -1rem; + margin-bottom: 0.5rem; padding: 0 0.5rem 0 0.5rem; .rejected-details, @@ -219,7 +282,7 @@ } .reject { - background-color: rgba(164, 162, 168, 0.5); + background-color: rgba(255, 166, 166, 0.4); } .transfer { @@ -227,7 +290,7 @@ } .pending { - background-color: rgba(255, 166, 166, 0.4); + background-color: rgba(164, 162, 168, 0.5); } .authorized { @@ -269,6 +332,14 @@ } } + .stage-legends { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + margin-top: 1rem; + } + .updated-on { display: flex; flex-direction: row; @@ -280,31 +351,54 @@ .updated-moment-container { display: flex; flex-direction: row; - font-size: 0.813rem; + font-size: 0.75rem; border-radius: 3.125rem; - padding: 0 0.5rem 0 0.5rem; + padding: 0.05rem 0.8rem 0.05rem 0.8rem; color: $primary-blue; background-color: rgba($color: #b9e2f4, $alpha: 0.4); } } - .pie-charts-title { - font-family: 'Inter'; - align-items: flex-start; - justify-content: flex-start; - font-style: normal; - font-weight: 600; - height: 50px; - font-size: 1.15rem; - margin-top: 1rem; - padding: 0 1rem 1rem 1rem; - color: $card-title; + .pie-charts-top { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 0 1rem 0 1rem; + .pie-charts-title { + display: flex; + flex-direction: row; + font-family: 'Inter'; + align-items: flex-start; + justify-content: flex-start; + font-style: normal; + font-weight: 600; + height: 50px; + font-size: 1.15rem; + margin-top: 1rem; + color: $card-title; + + .unit { + display: flex; + margin: 0 0 0 0.5rem; + font-family: 'Inter'; + font-size: 1.15rem; + font-weight: 500; + color: rgba(58, 53, 65, 0.5); + } + } + + .info-container { + display: flex; + flex-direction: row; + align-items: center; + } } .pie-charts-section { display: flex; align-items: center; justify-content: center; + width: 100%; } } } diff --git a/web/src/Pages/Dashboard/dashboard.tsx b/web/src/Pages/Dashboard/dashboard.tsx index 709cbe02a..52051fef2 100644 --- a/web/src/Pages/Dashboard/dashboard.tsx +++ b/web/src/Pages/Dashboard/dashboard.tsx @@ -1,26 +1,24 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Col, DatePicker, Progress, Radio, Row, Skeleton, message } from 'antd'; -import Chart from 'react-apexcharts'; -import ReactMapboxGl, { Layer, Feature } from 'react-mapbox-gl'; +import { Col, DatePicker, Radio, Row, Skeleton, Tooltip, message } from 'antd'; import 'mapbox-gl/dist/mapbox-gl.css'; -import MapCard from '../../Components/MapCards.tsx/MapCard'; import StasticCard from '../../Components/StasticCard/StasticCard'; import './dashboard.scss'; import { optionDonutPieA, optionDonutPieB, - seriesY, totalCreditsCertifiedOptions, totalCreditsOptions, totalProgrammesOptions, totalProgrammesOptionsSub, -} from './DUMMY_DATAS'; +} from './CHART_OPTIONS'; import ProgrammeRejectAndTransfer from './ProgrammeRejectAndTransfer'; import moment from 'moment'; import { useConnection } from '../../Context/ConnectionContext/connectionContext'; import mapboxgl from 'mapbox-gl'; -import Geocoding from '@mapbox/mapbox-sdk/services/geocoding'; -import { addCommSep } from '../../Definitions/InterfacesAndType/programme.definitions'; +import { + addCommSep, + addRoundNumber, +} from '../../Definitions/InterfacesAndType/programme.definitions'; import { ClockHistory, BoxArrowInRight, @@ -29,11 +27,23 @@ import { BoxArrowRight, ShieldCheck, Gem, - BoxArrowInLeft, + InfoCircle, } from 'react-bootstrap-icons'; import PieChartsStat from './pieChartStat'; import BarChartsStat from './barChartStats'; -import TransferLocationsMap from './transferLocations'; +import LegendItem from '../../Components/LegendItem/legendItem'; +import { + ChartSeriesItem, + totalCertifiedCreditsSeriesInitialValues, + totalCreditsSeriesInitialValues, + getTotalProgrammesInitialValues, + getTotalProgrammesSectorInitialValues, +} from './dashboardTypesInitialValues'; +import { Sector } from '../../Casl/enums/sector.enum'; +import { ProgrammeStage, ProgrammeStageLegend } from '../../Casl/enums/programme-status.enum'; +import { CompanyRole } from '../../Casl/enums/company.role.enum'; +import { useUserContext } from '../../Context/UserInformationContext/userInformationContext'; +import { useTranslation } from 'react-i18next'; const { RangePicker } = DatePicker; @@ -44,57 +54,56 @@ const Dashboard = () => { const { get, post, delete: del } = useConnection(); const mapContainerRef = useRef(null); const mapContainerInternationalRef = useRef(null); - const [loading, setLoading] = useState(false); - const [userDetails, setUserDetails] = useState([]); - const [companyRole, setCompanyRole] = useState(); + const { userInfoState } = useUserContext(); + const { i18n, t } = useTranslation(['dashboard']); const [loadingWithoutTimeRange, setLoadingWithoutTimeRange] = useState(false); + const [loading, setLoading] = useState(false); + const [loadingCharts, setLoadingCharts] = useState(false); const [totalProjects, setTotalProjects] = useState(0); - const [pendingProjects, setPendingProjects] = useState(0); const [pendingProjectsWithoutTimeRange, setPendingProjectsWithoutTimeRange] = useState(0); - const [issuedProjects, setIssuedProjects] = useState(0); + const [pendingProjects, setPendingProjects] = useState(0); const [rejectedProjects, setRejectedProjects] = useState(0); const [authorisedProjects, setAuthorisedProjects] = useState(0); - const [transfererequestsSent, setTransfererequestsSent] = useState(0); const [creditBalance, setCreditBalance] = useState(0); const [creditBalanceWithoutTimeRange, setCreditBalanceWithoutTimeRange] = useState(0); const [creditCertiedBalanceWithoutTimeRange, setCreditCertifiedBalanceWithoutTimeRange] = useState(0); const [creditsPieSeries, setCreditPieSeries] = useState([1, 1, 0, 0]); const [creditsCertifiedPieSeries, setCreditCertifiedPieSeries] = useState([1, 1, 0]); - const [lastUpdate, setLastUpdate] = useState(); - const [lastUpdateProgrammesStats, setLastUpdateProgrammesStats] = useState(); - const [estimatedCredits, setEstimatedCredits] = useState(); + const [creditsPieChartTotal, setCreditsPieChartTotal] = useState(0); + const [certifiedCreditsPieChartTotal, setCertifiedCreditsPieChartTotal] = useState(0); const [startTime, setStartTime] = useState(0); const [endTime, setEndTime] = useState(0); const [categoryType, setCategoryType] = useState('overall'); - const [authorisedProgrammes, setAuthorisedProgrammes] = useState([0, 0, 0, 0]); - const [rejectedProgrammes, setRejectedProgrammes] = useState([0, 0, 0, 0]); - const [pendingProgrammes, setPendingProgrammes] = useState([0, 0, 0, 0]); + // states for totalProgrammes chart + const [totalProgrammesSeries, setTotalProgrammesSeries] = useState( + getTotalProgrammesInitialValues() + ); + const [totalProgrammesOptionsLabels, setTotalProgrammesOptionsLabels] = useState([]); // states for totalProgrammes sub sector chart - const [energyProgrammes, setEnergyProgrammes] = useState([0, 0, 0, 0]); - const [healthProgrammes, setHealthProgrammes] = useState([0, 0, 0, 0]); - const [educationProgrammes, setEducationProgrammes] = useState([0, 0, 0, 0]); - const [transportProgrammes, setTransportProgrammes] = useState([0, 0, 0, 0]); - const [manufacturingProgrammes, setManufacturingProgrammes] = useState([0, 0, 0, 0]); - const [hospitalityProgrammes, setHospitalityProgrammes] = useState([0, 0, 0, 0]); - const [forestryProgrammes, setForestryProgrammes] = useState([0, 0, 0, 0]); - const [wasteProgrammes, setWasteProgrammes] = useState([0, 0, 0, 0]); - const [agricultureProgrammes, setAgricultureProgrammes] = useState([0, 0, 0, 0]); - const [otherProgrammes, setOtherProgrammes] = useState([0, 0, 0, 0]); + const [totalProgrammesSectorSeries, setTotalProgrammesSectorSeries] = useState( + getTotalProgrammesSectorInitialValues() + ); + const [totalProgrammesSectorOptionsLabels, setTotalProgrammesSectorOptionsLabels] = useState< + any[] + >([]); // states for totalCredits chart - const [authorizedCredits, setAuthorizedCredits] = useState([0, 0, 0, 0]); - const [issuedCredits, setIssuedCredits] = useState([0, 0, 0, 0]); - const [retiredCredits, setRetiredCredits] = useState([0, 0, 0, 0]); - const [transferredCredits, setTransferredCredits] = useState([0, 0, 0, 0]); + const [totalCreditsSeries, setTotalCreditsSeries] = useState( + totalCreditsSeriesInitialValues + ); + const [totalCreditsOptionsLabels, setTotalCreditsOptionsLabels] = useState([]); // states for totalCreditsCertified chart - const [certifiedCredits, setCertifiedCredits] = useState([0, 0, 0, 0]); - const [unCertifiedCredits, setUnCertifiedCredits] = useState([0, 0, 0, 0]); - const [revokedCredits, setRevokedCredits] = useState([0, 0, 0, 0]); + const [totalCertifiedCreditsSeries, setTotalCertifiedCreditsSeries] = useState( + totalCertifiedCreditsSeriesInitialValues + ); + const [totalCertifiedCreditsOptionsLabels, setTotalCertifiedCreditsOptionsLabels] = useState< + any[] + >([]); // locations of programmes const [programmeLocations, setProgrammeLocations] = useState(); @@ -103,30 +112,64 @@ const Dashboard = () => { //certifier view states const [programmesCertifed, setProgrammesCertifed] = useState(0); const [programmesUnCertifed, setProgrammesUnCertifed] = useState(0); - const [certifcationsRevoked, setCertifcationsRevoked] = useState(20); //programmeDeveloper const [transferRequestSent, setTransferRequestSent] = useState(0); const [transferRequestReceived, setTransferRequestReceived] = useState(0); - const currentYear = new Date(); - const startOfTheYear = Date.parse(String(moment(currentYear).startOf('year'))); - const endOfTheYear = Date.parse(String(moment(currentYear).endOf('year'))); - console.log({ currentYear, startOfTheYear, endOfTheYear }); + //last time updates + const [lastUpdateProgrammesStatsEpoch, setLastUpdateProgrammesStatsEpoch] = useState(0); + const [lastUpdateProgrammesStats, setLastUpdateProgrammesStats] = useState('0'); - const getUserProfileDetails = async () => { - try { - setLoading(true); - const response = await get('national/User/profile'); - if (response.data) { - setUserDetails(response.data.user); - setCompanyRole(response.data.user?.companyRole); - } - } catch (exception) { - } finally { - setLoading(false); - } - }; + const [lastUpdatePendingTransferSentEpoch, setLastUpdatePendingTransferSentEpoch] = + useState(0); + const [lastUpdatePendingTransferSent, setLastUpdatePendingTransferSent] = useState('0'); + + const [lastUpdateCreditBalanceEpoch, setLastUpdateCreditBalanceEpoch] = useState(0); + const [lastUpdateCreditBalance, setLastUpdateCreditBalance] = useState('0'); + + const [lastUpdatePendingTransferReceivedEpoch, setLastUpdatePendingTransferReceivedEpoch] = + useState(0); + const [lastUpdatePendingTransferReceived, setLastUpdatePendingTransferReceived] = + useState('0'); + + const [lastUpdateProgrammesCertifiableEpoch, setLastUpdateProgrammesCertifiableEpoch] = + useState(0); + const [lastUpdateProgrammesCertifiable, setLastUpdateProgrammesCertifiable] = + useState('0'); + + const [lastUpdateCertifiedCreditsStatsEpoch, setLastUpdateCertifiedCreditsStatsEpoch] = + useState(0); + const [lastUpdateCertifiedCreditsStats, setLastUpdateCertifiedCreditsStats] = + useState('0'); + + const [lastUpdateProgrammesCertifiedEpoch, setLastUpdateProgrammesCertifiedEpoch] = + useState(0); + const [lastUpdateProgrammesCertified, setLastUpdateProgrammesCertified] = useState('0'); + + const [lastUpdateProgrammesStatsCEpoch, setLastUpdateProgrammesStatsCEpoch] = useState(0); + const [lastUpdateProgrammesStatsC, setLastUpdateProgrammesStatsC] = useState('0'); + + const [lastUpdateProgrammesCreditsStatsEpoch, setLastUpdateProgrammesCreditsStatsEpoch] = + useState(0); + const [lastUpdateProgrammesCreditsStats, setLastUpdateProgrammesCreditsStats] = + useState('0'); + + const [lastUpdateProgrammesSectorStatsCEpoch, setLastUpdateProgrammesSectorStatsCEpoch] = + useState(0); + const [lastUpdateProgrammesSectorStatsC, setLastUpdateProgrammesSectorStatsC] = + useState('0'); + const [lastUpdateTotalCreditsEpoch, setLastUpdateTotalCreditsEpoch] = useState(0); + const [lastUpdateTotalCredits, setLastUpdateTotalCredits] = useState('0'); + + const [lastUpdateTotalCreditsCertifiedEpoch, setLastUpdateTotalCreditsCertifiedEpoch] = + useState(0); + const [lastUpdateTotalCreditsCertified, setLastUpdateTotalCreditsCertified] = + useState('0'); + + const [lastUpdateTransferLocationsEpoch, setLastUpdateTransferLocationsEpoch] = + useState(0); + const [lastUpdateTransferLocations, setLastUpdateTransferLocations] = useState('0'); const getAllProgrammeAnalyticsStatsParamsWithoutTimeRange = () => { return { @@ -144,15 +187,17 @@ const Dashboard = () => { type: 'PENDING_TRANSFER_RECV', }, { - type: 'CERTIFIED_REVOKED_BY_ME', + type: 'UNCERTIFIED_BY_ME', + }, + { + type: 'CERTIFIED_BY_ME', }, ], - category: 'overall', }; }; const getAllProgrammeAnalyticsStatsParams = () => { - if (companyRole === 'ProgrammeDeveloper' || categoryType === 'mine') { + if (userInfoState?.companyRole === CompanyRole.PROGRAMME_DEVELOPER) { return { stats: [ { @@ -162,6 +207,13 @@ const Dashboard = () => { endTime: endTime !== 0 ? endTime : undefined, }, }, + { + type: 'MY_AGG_AUTH_PROGRAMME_BY_STATUS', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, { type: 'MY_CERTIFIED_REVOKED_PROGRAMMES', statFilter: { @@ -171,6 +223,58 @@ const Dashboard = () => { }, ], }; + } else if (userInfoState?.companyRole === 'Certifier' && categoryType === 'mine') { + return { + stats: [ + { + type: 'CERTIFIED_BY_ME_BY_STATE', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, + { + type: 'AUTH_CERTIFIED_BY_ME_BY_STATE', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, + { + type: 'CERTIFIED_REVOKED_BY_ME', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, + ], + }; + } else if (userInfoState?.companyRole === 'Certifier' && categoryType === 'overall') { + return { + stats: [ + { + type: 'AGG_PROGRAMME_BY_STATUS', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, + { + type: 'AGG_AUTH_PROGRAMME_BY_STATUS', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, + { + type: 'CERTIFIED_REVOKED_PROGRAMMES', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, + ], + }; } else { return { stats: [ @@ -181,6 +285,13 @@ const Dashboard = () => { endTime: endTime !== 0 ? endTime : undefined, }, }, + { + type: 'AGG_AUTH_PROGRAMME_BY_STATUS', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, { type: 'CERTIFIED_REVOKED_PROGRAMMES', statFilter: { @@ -194,46 +305,179 @@ const Dashboard = () => { }; const getAllChartsParams = () => { - return { - stats: [ - { - type: 'AGG_PROGRAMME_BY_STATUS', - statFilter: { - startTime: startTime !== 0 ? startTime : undefined, - endTime: endTime !== 0 ? endTime : undefined, - timeGroup: true, + if (userInfoState?.companyRole === CompanyRole.PROGRAMME_DEVELOPER) { + return { + stats: [ + { + type: 'MY_AGG_PROGRAMME_BY_STATUS', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + timeGroup: true, + }, }, - }, - ], - }; - }; - - const getAllProgrammeAnalyticsStatsChartsParams = () => { - return { - stats: [ - { - type: 'TOTAL_PROGRAMS', - }, - { - type: 'TOTAL_PROGRAMS_SECTOR', - }, - { - type: 'TOTAL_CREDITS', - }, - { - type: 'TOTAL_CREDITS_CERTIFIED', - }, - { - type: 'PROGRAMME_LOCATIONS', - }, - { - type: 'TRANSFER_LOCATIONS', - }, - ], - category: companyRole === 'ProgrammeDeveloper' ? 'mine' : categoryType, - startTime: startTime !== 0 ? startTime : startOfTheYear, - endTime: endTime !== 0 ? endTime : endOfTheYear, - }; + { + type: 'MY_AGG_PROGRAMME_BY_SECTOR', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + timeGroup: true, + }, + }, + { + type: 'MY_CERTIFIED_REVOKED_PROGRAMMES', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + timeGroup: true, + }, + }, + { + type: 'MY_TRANSFER_LOCATION', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, + { + type: 'MY_PROGRAMME_LOCATION', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, + ], + }; + } else if (userInfoState?.companyRole === 'Certifier' && categoryType === 'mine') { + return { + stats: [ + { + type: 'CERTIFIED_BY_ME_BY_STATE', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + timeGroup: true, + }, + }, + { + type: 'CERTIFIED_BY_ME_BY_SECTOR', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + timeGroup: true, + }, + }, + { + type: 'CERTIFIED_REVOKED_BY_ME', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + timeGroup: true, + }, + }, + { + type: 'MY_TRANSFER_LOCATION', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, + { + type: 'MY_PROGRAMME_LOCATION', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, + ], + }; + } else if (userInfoState?.companyRole === 'Certifier' && categoryType === 'overall') { + return { + stats: [ + { + type: 'AGG_PROGRAMME_BY_STATUS', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + timeGroup: true, + }, + }, + { + type: 'AGG_PROGRAMME_BY_SECTOR', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + timeGroup: true, + }, + }, + { + type: 'CERTIFIED_REVOKED_PROGRAMMES', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + timeGroup: true, + }, + }, + { + type: 'ALL_TRANSFER_LOCATION', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, + { + type: 'ALL_PROGRAMME_LOCATION', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, + ], + }; + } else { + return { + stats: [ + { + type: 'AGG_PROGRAMME_BY_STATUS', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + timeGroup: true, + }, + }, + { + type: 'AGG_PROGRAMME_BY_SECTOR', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + timeGroup: true, + }, + }, + { + type: 'CERTIFIED_REVOKED_PROGRAMMES', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + timeGroup: true, + }, + }, + { + type: 'ALL_TRANSFER_LOCATION', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, + { + type: 'ALL_PROGRAMME_LOCATION', + statFilter: { + startTime: startTime !== 0 ? startTime : undefined, + endTime: endTime !== 0 ? endTime : undefined, + }, + }, + ], + }; + } }; const onChangeRange = async (dateMoment: any, dateString: any) => { @@ -255,235 +499,335 @@ const Dashboard = () => { } }; - const getAllProgrammeAnalyticsStatsCharts = async () => { - setLoading(true); - try { - const pendingProgrames: any = []; - const authorisedProgrames: any = []; - const rejectedProgrames: any = []; - const timeLabelsProgrames: any = []; - - const energyProgrames: any = []; - const healthProgrames: any = []; - const educationProgrames: any = []; - const transportProgrames: any = []; - const manufacturingProgrames: any = []; - const hospitalityProgrames: any = []; - const forestryProgrames: any = []; - const wasteProgrames: any = []; - const agricultureProgrames: any = []; - const otherProgrames: any = []; - - const authorizedCredit: any = []; - const issuedCredit: any = []; - const retiredCredit: any = []; - const transferredCredit: any = []; - - const certifiedCredit: any = []; - const unCertifiedCredit: any = []; - const revokedCredit: any = []; + const firstLower = (lower: any) => { + return (lower && lower[0].toLowerCase() + lower.slice(1)) || lower; + }; - const response: any = await post( - 'stats/programme/dashboardCharts', - getAllProgrammeAnalyticsStatsChartsParams() - ); - console.log(response?.data?.stats); - if (response?.data?.stats?.TOTAL_PROGRAMS) { - const totalProgrammes = response?.data?.stats?.TOTAL_PROGRAMS; - if (totalProgrammes?.awaitingAuthorization) { - const pendings = totalProgrammes?.awaitingAuthorization; - pendings?.map((item: any, index: any) => { - const programesCount = Object.values(item); - const label = Object.getOwnPropertyNames(item); - const date = new Date(parseInt(label[0])); - const formattedDate = moment(date).format('DD-MM-YYYY'); - pendingProgrames.push(programesCount[0]); - timeLabelsProgrames.push(formattedDate); - }); + const getAllProgrammesAggChartStats = async () => { + setLoadingCharts(true); + try { + const response: any = await post('stats/programme/agg', getAllChartsParams()); + let programmesAggByStatus: any; + let programmesAggBySector: any; + let totalCreditsCertifiedStats: any; + let programmeLocationsStats: any; + let transferLocationsStats: any; + if (userInfoState?.companyRole === CompanyRole.PROGRAMME_DEVELOPER) { + if ( + response?.data?.stats?.MY_AGG_PROGRAMME_BY_STATUS?.all?.creditUpdateTime && + String(response?.data?.stats?.MY_AGG_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) !== '0' + ) { + setLastUpdateTotalCreditsEpoch( + parseInt(response?.data?.stats?.MY_AGG_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) + ); + setLastUpdateTotalCredits( + moment( + parseInt(response?.data?.stats?.MY_AGG_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) + ).fromNow() + ); } - if (totalProgrammes?.authorised) { - const authorised = totalProgrammes?.authorised; - authorised?.map((item: any, index: any) => { - const programesCount = Object.values(item); - authorisedProgrames.push(programesCount[0]); - }); + programmesAggByStatus = response?.data?.stats?.MY_AGG_PROGRAMME_BY_STATUS?.data; + if ( + response?.data?.stats?.MY_AGG_PROGRAMME_BY_SECTOR?.all?.statusUpdateTime && + String(response?.data?.stats?.MY_AGG_PROGRAMME_BY_SECTOR?.all?.statusUpdateTime) !== '0' + ) { + setLastUpdateProgrammesSectorStatsCEpoch( + parseInt(response?.data?.stats?.MY_AGG_PROGRAMME_BY_SECTOR?.all?.statusUpdateTime) + ); + setLastUpdateProgrammesSectorStatsC( + moment( + parseInt(response?.data?.stats?.MY_AGG_PROGRAMME_BY_SECTOR?.all?.statusUpdateTime) + ).fromNow() + ); } - if (totalProgrammes?.rejected) { - const rejected = totalProgrammes?.rejected; - rejected?.map((item: any, index: any) => { - const programesCount = Object.values(item); - rejectedProgrames.push(programesCount[0]); - }); + programmesAggBySector = response?.data?.stats?.MY_AGG_PROGRAMME_BY_SECTOR?.data; + if ( + response?.data?.stats?.MY_CERTIFIED_REVOKED_PROGRAMMES?.last && + String(response?.data?.stats?.MY_CERTIFIED_REVOKED_PROGRAMMES?.last) !== '0' + ) { + setLastUpdateTotalCreditsCertifiedEpoch( + parseInt(response?.data?.stats?.MY_CERTIFIED_REVOKED_PROGRAMMES?.last) + ); + setLastUpdateTotalCreditsCertified( + moment(parseInt(response?.data?.stats?.MY_CERTIFIED_REVOKED_PROGRAMMES?.last)).fromNow() + ); } - } - if (response?.data?.stats?.TOTAL_PROGRAMS_SECTOR) { - const totalProgrammesSector = response?.data?.stats?.TOTAL_PROGRAMS_SECTOR; - if (totalProgrammesSector?.agriculture) { - const agriculture = totalProgrammesSector?.agriculture; - agriculture?.map((item: any, index: any) => { - const programesCount = Object.values(item); - agricultureProgrames.push(programesCount[0]); - }); + totalCreditsCertifiedStats = response?.data?.stats?.MY_CERTIFIED_REVOKED_PROGRAMMES?.data; + if ( + response?.data?.stats?.MY_TRANSFER_LOCATION?.last && + String(response?.data?.stats?.MY_TRANSFER_LOCATION?.last) !== '0' + ) { + setLastUpdateTransferLocationsEpoch( + parseInt(response?.data?.stats?.MY_TRANSFER_LOCATION?.last) + ); + setLastUpdateTransferLocations( + moment(parseInt(response?.data?.stats?.MY_TRANSFER_LOCATION?.last)).fromNow() + ); } - if (totalProgrammesSector?.education) { - const education = totalProgrammesSector?.education; - education?.map((item: any, index: any) => { - const programesCount = Object.values(item); - educationProgrames.push(programesCount[0]); - }); + transferLocationsStats = response?.data?.stats?.MY_TRANSFER_LOCATION?.data; + programmeLocationsStats = response?.data?.stats?.MY_PROGRAMME_LOCATION; + } else if (userInfoState?.companyRole === CompanyRole.CERTIFIER && categoryType === 'mine') { + if ( + response?.data?.stats?.CERTIFIED_BY_ME_BY_STATE?.all?.creditUpdateTime && + String(response?.data?.stats?.CERTIFIED_BY_ME_BY_STATE?.all?.creditUpdateTime) !== '0' + ) { + setLastUpdateTotalCreditsEpoch( + parseInt(response?.data?.stats?.CERTIFIED_BY_ME_BY_STATE?.all?.creditUpdateTime) + ); + setLastUpdateTotalCredits( + moment( + parseInt(response?.data?.stats?.CERTIFIED_BY_ME_BY_STATE?.all?.creditUpdateTime) + ).fromNow() + ); } - if (totalProgrammesSector?.energy) { - const energy = totalProgrammesSector?.energy; - energy?.map((item: any, index: any) => { - const programesCount = Object.values(item); - energyProgrames.push(programesCount[0]); - }); + programmesAggByStatus = response?.data?.stats?.CERTIFIED_BY_ME_BY_STATE?.data; + if ( + response?.data?.stats?.CERTIFIED_BY_ME_BY_SECTOR?.all?.statusUpdateTime && + String(response?.data?.stats?.CERTIFIED_BY_ME_BY_SECTOR?.all?.statusUpdateTime) !== '0' + ) { + setLastUpdateProgrammesSectorStatsCEpoch( + parseInt(response?.data?.stats?.CERTIFIED_BY_ME_BY_SECTOR?.all?.statusUpdateTime) + ); + setLastUpdateProgrammesSectorStatsC( + moment( + parseInt(response?.data?.stats?.CERTIFIED_BY_ME_BY_SECTOR?.all?.statusUpdateTime) + ).fromNow() + ); } - if (totalProgrammesSector?.forestry) { - const forestry = totalProgrammesSector?.forestry; - forestry?.map((item: any, index: any) => { - const programesCount = Object.values(item); - forestryProgrames.push(programesCount[0]); - }); + programmesAggBySector = response?.data?.stats?.CERTIFIED_BY_ME_BY_SECTOR?.data; + if ( + response?.data?.stats?.CERTIFIED_REVOKED_BY_ME?.last && + String(response?.data?.stats?.CERTIFIED_REVOKED_BY_ME?.last) !== '0' + ) { + setLastUpdateTotalCreditsCertifiedEpoch( + parseInt(response?.data?.stats?.CERTIFIED_REVOKED_BY_ME?.last) + ); + setLastUpdateTotalCreditsCertified( + moment(parseInt(response?.data?.stats?.CERTIFIED_REVOKED_BY_ME?.last)).fromNow() + ); } - if (totalProgrammesSector?.health) { - const health = totalProgrammesSector?.health; - health?.map((item: any, index: any) => { - const programesCount = Object.values(item); - healthProgrames.push(programesCount[0]); - }); + totalCreditsCertifiedStats = response?.data?.stats?.CERTIFIED_REVOKED_BY_ME?.data; + if ( + response?.data?.stats?.MY_TRANSFER_LOCATION?.last && + String(response?.data?.stats?.MY_TRANSFER_LOCATION?.last) !== '0' + ) { + setLastUpdateTransferLocationsEpoch( + parseInt(response?.data?.stats?.MY_TRANSFER_LOCATION?.last) + ); + setLastUpdateTransferLocations( + moment(parseInt(response?.data?.stats?.MY_TRANSFER_LOCATION?.last)).fromNow() + ); } - if (totalProgrammesSector?.hospitality) { - const hospitality = totalProgrammesSector?.hospitality; - hospitality?.map((item: any, index: any) => { - const programesCount = Object.values(item); - hospitalityProgrames.push(programesCount[0]); - }); + transferLocationsStats = response?.data?.stats?.MY_TRANSFER_LOCATION?.data; + programmeLocationsStats = response?.data?.stats?.MY_PROGRAMME_LOCATION; + } else if ( + userInfoState?.companyRole === CompanyRole.CERTIFIER && + categoryType === 'overall' + ) { + if ( + response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.creditUpdateTime && + String(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) !== '0' + ) { + setLastUpdateTotalCreditsEpoch( + parseInt(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) + ); + setLastUpdateTotalCredits( + moment( + parseInt(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) + ).fromNow() + ); } - if (totalProgrammesSector?.manufacturing) { - const manufacturing = totalProgrammesSector?.manufacturing; - manufacturing?.map((item: any, index: any) => { - const programesCount = Object.values(item); - manufacturingProgrames.push(programesCount[0]); - }); + programmesAggByStatus = response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.data; + if ( + response?.data?.stats?.AGG_PROGRAMME_BY_SECTOR?.all?.statusUpdateTime && + String(response?.data?.stats?.AGG_PROGRAMME_BY_SECTOR?.all?.statusUpdateTime) !== '0' + ) { + setLastUpdateProgrammesSectorStatsCEpoch( + parseInt(response?.data?.stats?.AGG_PROGRAMME_BY_SECTOR?.all?.statusUpdateTime) + ); + setLastUpdateProgrammesSectorStatsC( + moment( + parseInt(response?.data?.stats?.AGG_PROGRAMME_BY_SECTOR?.all?.statusUpdateTime) + ).fromNow() + ); } - if (totalProgrammesSector?.other) { - const other = totalProgrammesSector?.other; - other?.map((item: any, index: any) => { - const programesCount = Object.values(item); - otherProgrames.push(programesCount[0]); - }); + programmesAggBySector = response?.data?.stats?.AGG_PROGRAMME_BY_SECTOR?.data; + if ( + response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last && + String(response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last) !== '0' + ) { + setLastUpdateTotalCreditsCertifiedEpoch( + parseInt(response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last) + ); + setLastUpdateTotalCreditsCertified( + moment(parseInt(response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last)).fromNow() + ); } - if (totalProgrammesSector?.transport) { - const transport = totalProgrammesSector?.transport; - transport?.map((item: any, index: any) => { - const programesCount = Object.values(item); - transportProgrames.push(programesCount[0]); - }); + totalCreditsCertifiedStats = response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.data; + if ( + response?.data?.stats?.ALL_TRANSFER_LOCATION?.last && + String(response?.data?.stats?.ALL_TRANSFER_LOCATION?.last) !== '0' + ) { + setLastUpdateTransferLocationsEpoch( + parseInt(response?.data?.stats?.ALL_TRANSFER_LOCATION?.last) + ); + setLastUpdateTransferLocations( + moment(parseInt(response?.data?.stats?.ALL_TRANSFER_LOCATION?.last)).fromNow() + ); } - if (totalProgrammesSector?.waste) { - const waste = totalProgrammesSector?.waste; - waste?.map((item: any, index: any) => { - const programesCount = Object.values(item); - wasteProgrames.push(programesCount[0]); - }); + transferLocationsStats = response?.data?.stats?.ALL_TRANSFER_LOCATION?.data; + programmeLocationsStats = response?.data?.stats?.ALL_PROGRAMME_LOCATION; + } else { + if ( + response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.creditUpdateTime && + String(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) !== '0' + ) { + setLastUpdateTotalCreditsEpoch( + parseInt(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) + ); + setLastUpdateTotalCredits( + moment( + parseInt(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) + ).fromNow() + ); } - } - if (response?.data?.stats?.TOTAL_CREDITS) { - const totalCredits = response?.data?.stats?.TOTAL_CREDITS; - if (totalCredits?.authorized) { - const authorized = totalCredits?.authorized; - authorized?.map((item: any, index: any) => { - const credit = Object.values(item); - authorizedCredit.push(credit[0]); - }); + programmesAggByStatus = response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.data; + if ( + response?.data?.stats?.AGG_PROGRAMME_BY_SECTOR?.all?.statusUpdateTime && + String(response?.data?.stats?.AGG_PROGRAMME_BY_SECTOR?.all?.statusUpdateTime) !== '0' + ) { + setLastUpdateProgrammesSectorStatsCEpoch( + parseInt(response?.data?.stats?.AGG_PROGRAMME_BY_SECTOR?.all?.statusUpdateTime) + ); + setLastUpdateProgrammesSectorStatsC( + moment( + parseInt(response?.data?.stats?.AGG_PROGRAMME_BY_SECTOR?.all?.statusUpdateTime) + ).fromNow() + ); } - if (totalCredits?.issued) { - const issued = totalCredits?.issued; - issued?.map((item: any, index: any) => { - const credit = Object.values(item); - issuedCredit.push(credit[0]); - }); + programmesAggBySector = response?.data?.stats?.AGG_PROGRAMME_BY_SECTOR?.data; + if ( + response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last && + String(response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last) !== '0' + ) { + setLastUpdateTotalCreditsCertifiedEpoch( + parseInt(response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last) + ); + setLastUpdateTotalCreditsCertified( + moment(parseInt(response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last)).fromNow() + ); } - if (totalCredits?.retired) { - const retired = totalCredits?.retired; - retired?.map((item: any, index: any) => { - const credit = Object.values(item); - retiredCredit.push(credit[0]); - }); + totalCreditsCertifiedStats = response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.data; + if ( + response?.data?.stats?.ALL_TRANSFER_LOCATION?.last && + String(response?.data?.stats?.ALL_TRANSFER_LOCATION?.last) !== '0' + ) { + setLastUpdateTransferLocationsEpoch( + parseInt(response?.data?.stats?.ALL_TRANSFER_LOCATION?.last) + ); + setLastUpdateTransferLocations( + moment(parseInt(response?.data?.stats?.ALL_TRANSFER_LOCATION?.last)).fromNow() + ); } - if (totalCredits?.transferred) { - const transferred = totalCredits?.transferred; - transferred?.map((item: any, index: any) => { - const credit = Object.values(item); - transferredCredit.push(credit[0]); + transferLocationsStats = response?.data?.stats?.ALL_TRANSFER_LOCATION?.data; + programmeLocationsStats = response?.data?.stats?.ALL_PROGRAMME_LOCATION; + } + let timeLabelDataStatus = []; + let formattedTimeLabelDataStatus: any = []; + let timeLabelDataSector = []; + let formattedTimeLabelDataSector: any = []; + let timeLabelCertifiedCreditsStats = []; + let formattedTimeLabelCertifiedCreditsStats: any = []; + if (programmesAggByStatus) { + timeLabelDataStatus = programmesAggByStatus?.timeLabel; + formattedTimeLabelDataStatus = timeLabelDataStatus?.map((item: any) => { + return moment(new Date(item.substr(0, 16))).format('DD-MM-YYYY'); + }); + setTotalProgrammesOptionsLabels(formattedTimeLabelDataStatus); + setTotalCreditsOptionsLabels(formattedTimeLabelDataStatus); + const statusArray = Object.values(ProgrammeStageLegend); + const totalProgrammesValues: ChartSeriesItem[] = []; + statusArray?.map((status: any) => { + totalProgrammesValues.push({ + name: status === 'AwaitingAuthorization' ? 'Pending' : status, + data: programmesAggByStatus[firstLower(status)], }); - } + }); + setTotalProgrammesSeries(totalProgrammesValues); + totalProgrammesOptions.xaxis.categories = formattedTimeLabelDataStatus; + + const totalCreditsValues: ChartSeriesItem[] = [ + { + name: 'Authorised', + data: programmesAggByStatus?.authorisedCredits, + }, + { + name: 'Issued', + data: programmesAggByStatus?.issuedCredits, + }, + { + name: 'Transferred', + data: programmesAggByStatus?.transferredCredits, + }, + { + name: 'Retired', + data: programmesAggByStatus?.retiredCredits, + }, + ]; + setTotalCreditsSeries(totalCreditsValues); + totalCreditsOptions.xaxis.categories = formattedTimeLabelDataStatus; } - if (response?.data?.stats?.PROGRAMME_LOCATIONS) { - const locations = response?.data?.stats?.PROGRAMME_LOCATIONS; - setProgrammeLocations(locations); + if (programmesAggBySector) { + timeLabelDataSector = programmesAggByStatus?.timeLabel; + formattedTimeLabelDataSector = timeLabelDataSector?.map((item: any) => { + return moment(new Date(item.substr(0, 16))).format('DD-MM-YYYY'); + }); + setTotalProgrammesSectorOptionsLabels(formattedTimeLabelDataSector); + const progarmmesSectorSeriesData: ChartSeriesItem[] = []; + const sectorsArray = Object.values(Sector); + sectorsArray?.map((sector: any) => { + if (programmesAggBySector[firstLower(sector)] !== undefined) { + progarmmesSectorSeriesData.push({ + name: sector, + data: programmesAggBySector[firstLower(sector)], + }); + } + }); + setTotalProgrammesSectorSeries(progarmmesSectorSeriesData); + totalProgrammesOptionsSub.xaxis.categories = formattedTimeLabelDataSector; } - if (response?.data?.stats?.TRANSFER_LOCATIONS) { - const locations = response?.data?.stats?.TRANSFER_LOCATIONS; - setProgrammeTransferLocations(locations); + if (totalCreditsCertifiedStats) { + timeLabelCertifiedCreditsStats = totalCreditsCertifiedStats?.timeLabel; + formattedTimeLabelCertifiedCreditsStats = timeLabelCertifiedCreditsStats?.map( + (item: any) => { + return moment(new Date(item.substr(0, 16))).format('DD-MM-YYYY'); + } + ); + const totalCertifiedCreditsSeriesValues = [ + { + name: 'Certified', + data: totalCreditsCertifiedStats?.certifiedSum, + }, + { + name: 'Uncertified', + data: totalCreditsCertifiedStats?.uncertifiedSum, + }, + { + name: 'Revoked', + data: totalCreditsCertifiedStats?.revokedSum, + }, + ]; + setTotalCertifiedCreditsSeries(totalCertifiedCreditsSeriesValues); + setTotalCertifiedCreditsOptionsLabels(formattedTimeLabelCertifiedCreditsStats); + + totalCreditsCertifiedOptions.xaxis.categories = formattedTimeLabelCertifiedCreditsStats; } - if (response?.data?.stats?.TOTAL_CREDITS_CERTIFIED) { - const totalCredits = response?.data?.stats?.TOTAL_CREDITS_CERTIFIED; - if (totalCredits?.certified) { - const certified = totalCredits?.certified; - certified?.map((item: any, index: any) => { - const credit = Object.values(item); - certifiedCredit.push(credit[0]); - }); - } - if (totalCredits?.uncertified) { - const uncertified = totalCredits?.uncertified; - uncertified?.map((item: any, index: any) => { - const credit = Object.values(item); - unCertifiedCredit.push(credit[0]); - }); - } - if (totalCredits?.revoked) { - const revoked = totalCredits?.revoked; - revoked?.map((item: any, index: any) => { - const credit = Object.values(item); - revokedCredit.push(credit[0]); - }); - } + if (transferLocationsStats) { + setProgrammeTransferLocations(transferLocationsStats); + } + if (programmeLocationsStats) { + setProgrammeLocations(programmeLocationsStats); } - console.log({ - pendingProgrames, - authorisedProgrames, - rejectedProgrames, - timeLabelsProgrames, - }); - setPendingProgrammes(pendingProgrames); - setAuthorisedProgrammes(authorisedProgrames); - setRejectedProgrammes(rejectedProgrames); - - setEnergyProgrammes(energyProgrames); - setHealthProgrammes(healthProgrames); - setEducationProgrammes(educationProgrames); - setTransportProgrammes(transportProgrames); - setManufacturingProgrammes(manufacturingProgrames); - setHospitalityProgrammes(hospitalityProgrames); - setForestryProgrammes(forestryProgrames); - setWasteProgrammes(wasteProgrames); - setAgricultureProgrammes(agricultureProgrames); - setOtherProgrammes(otherProgrames); - - setAuthorizedCredits(authorizedCredit); - setIssuedCredits(issuedCredit); - setRetiredCredits(retiredCredit); - setTransferredCredits(transferredCredit); - setCertifiedCredits(certifiedCredit); - setUnCertifiedCredits(unCertifiedCredit); - setRevokedCredits(revokedCredit); - totalProgrammesOptions.xaxis.categories = timeLabelsProgrames; - totalProgrammesOptionsSub.xaxis.categories = timeLabelsProgrames; - totalCreditsOptions.xaxis.categories = timeLabelsProgrames; - totalCreditsCertifiedOptions.xaxis.categories = timeLabelsProgrames; } catch (error: any) { console.log('Error in getting users', error); message.open({ @@ -493,7 +837,7 @@ const Dashboard = () => { style: { textAlign: 'right', marginRight: 15, marginTop: 10 }, }); } finally { - setLoading(false); + setLoadingCharts(false); } }; @@ -504,7 +848,6 @@ const Dashboard = () => { 'stats/programme/agg', getAllProgrammeAnalyticsStatsParamsWithoutTimeRange() ); - console.log('stats data -- > ', response?.data); const programmeByStatusAggregationResponse = response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.data; const pendingTransferInitAggregationResponse = @@ -512,17 +855,13 @@ const Dashboard = () => { const pendingTransferReceivedAggregationResponse = response?.data?.stats?.PENDING_TRANSFER_RECV?.data; const myCreditAggregationResponse = response?.data?.stats?.MY_CREDIT?.data; - const certifiedByMeAggregationResponse = response?.data?.stats?.CERTIFIED_REVOKED_BY_ME?.data; - const programmeByStatusAggregationResponseLastUpdate = - response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.last; + const certifiedByMeAggregationResponse = response?.data?.stats?.CERTIFIED_BY_ME?.data[0]; + const unCertifiedByMeAggregationResponse = response?.data?.stats?.UNCERTIFIED_BY_ME?.data; programmeByStatusAggregationResponse?.map((responseItem: any, index: any) => { if (responseItem?.currentStage === 'AwaitingAuthorization') { setPendingProjectsWithoutTimeRange(parseInt(responseItem?.count)); } }); - if (programmeByStatusAggregationResponseLastUpdate) { - setLastUpdateProgrammesStats(programmeByStatusAggregationResponseLastUpdate); - } if (pendingTransferInitAggregationResponse) { setTransferRequestSent(parseInt(pendingTransferInitAggregationResponse[0]?.count)); } @@ -533,12 +872,80 @@ const Dashboard = () => { setTransferRequestReceived(parseInt(pendingTransferReceivedAggregationResponse[0]?.count)); } if (certifiedByMeAggregationResponse) { - setProgrammesCertifed(parseInt(certifiedByMeAggregationResponse?.certifiedCount)); - setProgrammesUnCertifed(parseInt(certifiedByMeAggregationResponse?.uncertifiedCount)); + setProgrammesCertifed(parseInt(certifiedByMeAggregationResponse?.count)); setCreditCertifiedBalanceWithoutTimeRange( certifiedByMeAggregationResponse?.certifiedSum === null ? 0 - : parseFloat(certifiedByMeAggregationResponse?.certifiedSum) + : parseFloat(certifiedByMeAggregationResponse?.sum) + ); + } + if (unCertifiedByMeAggregationResponse) { + setProgrammesUnCertifed(parseInt(unCertifiedByMeAggregationResponse?.uncertifiedCount)); + } + if ( + response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime && + String(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime) !== '0' + ) { + setLastUpdateProgrammesStatsEpoch( + parseInt(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime) + ); + setLastUpdateProgrammesStats( + moment( + parseInt(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime) + ).fromNow() + ); + } + if ( + response?.data?.stats?.PENDING_TRANSFER_INIT?.all?.txTime && + String(response?.data?.stats?.PENDING_TRANSFER_INIT?.all?.txTime) !== '0' + ) { + setLastUpdatePendingTransferSentEpoch( + parseInt(response?.data?.stats?.PENDING_TRANSFER_INIT?.all?.txTime) + ); + setLastUpdatePendingTransferSent( + moment(parseInt(response?.data?.stats?.PENDING_TRANSFER_INIT?.all?.txTime)).fromNow() + ); + } + if ( + response?.data?.stats?.MY_CREDIT?.last && + String(response?.data?.stats?.MY_CREDIT?.last) !== '0' + ) { + setLastUpdateCreditBalanceEpoch(parseInt(response?.data?.stats?.MY_CREDIT?.last)); + setLastUpdateCreditBalance( + moment(parseInt(response?.data?.stats?.MY_CREDIT?.last)).fromNow() + ); + } + if ( + response?.data?.stats?.UNCERTIFIED_BY_ME?.last && + String(response?.data?.stats?.UNCERTIFIED_BY_ME?.last) !== '0' + ) { + setLastUpdateProgrammesCertifiableEpoch( + parseInt(response?.data?.stats?.UNCERTIFIED_BY_ME?.last) + ); + setLastUpdateProgrammesCertifiable( + moment(parseInt(response?.data?.stats?.UNCERTIFIED_BY_ME?.last)).fromNow() + ); + } + if ( + response?.data?.stats?.CERTIFIED_BY_ME?.last && + String(response?.data?.stats?.CERTIFIED_BY_ME?.last) !== '0' + ) { + setLastUpdateProgrammesCertifiedEpoch( + parseInt(response?.data?.stats?.CERTIFIED_BY_ME?.last) + ); + setLastUpdateProgrammesCertified( + moment(parseInt(response?.data?.stats?.CERTIFIED_BY_ME?.last)).fromNow() + ); + } + if ( + response?.data?.stats?.PENDING_TRANSFER_RECV?.last && + String(response?.data?.stats?.PENDING_TRANSFER_RECV?.last) !== '0' + ) { + setLastUpdatePendingTransferReceivedEpoch( + parseInt(response?.data?.stats?.PENDING_TRANSFER_RECV?.last) + ); + setLastUpdatePendingTransferReceived( + moment(parseInt(response?.data?.stats?.PENDING_TRANSFER_RECV?.last)).fromNow() ); } } catch (error: any) { @@ -563,69 +970,240 @@ const Dashboard = () => { 'stats/programme/agg', getAllProgrammeAnalyticsStatsParams() ); - console.log('stats data 2nd -- > ', response?.data); let programmeByStatusAggregationResponse: any; + let programmeByStatusAuthAggregationResponse: any; let certifiedRevokedAggregationResponse: any; - if (companyRole === 'ProgrammeDeveloper' || categoryType === 'mine') { + if (userInfoState?.companyRole === CompanyRole.PROGRAMME_DEVELOPER) { + if ( + response?.data?.stats?.MY_AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime && + String(response?.data?.stats?.MY_AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime) !== '0' + ) { + setLastUpdateProgrammesStatsCEpoch( + parseInt(response?.data?.stats?.MY_AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime) + ); + setLastUpdateProgrammesStatsC( + moment( + parseInt(response?.data?.stats?.MY_AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime) + ).fromNow() + ); + } programmeByStatusAggregationResponse = response?.data?.stats?.MY_AGG_PROGRAMME_BY_STATUS?.data; + if ( + response?.data?.stats?.MY_AGG_AUTH_PROGRAMME_BY_STATUS?.all?.creditUpdateTime && + String(response?.data?.stats?.MY_AGG_AUTH_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) !== + '0' + ) { + setLastUpdateProgrammesCreditsStatsEpoch( + parseInt(response?.data?.stats?.MY_AGG_AUTH_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) + ); + setLastUpdateProgrammesCreditsStats( + moment( + parseInt( + response?.data?.stats?.MY_AGG_AUTH_PROGRAMME_BY_STATUS?.all?.creditUpdateTime + ) + ).fromNow() + ); + } + programmeByStatusAuthAggregationResponse = + response?.data?.stats?.MY_AGG_AUTH_PROGRAMME_BY_STATUS?.data; + if ( + response?.data?.stats?.MY_CERTIFIED_REVOKED_PROGRAMMES?.last && + String(response?.data?.stats?.MY_CERTIFIED_REVOKED_PROGRAMMES?.last) !== '0' + ) { + setLastUpdateCertifiedCreditsStatsEpoch( + parseInt(response?.data?.stats?.MY_CERTIFIED_REVOKED_PROGRAMMES?.last) + ); + setLastUpdateCertifiedCreditsStats( + moment(parseInt(response?.data?.stats?.MY_CERTIFIED_REVOKED_PROGRAMMES?.last)).fromNow() + ); + } certifiedRevokedAggregationResponse = response?.data?.stats?.MY_CERTIFIED_REVOKED_PROGRAMMES?.data; + } else if (userInfoState?.companyRole === CompanyRole.CERTIFIER && categoryType === 'mine') { + if ( + response?.data?.stats?.CERTIFIED_BY_ME_BY_STATE?.all?.statusUpdateTime && + String(response?.data?.stats?.CERTIFIED_BY_ME_BY_STATE?.all?.statusUpdateTime) !== '0' + ) { + setLastUpdateProgrammesStatsCEpoch( + parseInt(response?.data?.stats?.CERTIFIED_BY_ME_BY_STATE?.all?.statusUpdateTime) + ); + setLastUpdateProgrammesStatsC( + moment( + parseInt(response?.data?.stats?.CERTIFIED_BY_ME_BY_STATE?.all?.statusUpdateTime) + ).fromNow() + ); + } + programmeByStatusAggregationResponse = + response?.data?.stats?.CERTIFIED_BY_ME_BY_STATE?.data; + if ( + response?.data?.stats?.AUTH_CERTIFIED_BY_ME_BY_STATE?.all?.creditUpdateTime && + String(response?.data?.stats?.AUTH_CERTIFIED_BY_ME_BY_STATE?.all?.creditUpdateTime) !== + '0' + ) { + setLastUpdateProgrammesCreditsStatsEpoch( + parseInt(response?.data?.stats?.AUTH_CERTIFIED_BY_ME_BY_STATE?.all?.creditUpdateTime) + ); + setLastUpdateProgrammesCreditsStats( + moment( + parseInt(response?.data?.stats?.AUTH_CERTIFIED_BY_ME_BY_STATE?.all?.creditUpdateTime) + ).fromNow() + ); + } + programmeByStatusAuthAggregationResponse = + response?.data?.stats?.AUTH_CERTIFIED_BY_ME_BY_STATE?.data; + if ( + response?.data?.stats?.CERTIFIED_REVOKED_BY_ME?.last && + String(response?.data?.stats?.CERTIFIED_REVOKED_BY_ME?.last) !== '0' + ) { + setLastUpdateCertifiedCreditsStatsEpoch( + parseInt(response?.data?.stats?.CERTIFIED_REVOKED_BY_ME?.last) + ); + setLastUpdateCertifiedCreditsStats( + moment(parseInt(response?.data?.stats?.CERTIFIED_REVOKED_BY_ME?.last)).fromNow() + ); + } + certifiedRevokedAggregationResponse = response?.data?.stats?.CERTIFIED_REVOKED_BY_ME?.data; + } else if ( + userInfoState?.companyRole === CompanyRole.CERTIFIER && + categoryType === 'overall' + ) { + if ( + response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime && + String(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime) !== '0' + ) { + setLastUpdateProgrammesStatsCEpoch( + parseInt(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime) + ); + setLastUpdateProgrammesStatsC( + moment( + parseInt(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime) + ).fromNow() + ); + } + programmeByStatusAggregationResponse = response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.data; + if ( + response?.data?.stats?.AGG_AUTH_PROGRAMME_BY_STATUS?.all?.creditUpdateTime && + String(response?.data?.stats?.AGG_AUTH_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) !== '0' + ) { + setLastUpdateProgrammesCreditsStatsEpoch( + parseInt(response?.data?.stats?.AGG_AUTH_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) + ); + setLastUpdateProgrammesCreditsStats( + moment( + parseInt(response?.data?.stats?.AGG_AUTH_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) + ).fromNow() + ); + } + programmeByStatusAuthAggregationResponse = + response?.data?.stats?.AGG_AUTH_PROGRAMME_BY_STATUS?.data; + if ( + response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last && + String(response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last) !== '0' + ) { + setLastUpdateCertifiedCreditsStatsEpoch( + parseInt(response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last) + ); + setLastUpdateCertifiedCreditsStats( + moment(parseInt(response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last)).fromNow() + ); + } + certifiedRevokedAggregationResponse = + response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.data; } else { + if ( + response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime && + String(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime) !== '0' + ) { + setLastUpdateProgrammesStatsCEpoch( + parseInt(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime) + ); + setLastUpdateProgrammesStatsC( + moment( + parseInt(response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.all?.statusUpdateTime) + ).fromNow() + ); + } programmeByStatusAggregationResponse = response?.data?.stats?.AGG_PROGRAMME_BY_STATUS?.data; + if ( + response?.data?.stats?.AGG_AUTH_PROGRAMME_BY_STATUS?.all?.creditUpdateTime && + String(response?.data?.stats?.AGG_AUTH_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) !== '0' + ) { + setLastUpdateProgrammesCreditsStatsEpoch( + parseInt(response?.data?.stats?.AGG_AUTH_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) + ); + setLastUpdateProgrammesCreditsStats( + moment( + parseInt(response?.data?.stats?.AGG_AUTH_PROGRAMME_BY_STATUS?.all?.creditUpdateTime) + ).fromNow() + ); + } + programmeByStatusAuthAggregationResponse = + response?.data?.stats?.AGG_AUTH_PROGRAMME_BY_STATUS?.data; + if ( + response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last && + String(response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last) !== '0' + ) { + setLastUpdateCertifiedCreditsStatsEpoch( + parseInt(response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last) + ); + setLastUpdateCertifiedCreditsStats( + moment(parseInt(response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.last)).fromNow() + ); + } certifiedRevokedAggregationResponse = response?.data?.stats?.CERTIFIED_REVOKED_PROGRAMMES?.data; } - let totalProgrammes = 0; - let totalEstCredits = 0; - let totalIssuedCredits = 0; - let totalRetiredCredits = 0; - let totalBalancecredit = 0; - let totalTxCredits = 0; - let totalCertifiedCredit = 0; - let totalUnCertifiedredit = 0; - let totalRevokedCredits = 0; + let totalProgrammes: any = 0; + let totalEstCredits: any = 0; + let totalIssuedCredits: any = 0; + let totalRetiredCredits: any = 0; + let totalBalancecredit: any = 0; + let totalTxCredits: any = 0; + let totalFrozenCredits: any = 0; + let totalCertifiedCredit: any = 0; + let totalUnCertifiedredit: any = 0; + let totalRevokedCredits: any = 0; + let pendingProgrammesC: any = 0; + let authorisedProgrammesC: any = 0; + let rejectedProgrammesC: any = 0; + const programmeStatusA = Object.values(ProgrammeStageLegend); if (programmeByStatusAggregationResponse?.length > 0) { programmeByStatusAggregationResponse?.map((responseItem: any, index: any) => { - console.log('programmeByStatusAggregationResponse ---- > ', responseItem); - if (responseItem?.currentStage === 'AwaitingAuthorization') { + console.log('mine --> check -- > ', programmeByStatusAggregationResponse); + if ([ProgrammeStage.AWAITING_AUTHORIZATION].includes(responseItem?.currentStage)) { totalProgrammes = totalProgrammes + parseInt(responseItem?.count); - totalEstCredits = totalEstCredits + parseFloat(responseItem?.totalestcredit); - totalIssuedCredits = totalIssuedCredits + parseFloat(responseItem?.totalissuedcredit); - totalRetiredCredits = - totalRetiredCredits + parseFloat(responseItem?.totalretiredcredit); - totalBalancecredit = totalBalancecredit + parseFloat(responseItem?.totalbalancecredit); - totalTxCredits = totalTxCredits + parseFloat(responseItem?.totaltxcredit); - setPendingProjects(parseInt(responseItem?.count)); + pendingProgrammesC = parseInt(responseItem?.count); } - if (responseItem?.currentStage === 'Rejected') { + if ([ProgrammeStage.REJECTED].includes(responseItem?.currentStage)) { totalProgrammes = totalProgrammes + parseInt(responseItem?.count); - totalEstCredits = totalEstCredits + parseFloat(responseItem?.totalestcredit); - totalIssuedCredits = totalIssuedCredits + parseFloat(responseItem?.totalissuedcredit); - totalRetiredCredits = - totalRetiredCredits + parseFloat(responseItem?.totalretiredcredit); - totalBalancecredit = totalBalancecredit + parseFloat(responseItem?.totalbalancecredit); - totalTxCredits = totalTxCredits + parseFloat(responseItem?.totaltxcredit); - setRejectedProjects(parseInt(responseItem?.count)); + rejectedProgrammesC = parseInt(responseItem?.count); } - if (responseItem?.currentStage === 'Authorised') { + if ([ProgrammeStage.AUTHORISED].includes(responseItem?.currentStage)) { totalProgrammes = totalProgrammes + parseInt(responseItem?.count); - totalEstCredits = totalEstCredits + parseFloat(responseItem?.totalestcredit); - totalIssuedCredits = totalIssuedCredits + parseFloat(responseItem?.totalissuedcredit); - totalRetiredCredits = - totalRetiredCredits + parseFloat(responseItem?.totalretiredcredit); - totalBalancecredit = totalBalancecredit + parseFloat(responseItem?.totalbalancecredit); - totalTxCredits = totalTxCredits + parseFloat(responseItem?.totaltxcredit); - setAuthorisedProjects(parseInt(responseItem?.count)); + authorisedProgrammesC = parseInt(responseItem?.count); } }); setTotalProjects(totalProgrammes); + setPendingProjects(pendingProgrammesC); + setAuthorisedProjects(authorisedProgrammesC); + setRejectedProjects(rejectedProgrammesC); } else { setPendingProjects(0); setAuthorisedProjects(0); setRejectedProjects(0); setTotalProjects(0); } + if (programmeByStatusAuthAggregationResponse?.length > 0) { + programmeByStatusAuthAggregationResponse?.map((responseItem: any) => { + totalEstCredits = totalEstCredits + parseFloat(responseItem?.totalestcredit); + totalIssuedCredits = totalIssuedCredits + parseFloat(responseItem?.totalissuedcredit); + totalRetiredCredits = totalRetiredCredits + parseFloat(responseItem?.totalretiredcredit); + totalBalancecredit = totalBalancecredit + parseFloat(responseItem?.totalbalancecredit); + totalTxCredits = totalTxCredits + parseFloat(responseItem?.totaltxcredit); + totalFrozenCredits = totalFrozenCredits + parseFloat(responseItem?.totalfreezecredit); + }); + } if (certifiedRevokedAggregationResponse) { totalCertifiedCredit = parseFloat(certifiedRevokedAggregationResponse?.certifiedSum); totalUnCertifiedredit = parseFloat(certifiedRevokedAggregationResponse?.uncertifiedSum); @@ -633,25 +1211,32 @@ const Dashboard = () => { } setCreditBalance(parseFloat(response?.data?.stats?.CREDIT_STATS_BALANCE?.sum)); const creditAuthorized = totalEstCredits - totalIssuedCredits; - pieSeriesCreditsData.push(creditAuthorized); - pieSeriesCreditsData.push(totalBalancecredit); - pieSeriesCreditsData.push(totalTxCredits); - pieSeriesCreditsData.push(totalRetiredCredits); - - pieSeriesCreditsCerifiedData.push(totalCertifiedCredit); - pieSeriesCreditsCerifiedData.push(totalUnCertifiedredit); - pieSeriesCreditsCerifiedData.push(totalRevokedCredits); - const totalCreditsCertified = - totalCertifiedCredit + totalUnCertifiedredit + totalRevokedCredits; + pieSeriesCreditsData.push(addRoundNumber(creditAuthorized)); + pieSeriesCreditsData.push( + addRoundNumber( + totalIssuedCredits - totalTxCredits - totalRetiredCredits - totalFrozenCredits + ) + ); + pieSeriesCreditsData.push(addRoundNumber(totalTxCredits)); + pieSeriesCreditsData.push(addRoundNumber(totalRetiredCredits)); + + pieSeriesCreditsCerifiedData.push(addRoundNumber(totalCertifiedCredit)); + pieSeriesCreditsCerifiedData.push(addRoundNumber(totalUnCertifiedredit)); + pieSeriesCreditsCerifiedData.push(addRoundNumber(totalRevokedCredits)); + const totalCreditsCertified = addRoundNumber( + totalCertifiedCredit + totalUnCertifiedredit + totalRevokedCredits + ); + setCreditsPieChartTotal( + String(addCommSep(totalEstCredits)) !== 'NaN' ? addCommSep(totalEstCredits) : 0 + ); + setCertifiedCreditsPieChartTotal(addCommSep(totalCreditsCertified)); + const output = '

' + 'ITMOs' + '

' + addCommSep(totalCreditsCertified) + '

'; optionDonutPieA.plotOptions.pie.donut.labels.total.formatter = () => '' + String(addCommSep(totalEstCredits)) !== 'NaN' ? addCommSep(totalEstCredits) : 0; optionDonutPieB.plotOptions.pie.donut.labels.total.formatter = () => '' + addCommSep(totalCreditsCertified); - - console.log({ pieSeriesCreditsData, pieSeriesCreditsCerifiedData }); setCreditPieSeries(pieSeriesCreditsData); setCreditCertifiedPieSeries(pieSeriesCreditsCerifiedData); - setLastUpdate(response?.data?.lastUpdate); } catch (error: any) { console.log('Error in getting users', error); message.open({ @@ -665,132 +1250,171 @@ const Dashboard = () => { } }; - useEffect(() => { - getUserProfileDetails(); - }, []); - - useEffect(() => { - console.log('rejected projects hanges --- ', rejectedProjects); - }, [rejectedProjects]); - useEffect(() => { getAllProgrammeAnalyticsStatsWithoutTimeRange(); - if (companyRole === 'ProgrammeDeveloper') { + if (userInfoState?.companyRole === CompanyRole.PROGRAMME_DEVELOPER) { setCategoryType('mine'); } - }, [companyRole]); + }, []); useEffect(() => { getAllProgrammeAnalyticsStats(); - getAllProgrammeAnalyticsStatsCharts(); + getAllProgrammesAggChartStats(); }, [startTime, endTime, categoryType]); - const seriesTotalProgrammesY = [ - { - name: 'Authorised', - data: authorisedProgrammes, - }, - { - name: 'Rejected', - data: rejectedProgrammes, - }, - { - name: 'Pending', - data: pendingProgrammes, - }, - ]; - - const seriesTotalProgrammesSubY = [ - { - name: 'Energy', - data: energyProgrammes, - }, - { - name: 'Health', - data: healthProgrammes, - }, - { - name: 'Education', - data: educationProgrammes, - }, - { - name: 'Transport', - data: transportProgrammes, - }, - { - name: 'Manufacturing', - data: manufacturingProgrammes, - }, - { - name: 'Hospitality', - data: hospitalityProgrammes, - }, - { - name: 'Forestry', - data: forestryProgrammes, - }, - { - name: 'Waste', - data: wasteProgrammes, - }, - { - name: 'Agriculture', - data: agricultureProgrammes, - }, - { - name: 'Other', - data: otherProgrammes, - }, - ]; - - const seriesTotalCreditsY = [ - { - name: 'Authorised', - data: authorizedCredits, - }, - { - name: 'Issued', - data: issuedCredits, - }, - { - name: 'Transferred', - data: transferredCredits, - }, - { - name: 'Retired', - data: retiredCredits, - }, - ]; - - const seriesTotalCreditsCertifiedY = [ - { - name: 'Certified', - data: certifiedCredits, - }, - { - name: 'UnCertified', - data: unCertifiedCredits, - }, - { - name: 'Revoked', - data: revokedCredits, - }, - ]; + useEffect(() => { + ApexCharts.exec('total-programmes-sector', 'updateSeries', { + data: totalProgrammesSectorSeries, + }); + ApexCharts.exec('total-programmes-sector', 'updateOptions', { + xaxis: { + categories: totalProgrammesSectorOptionsLabels, + }, + }); + }, [totalProgrammesSectorSeries, categoryType, totalProgrammesSectorOptionsLabels]); useEffect(() => { - console.log('transfr credit --- > ', transferredCredits); - }, [transferredCredits]); + ApexCharts.exec('total-programmes', 'updateSeries', { + data: totalProgrammesSeries, + }); + ApexCharts.exec('total-programmes', 'updateOptions', { + xaxis: { + categories: totalProgrammesOptionsLabels, + }, + }); + }, [totalProgrammesSeries, categoryType, totalProgrammesOptionsLabels]); - const count1 = ['all', ['>=', ['get', 'count'], 0], ['<', ['get', 'count'], 4]]; - const count2 = ['all', ['>=', ['get', 'count'], 4], ['<', ['get', 'count'], 6]]; - const count3 = ['all', ['>=', ['get', 'count'], 6], ['<', ['get', 'count'], 10]]; - const count4 = ['all', ['>=', ['get', 'count'], 10], ['<', ['get', 'count'], 16]]; - const count5 = ['>=', ['get', 'count'], 16]; + useEffect(() => { + ApexCharts.exec('total-credits', 'updateSeries', { + data: totalCreditsSeries, + }); + ApexCharts.exec('total-credits', 'updateOptions', { + xaxis: { + categories: totalCreditsOptionsLabels, + }, + }); + }, [totalCreditsSeries, categoryType, totalCreditsOptionsLabels]); - // colors to use for the categories - const colors = ['#33adff', '#4db8ff', '#80ccff', '#99d6ff', '#ccebff']; + useEffect(() => { + ApexCharts.exec('total-credits-certified', 'updateSeries', { + data: totalCertifiedCreditsSeries, + }); + ApexCharts.exec('total-credits-certified', 'updateOptions', { + xaxis: { + categories: totalCertifiedCreditsOptionsLabels, + }, + }); + }, [totalCertifiedCreditsSeries, categoryType, totalCertifiedCreditsOptionsLabels]); + + useEffect(() => { + ApexCharts.exec('credits', 'updateSeries', { + series: creditsPieSeries, + }); + ApexCharts.exec('credits', 'updateOptions', { + plotOptions: { + pie: { + labels: { + total: { + formatter: () => creditsPieChartTotal, + }, + }, + }, + }, + }); + }, [creditsPieSeries, categoryType, creditsPieChartTotal]); + + useEffect(() => { + ApexCharts.exec('certified-credits', 'updateSeries', { + series: creditsCertifiedPieSeries, + }); + ApexCharts.exec('credits', 'updateOptions', { + plotOptions: { + pie: { + labels: { + total: { + formatter: () => certifiedCreditsPieChartTotal, + }, + }, + }, + }, + }); + }, [creditsCertifiedPieSeries, categoryType, certifiedCreditsPieChartTotal]); - function donutSegment(start: any, end: any, r: any, r0: any, color: any) { + useEffect(() => { + const timer = setInterval(() => { + if (lastUpdateProgrammesStatsEpoch !== 0) { + setLastUpdateProgrammesStats(moment(lastUpdateProgrammesStatsEpoch).fromNow()); + } + if (lastUpdateProgrammesStatsCEpoch !== 0) { + setLastUpdateProgrammesStatsC(moment(lastUpdateProgrammesStatsCEpoch).fromNow()); + } + if (lastUpdatePendingTransferSentEpoch !== 0) { + setLastUpdatePendingTransferSent(moment(lastUpdatePendingTransferSentEpoch).fromNow()); + } + if (lastUpdateCreditBalanceEpoch !== 0) { + setLastUpdateCreditBalance(moment(lastUpdateCreditBalanceEpoch).fromNow()); + } + if (lastUpdatePendingTransferReceivedEpoch !== 0) { + setLastUpdatePendingTransferReceived( + moment(lastUpdatePendingTransferReceivedEpoch).fromNow() + ); + } + if (lastUpdateProgrammesCertifiableEpoch !== 0) { + setLastUpdateProgrammesCertifiable(moment(lastUpdateProgrammesCertifiableEpoch).fromNow()); + } + if (lastUpdateProgrammesCertifiedEpoch !== 0) { + setLastUpdateProgrammesCertified(moment(lastUpdateProgrammesCertifiedEpoch).fromNow()); + } + if (lastUpdateCertifiedCreditsStatsEpoch !== 0) { + setLastUpdateCertifiedCreditsStats(moment(lastUpdateCertifiedCreditsStatsEpoch).fromNow()); + } + if (lastUpdateProgrammesCreditsStatsEpoch !== 0) { + setLastUpdateProgrammesCreditsStats( + moment(lastUpdateProgrammesCreditsStatsEpoch).fromNow() + ); + } + if (lastUpdateProgrammesSectorStatsCEpoch !== 0) { + setLastUpdateProgrammesSectorStatsC( + moment(lastUpdateProgrammesSectorStatsCEpoch).fromNow() + ); + } + if (lastUpdateTotalCreditsEpoch !== 0) { + setLastUpdateTotalCredits(moment(lastUpdateTotalCreditsEpoch).fromNow()); + } + if (lastUpdateTotalCreditsCertifiedEpoch !== 0) { + setLastUpdateTotalCreditsCertified(moment(lastUpdateTotalCreditsCertifiedEpoch).fromNow()); + } + if (lastUpdateTransferLocationsEpoch !== 0) { + setLastUpdateTransferLocations(moment(lastUpdateTransferLocationsEpoch).fromNow()); + } + }, 60 * 1000); + return () => { + clearInterval(timer); + }; + }, [ + lastUpdateProgrammesStatsEpoch, + lastUpdateProgrammesStatsCEpoch, + lastUpdatePendingTransferSentEpoch, + lastUpdateCreditBalanceEpoch, + lastUpdatePendingTransferReceivedEpoch, + lastUpdateProgrammesCertifiableEpoch, + lastUpdateProgrammesCertifiedEpoch, + lastUpdateProgrammesCreditsStatsEpoch, + lastUpdateCertifiedCreditsStatsEpoch, + lastUpdateProgrammesSectorStatsCEpoch, + lastUpdateTotalCreditsEpoch, + lastUpdateTotalCreditsCertifiedEpoch, + lastUpdateTransferLocationsEpoch, + ]); + + const countS = ['all', ['>=', ['get', 'count'], 0]]; + const pending = ['==', ['get', 'stage'], 'AwaitingAuthorization']; + const authorised = ['==', ['get', 'stage'], 'Authorised']; + const rejected = ['==', ['get', 'stage'], 'Rejected']; + + const colors = ['#6ACDFF', '#FF8183', '#CDCDCD']; + + const donutSegment = (start: any, end: any, r: any, r0: any, color: any) => { if (end - start === 1) end -= 0.00001; const a0 = 2 * Math.PI * (start - 0.25); const a1 = 2 * Math.PI * (end - 0.25); @@ -806,39 +1430,58 @@ const Dashboard = () => { } A ${r} ${r} 0 ${largeArc} 1 ${r + r * x1} ${r + r * y1} L ${r + r0 * x1} ${ r + r0 * y1 } A ${r0} ${r0} 0 ${largeArc} 0 ${r + r0 * x0} ${r + r0 * y0}" fill="${color}" />`; - } + }; // code for creating an SVG donut chart from feature properties - function createDonutChart(properties: any) { + const createDonutChart = (properties: any) => { console.log('properties of donut creator --- > ', properties); const offsets = []; + const offsetsStage = []; let counts: any = []; - if (properties.count1) { - counts = [ - properties.count1, - properties.count2, - properties.count3, - properties.count4, - properties.count5, - ]; - } else { + let programmeStageCounts: any = []; + if (properties.count) { counts = [properties.count]; } + + if (properties.cluster_id) { + programmeStageCounts = [properties.authorised, properties.rejected, properties.pending]; + } else { + if (properties?.stage === 'AwaitingAuthorization') { + programmeStageCounts = [0, 0, properties.count]; + } else if (properties?.stage === 'Authorised') { + programmeStageCounts = [properties.count, 0, 0]; + } else if (properties?.stage === 'Rejected') { + programmeStageCounts = [0, properties.count, 0]; + } + } let total = 0; for (const count of counts) { offsets.push(total); total += count; } - const fontSize = total >= 1000 ? 22 : total >= 100 ? 20 : total >= 10 ? 18 : 16; - const r = total >= 1000 ? 50 : total >= 100 ? 32 : total >= 10 ? 24 : 18; + let totalStage = 0; + for (const count of programmeStageCounts) { + offsetsStage.push(totalStage); + totalStage += count; + } + const fontSize = total >= 1000 ? 22 : total >= 500 ? 20 : total >= 100 ? 18 : 16; + const r = total >= 1000 ? 52 : total >= 500 ? 36 : total >= 100 ? 30 : 18; const r0 = Math.round(r * 0.6); const w = r * 2; let html = `
`; - for (let i = 0; i < counts.length; i++) { - html += donutSegment(offsets[i] / total, (offsets[i] + counts[i]) / total, r, r0, colors[i]); + for (let i = 0; i < programmeStageCounts?.length; i++) { + if (programmeStageCounts[i] !== 0) { + html += donutSegment( + offsetsStage[i] === 0 ? 0 : offsetsStage[i] / totalStage, + (offsetsStage[i] + programmeStageCounts[i]) / totalStage, + r, + r0, + colors[i] + ); + } } html += ` @@ -850,82 +1493,117 @@ ${total} const el = document.createElement('div'); el.innerHTML = html; return el.firstChild; - } + }; useEffect(() => { setTimeout(() => { - const map = new mapboxgl.Map({ - container: mapContainerInternationalRef.current || '', - // Choose from Mapbox's core styles, or make your own style with Mapbox Studio - style: 'mapbox://styles/mapbox/streets-v11', - center: [12, 50], - zoom: 0.5, - }); - - // Add markers to the map. - map.on('load', () => { - map.addSource('countries', { - type: 'vector', - url: 'mapbox://mapbox.country-boundaries-v1', + if (mapContainerInternationalRef?.current) { + const map = new mapboxgl.Map({ + container: mapContainerInternationalRef?.current || '', + // Choose from Mapbox's core styles, or make your own style with Mapbox Studio + style: 'mapbox://styles/mapbox/streets-v11', + center: [12, 50], + zoom: 0.5, }); - // Build a GL match expression that defines the color for every vector tile feature - // Use the ISO 3166-1 alpha 3 code as the lookup key for the country shape - const matchExpression: any = ['match', ['get', 'iso_3166_1']]; - - const transferLocations: any = [...programmeTransferLocations]; + // Add markers to the map. + map.on('load', () => { + map.addSource('countries', { + type: 'vector', + url: 'mapbox://mapbox.country-boundaries-v1', + }); - // Calculate color values for each country based on 'hdi' value - for (const row of transferLocations) { - // Convert the range of data values to a suitable color - // const blue = row.ratio * 255; + // Build a GL match expression that defines the color for every vector tile feature + // Use the ISO 3166-1 alpha 3 code as the lookup key for the country shape + const matchExpression: any = ['match', ['get', 'iso_3166_1']]; + const txLocationMap: any = {}; + + const transferLocations: any = [...programmeTransferLocations]; + + // Calculate color values for each country based on 'hdi' value + for (const row of transferLocations) { + // Convert the range of data values to a suitable color + // const blue = row.ratio * 255; + + const color = + row.count < 2 + ? `#4da6ff` + : row.count < 10 + ? '#0080ff' + : row.count < 50 + ? '#0059b3' + : row.count < 100 + ? '#003366' + : '#000d1a'; + + matchExpression.push(row.country, color); + txLocationMap[row.country] = row.count; + } - const color = - row.ratio < 0.25 - ? `#FFC343` - : row.ratio < 0.5 - ? '#FFAC6F' - : row.ratio < 0.75 - ? '#FF923D' - : '#FE8163'; + function getCountryCodes(dataSet: any) { + return dataSet.map((item: any) => item.code); + } - matchExpression.push(row.code, color); - } + map.on('click', function (e) { + const features = map.queryRenderedFeatures(e.point, { layers: ['countries-join'] }); + if (!features.length) { + return; + } - function getCountryCodes(dataSet: any) { - return dataSet.map((item: any) => item.code); - } + const feature = features[0]; + if (!txLocationMap[feature.properties?.iso_3166_1]) { + return; + } + console.log(feature); + + const popup = new mapboxgl.Popup() + .setLngLat(map.unproject(e.point)) + .setHTML( + `${feature.properties?.name_en} : ${txLocationMap[feature.properties?.iso_3166_1]}` + ) + .addTo(map); + }); - // Last value is the default, used where there is no data - matchExpression.push('rgba(0, 0, 0, 0)'); + // Use the same approach as above to indicate that the symbols are clickable + // by changing the cursor style to 'pointer'. + map.on('mousemove', function (e) { + const features = map.queryRenderedFeatures(e.point, { layers: ['countries-join'] }); + map.getCanvas().style.cursor = + features.length > 0 && txLocationMap[features[0].properties?.iso_3166_1] + ? 'pointer' + : ''; + }); - map.addLayer( - { - id: 'countries-join', - type: 'fill', - source: 'countries', - 'source-layer': 'country_boundaries', - paint: { - 'fill-color': matchExpression, + // Last value is the default, used where there is no data + matchExpression.push('rgba(0, 0, 0, 0)'); + + map.addLayer( + { + id: 'countries-join', + type: 'fill', + source: 'countries', + 'source-layer': 'country_boundaries', + paint: { + 'fill-color': matchExpression, + }, }, - }, - 'admin-1-boundary-bg' - ); - }); + 'admin-1-boundary-bg' + ); + }); + } }, 1000); }, [programmeTransferLocations]); useEffect(() => { - // const address = programmeLocations[0]; - + console.log('programme locations ---- > ', programmeLocations); setTimeout(() => { if (mapContainerRef.current) { const map = new mapboxgl.Map({ - container: mapContainerRef.current || '', + container: mapContainerRef?.current || '', zoom: 4, center: programmeLocations?.features[0]?.geometry?.coordinates ? programmeLocations?.features[0]?.geometry?.coordinates - : [54.44073, 16.39371], + : [7.4924165, 5.5324032], // Choose from Mapbox's core styles, or make your own style with Mapbox Studio style: 'mapbox://styles/mapbox/light-v11', }); @@ -937,12 +1615,11 @@ ${total} cluster: true, clusterRadius: 40, clusterProperties: { - // keep separate counts for each countnitude category in a cluster - count1: ['+', ['case', count1, 1, 0]], - count2: ['+', ['case', count2, 1, 0]], - count3: ['+', ['case', count3, 1, 0]], - count4: ['+', ['case', count4, 1, 0]], - count5: ['+', ['case', count5, 1, 0]], + // keep separate counts for each programmeStage category in a cluster + count: ['+', ['case', countS, ['get', 'count'], 0]], + pending: ['+', ['case', pending, ['get', 'count'], 0]], + authorised: ['+', ['case', authorised, ['get', 'count'], 0]], + rejected: ['+', ['case', rejected, ['get', 'count'], 0]], }, }); // circle and symbol layers for rendering individual programmeLocations (unclustered points) @@ -952,18 +1629,7 @@ ${total} source: 'programmeLocations', filter: ['!=', 'cluster', true], paint: { - 'circle-color': [ - 'case', - count1, - colors[0], - count2, - colors[1], - count3, - colors[2], - count4, - colors[3], - colors[4], - ], + 'circle-color': ['case', pending, colors[0], authorised, colors[1], colors[2]], 'circle-opacity': 1, 'circle-radius': 10, }, @@ -980,7 +1646,7 @@ ${total} // for every cluster on the screen, create an HTML marker for it (if we didn't yet), // and add it to the map if it's not there already for (const feature of features) { - console.log(feature.properties); + // console.log(feature.properties); const coords = feature.geometry.coordinates; const properties = feature.properties; // if (!properties.cluster) continue; @@ -992,10 +1658,6 @@ ${total} marker = markers[id] = new mapboxgl.Marker({ element: el, }).setLngLat(coords); - - // marker = markers[id] = new mapboxgl.Marker({ - // element: el, - // }).; } newMarkers[id] = marker; @@ -1029,88 +1691,109 @@ ${total} - ) : companyRole === 'ProgrammeDeveloper' ? ( + ) : userInfoState?.companyRole === CompanyRole.PROGRAMME_DEVELOPER ? ( ) : ( ) } loading={loadingWithoutTimeRange} + companyRole={userInfoState?.companyRole} /> - ) : companyRole === 'ProgrammeDeveloper' ? ( + ) : userInfoState?.companyRole === CompanyRole.PROGRAMME_DEVELOPER ? ( ) : ( ) } loading={loadingWithoutTimeRange} + companyRole={userInfoState?.companyRole} /> - ) : companyRole === 'ProgrammeDeveloper' ? ( + ) : userInfoState?.companyRole === CompanyRole.PROGRAMME_DEVELOPER ? ( ) : ( ) } loading={loadingWithoutTimeRange} + companyRole={userInfoState?.companyRole} /> @@ -1130,7 +1813,7 @@ ${total} />
- {companyRole === 'Certifier' && ( + {userInfoState?.companyRole === CompanyRole.CERTIFIER && ( OVERALL @@ -1150,26 +1833,55 @@ ${total} pending={pendingProjects} rejected={rejectedProjects} authorized={authorisedProjects} - updatedDate={parseInt(lastUpdateProgrammesStats)} + updatedDate={lastUpdateProgrammesStatsC} loading={loading} + toolTipText={t( + userInfoState?.companyRole === CompanyRole.GOVERNMENT + ? 'tTProgrammesGoverment' + : userInfoState?.companyRole === CompanyRole.PROGRAMME_DEVELOPER + ? 'tTProgrammesProgrammeDev' + : categoryType === 'mine' + ? 'tTProgrammesCertifierMine' + : 'tTProgrammesCertifierOverall' + )} /> - + @@ -1178,20 +1890,40 @@ ${total} @@ -1200,20 +1932,40 @@ ${total} @@ -1222,8 +1974,30 @@ ${total}
-
Programme Locations
- {loading ? ( +
+
{t('programmeLocations')}
+
+
+ + + +
+
+
+ {loadingCharts ? (
@@ -1233,10 +2007,13 @@ ${total}
-
-
- {moment(parseInt(lastUpdateProgrammesStats)).fromNow()} -
+
+ + + +
+
+
{lastUpdateProgrammesStatsC}
)} @@ -1244,8 +2021,28 @@ ${total}
-
Transfer Locations International
- {loading ? ( +
+
{t('trasnferLocations')}
+
+ + + +
+
+ {loadingCharts ? (
@@ -1255,9 +2052,9 @@ ${total}
-
+
- {moment(lastUpdate * 1000).fromNow()} + {lastUpdateTransferLocations !== '0' && lastUpdateTransferLocations}
diff --git a/web/src/Pages/Dashboard/dashboardTypesInitialValues.ts b/web/src/Pages/Dashboard/dashboardTypesInitialValues.ts new file mode 100644 index 000000000..4552a213c --- /dev/null +++ b/web/src/Pages/Dashboard/dashboardTypesInitialValues.ts @@ -0,0 +1,80 @@ +import { ProgrammeStageLegend } from '../../Casl/enums/programme-status.enum'; +import { Sector } from '../../Casl/enums/sector.enum'; + +export interface ChartSeriesItem { + name: string; + data: any[]; +} + +export const getTotalProgrammesInitialValues = () => { + const totalProgrammmesStatusInitialValues: ChartSeriesItem[] = []; + const statusArray = Object.values(ProgrammeStageLegend); + statusArray?.map((sector: any) => { + totalProgrammmesStatusInitialValues.push({ + name: sector, + data: [], + }); + }); + return totalProgrammmesStatusInitialValues; +}; + +export const totalProgrammesInitialValues = [ + { + name: 'Authorised', + data: [], + }, + { + name: 'Rejected', + data: [], + }, + { + name: 'Pending', + data: [], + }, +]; + +export const getTotalProgrammesSectorInitialValues = () => { + const totalProgrammmesSectorInitialValues: ChartSeriesItem[] = []; + const sectorsArray = Object.values(Sector); + sectorsArray?.map((sector: any) => { + totalProgrammmesSectorInitialValues.push({ + name: sector, + data: [], + }); + }); + return totalProgrammmesSectorInitialValues; +}; + +export const totalCreditsSeriesInitialValues = [ + { + name: 'Authorised', + data: [], + }, + { + name: 'Issued', + data: [], + }, + { + name: 'Transferred', + data: [], + }, + { + name: 'Retired', + data: [], + }, +]; + +export const totalCertifiedCreditsSeriesInitialValues = [ + { + name: 'Certified', + data: [], + }, + { + name: 'Uncertified', + data: [], + }, + { + name: 'Revoked', + data: [], + }, +]; diff --git a/web/src/Pages/Dashboard/pieChartStat.tsx b/web/src/Pages/Dashboard/pieChartStat.tsx index 7e19fcebc..112b3dc40 100644 --- a/web/src/Pages/Dashboard/pieChartStat.tsx +++ b/web/src/Pages/Dashboard/pieChartStat.tsx @@ -1,20 +1,24 @@ import React, { FC } from 'react'; -import { DatePicker, Skeleton } from 'antd'; +import { DatePicker, Skeleton, Tooltip } from 'antd'; import Chart from 'react-apexcharts'; import moment from 'moment'; +import { InfoCircle } from 'react-bootstrap-icons'; +import { StatsCardsTypes } from '../../Casl/enums/statsCards.type.enum'; const { RangePicker } = DatePicker; export interface PieChartStatsProps { - title: string; + id: string; + title: any; options: any; - series: any; - lastUpdate: number; + series: any[]; + lastUpdate: any; loading: boolean; + toolTipText: string; } const PieChartsStat: FC = (props: PieChartStatsProps) => { - const { title, options, series, lastUpdate, loading } = props; + const { id, title, options, series, lastUpdate, loading, toolTipText } = props; return (
{loading ? ( @@ -24,12 +28,29 @@ const PieChartsStat: FC = (props: PieChartStatsProps) => {
) : ( <> -
{title}
+
+
+ {title} + {[StatsCardsTypes.CREDITS, StatsCardsTypes.CERTIFIED_CREDITS].includes(title) && ( +
(ITMOs)
+ )} +
+
+ + + +
+
- +
-
{moment(lastUpdate).fromNow()}
+ {lastUpdate !== '0' &&
{lastUpdate}
}
)} diff --git a/web/src/Pages/Dashboard/toolTipTextGen.ts b/web/src/Pages/Dashboard/toolTipTextGen.ts new file mode 100644 index 000000000..118131c9a --- /dev/null +++ b/web/src/Pages/Dashboard/toolTipTextGen.ts @@ -0,0 +1,151 @@ +import { CompanyRole } from '../../Casl/enums/company.role.enum'; +import { StatsCardsTypes } from '../../Casl/enums/statsCards.type.enum'; + +export const toolTipTextGen = (companyRole: any, cardType: any, mine?: boolean) => { + let text: any = ''; + if (companyRole === CompanyRole.GOVERNMENT) { + if (cardType === StatsCardsTypes.PROGRAMMES_PENDING) { + text = 'Pending state programmes awaiting authorisation'; + } else if (cardType === StatsCardsTypes.TRANSFER_REQUEST_SENT) { + text = + 'Pending credit transfer requests sent to programme owners initiated by your organisation'; + } else if (cardType === StatsCardsTypes.CREDIT_BALANCE) { + text = 'Total credit balance owned by your organisation'; + } else if (cardType === StatsCardsTypes.PROGRAMMES) { + text = + 'Number of programmes created during the specified period and their programme state in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.CREDITS) { + text = + 'Number of credits of programmes created during the specified period and their credit state in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.CERTIFIED_CREDITS) { + text = + 'Number of credits of programmes created during the specified period, uncertified, certified and revoked in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.TOTAL_PROGRAMMES) { + text = + 'Graphical representation of the number of programmes created during the specified period in each programme state in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.TOTAL_PROGRAMMES_SECTOR) { + text = + 'Graphical representation of the number of programmes in each programme sector created during the specified time in the carbon registry'; + } else if (cardType === StatsCardsTypes.TOTAL_CREDITS) { + text = + 'Graphical representation of the number of credits of programmes created during the specified period in each credit state in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.TOTAL_CREDITS_CERTIFIED) { + text = + 'Graphical representation of the number of credits of programmes created during the specified period certified, uncertified and revoked in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.PROGRAMME_LOCATIONS) { + text = + 'Locations of the programmes created during the specified period and their programme states in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.TRANSFER_LOCATIONS_INTERNATIONAL) { + text = + 'Locations of credits of international transfer requests recognised during the specified period'; + } + } else if (companyRole === CompanyRole.PROGRAMME_DEVELOPER) { + if (cardType === StatsCardsTypes.TRANSFER_REQUEST_RECEIVED) { + text = 'Pending credit transfer requests received by your organisation'; + } else if (cardType === StatsCardsTypes.TRANSFER_REQUEST_SENT) { + text = 'Pending local credit transfer requests initiated by your organisation'; + } else if (cardType === StatsCardsTypes.CREDIT_BALANCE) { + text = 'Total credit balance owned by your organisation'; + } else if (cardType === StatsCardsTypes.PROGRAMMES) { + text = + 'Number of programmes created during the specified period and their programme state in the carbon registry at present, owned by your organisation'; + } else if (cardType === StatsCardsTypes.CREDITS) { + text = + 'Number of credits of programmes created during the specified period and their credit state in the carbon registry at present, owned by your organisation'; + } else if (cardType === StatsCardsTypes.CERTIFIED_CREDITS) { + text = + 'Number of credits of programmes created during the specified period, uncertified, certified and revoked in the carbon registry at present, owned by your organisation'; + } else if (cardType === StatsCardsTypes.TOTAL_PROGRAMMES) { + text = + 'Graphical representation of the number of programmes created during the specified period, owned by your organisation, in each programme state in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.TOTAL_PROGRAMMES_SECTOR) { + text = + 'Graphical representation of the number of programmes owned by your organisation, in each programme sector created during the specified time in the carbon registry'; + } else if (cardType === StatsCardsTypes.TOTAL_CREDITS) { + text = + 'Graphical representation of the number of credits of programmes created during the specified period, owned by your organisation, in each credit state in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.TOTAL_CREDITS_CERTIFIED) { + text = + 'Graphical representation of the number of credits of programmes created during the specified period, owned by your organisation, certified, uncertified and revoked in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.PROGRAMME_LOCATIONS) { + text = + 'Locations of the programmes created during the specified period, owned by your organisation, and their programme states in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.TRANSFER_LOCATIONS_INTERNATIONAL) { + text = + 'Locations of credits international transfer requests of programmes owned by your organisation recognised during the specified period'; + } + } else if (companyRole === CompanyRole.CERTIFIER && mine === true) { + if (cardType === StatsCardsTypes.PROGRAMMES_UNCERTIFIED) { + text = + 'Number of programmes not yet certified including certificates revoked by your organisation'; + } else if (cardType === StatsCardsTypes.PROGRAMMES_CERTIFIED) { + text = 'Number of programmes certified by your organisation'; + } else if (cardType === StatsCardsTypes.CREDIT_CERTIFIED) { + text = 'Number of credits certified by your organisation'; + } else if (cardType === StatsCardsTypes.PROGRAMMES) { + text = + 'Number of programmes created during the specified period, certified by your organisation, and their programme state in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.CREDITS) { + text = + 'Number of credits of programmes created during the specified period, certified by your organisation and their credit state in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.CERTIFIED_CREDITS) { + text = + 'Number of credits of programmes created during the specified period, certified by your organisation, uncertified, certified and revoked in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.TOTAL_PROGRAMMES) { + text = + 'Graphical representation of the number of programmes in each programme sector created during the specified time, certified by your company, in the carbon registry'; + } else if (cardType === StatsCardsTypes.TOTAL_PROGRAMMES_SECTOR) { + text = + 'Graphical representation of the number of programmes in each programme sector created during the specified time, certified by your company, in the carbon registry'; + } else if (cardType === StatsCardsTypes.TOTAL_CREDITS) { + text = + 'Graphical representation of the number of credits of programmes created during the specified period in each credit state in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.TOTAL_CREDITS_CERTIFIED) { + text = + 'Graphical representation of the number of credits of programmes certified, uncertified and revoked (refer above for states), by your organisation, spread over the specified time'; + } else if (cardType === StatsCardsTypes.PROGRAMME_LOCATIONS) { + text = + 'Locations of the programmes created during the specified period, certified by your organisation, and their programme states in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.TRANSFER_LOCATIONS_INTERNATIONAL) { + text = + 'Locations of credits of international transfer requests of programmes certified by your organisation recognised during the specified period'; + } + } else if (companyRole === CompanyRole.CERTIFIER && !mine) { + if (cardType === StatsCardsTypes.PROGRAMMES_UNCERTIFIED) { + text = + 'Number of programmes not yet certified including certificates revoked by your organisation'; + } else if (cardType === StatsCardsTypes.PROGRAMMES_CERTIFIED) { + text = 'Number of programmes certified by your organisation'; + } else if (cardType === StatsCardsTypes.CREDIT_CERTIFIED) { + text = 'Number of credits certified by your organisation'; + } else if (cardType === StatsCardsTypes.PROGRAMMES) { + text = + 'Number of programmes created during the specified period and their programme state in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.CREDITS) { + text = + 'Number of credits of programmes created during the specified period and their credit state in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.CERTIFIED_CREDITS) { + text = + 'Number of credits of programmes created during the specified period, uncertified, certified and revoked in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.TOTAL_PROGRAMMES) { + text = + 'Graphical representation of the number of programmes created during the specified period in each programme state in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.TOTAL_PROGRAMMES_SECTOR) { + text = + 'Graphical representation of the number of programmes in each programme sector created during the specified time in the carbon registry'; + } else if (cardType === StatsCardsTypes.TOTAL_CREDITS) { + text = + 'Graphical representation of the number of credits of programmes created during the specified period in each credit state in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.TOTAL_CREDITS_CERTIFIED) { + text = + 'Graphical representation of the number of credits of programmes created during the specified period certified, uncertified and revoked in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.PROGRAMME_LOCATIONS) { + text = + 'Locations of the programmes created during the specified period and their programme states in the carbon registry at present'; + } else if (cardType === StatsCardsTypes.TRANSFER_LOCATIONS_INTERNATIONAL) { + text = + 'Locations of credits of international transfer requests recognised during the specified period'; + } + } + return text; +}; diff --git a/web/src/Pages/Homepage/homepage.scss b/web/src/Pages/Homepage/homepage.scss index 0771a3558..06d04885d 100644 --- a/web/src/Pages/Homepage/homepage.scss +++ b/web/src/Pages/Homepage/homepage.scss @@ -6,6 +6,7 @@ overflow-y: auto; overflow-x: hidden; background-color: $background-color; + scroll-behavior: smooth; .homepage-header-container { background-color: rgba(255, 255, 255, 0.8); @@ -15,17 +16,20 @@ display: flex; flex-direction: row; align-items: center; - // margin-bottom: 2vw; - // padding-left: 2rem; padding: 0rem 0rem 0 0vw; cursor: pointer; + html { + scroll-behavior: smooth; + } + .title { font-family: 'MuseoSans'; font-size: 1.2rem; color: $logo-text-color; font-weight: 700; margin-right: 0.5rem; + padding-left: 5px; } .title-sub { @@ -33,41 +37,29 @@ font-family: 'MuseoSans'; font-size: 1.2rem; color: $logo-text-color; - // padding-bottom: 3px; } .logo { - // display: flex; - // flex-direction: row; - // justify-content: flex-start; - // align-items: center; - height: 53px; + height: 40px; + // width: 65px; padding-left: 2vw; - // overflow: hidden; - // margin-right: 0.8rem; - - // background-color: red; img { object-fit: cover; - height: auto; - height: 70%; - margin-top: 6px; + // height: auto; + // height: 100%; + // width: 100%; + height: 40px; + width: 36px; + // padding-right: 5px; } } - // button { - // margin-top: 6px; - // margin-right: 10px; - // margin-left: 30px; - // text-transform: uppercase; - // // font-size: 1.25rem; - // // font-weight: 500; - // // color: $primary-color; - // } + .country-name { font-size: 0.65rem; color: $logo-text-color; margin-top: -5px; font-family: 'MuseoSans'; + margin-left: 6px; } } .homepage-button-container { @@ -75,7 +67,7 @@ justify-content: right; min-width: 150px; height: 90px; - background-color: rgba(255, 255, 255, 0.8);; + background-color: rgba(255, 255, 255, 0.8); button { margin-top: 29px; @@ -83,10 +75,45 @@ margin-left: 30px; text-transform: uppercase; } + .ant-btn-primary { + background: linear-gradient(to right, $gradient-background-start, $gradient-background-end); + color: $dark-text-color; + border: none; + } + .ant-btn-primary:hover { + background-color: $gradient-background-start; + } } + // .image-container { + // background: linear-gradient(180deg, rgba(255, 255, 255, 0.3) 10%, rgba(22, 177, 255, 0.4) 40%), + // url('../../Assets/Images/factory.webp') left 10% bottom 20% / cover no-repeat border-box; + // background: linear-gradient(180deg, rgba(255, 255, 255, 0.3) 10%, rgba(22, 177, 255, 0.4) 40%), + // image-set( + // '../../Assets/Images/factory.webp' left 10% bottom 20% / cover no-repeat border-box + // type('image/webp'), + // '../../Assets/Images/factory.png' type('image/png') + // ); + // } + .image-container { + background-image: linear-gradient( + to bottom, + rgba(255, 255, 255, 0.3) 10%, + rgba(22, 177, 255, 0.4) 40% + ), + url('../../Assets/Images/factory.webp'); + background-image: linear-gradient( + to bottom, + rgba(255, 255, 255, 0.3) 10%, + rgba(22, 177, 255, 0.4) 40% + ), + image-set( + '../../Assets/Images/factory.png' type('image/png'), + '../../Assets/Images/factory.webp' type('image/webp') + ); + background-size: cover; + } + .homepage-img-container { - background: linear-gradient(180deg, rgba(255, 255, 255, 0.3) 10%, rgba(22, 177, 255, 0.4) 40%), - url('../../Assets/Images/factory.png') left 10% bottom 20% / cover no-repeat border-box; height: 100vh; background-position-x: 100%; background-position-y: 100%; @@ -96,13 +123,81 @@ flex: 1; text-align: left; - // background-size: 500vh; - @media (max-width: $lg-size) { text-align: center; justify-content: center; padding-top: 50vh; } + .arrows { + width: 60px; + height: 72px; + position: absolute; + left: 50%; + margin-left: -30px; + bottom: 20px; + @media (max-width: $lg-size) { + stroke-width: 2px; + width: 30px; + height: 36px; + } + } + + .arrows path { + stroke: #ffffff; + fill: transparent; + stroke-width: 2.5px; + animation: arrow 2s infinite; + -webkit-animation: arrow 2s infinite; + + @media (max-width: $lg-size) { + stroke-width: 2px; + } + } + + @keyframes arrow { + 0% { + opacity: 0; + } + 40% { + opacity: 1; + } + 80% { + opacity: 0; + } + 100% { + opacity: 0; + } + } + + @-webkit-keyframes arrow /*Safari and Chrome*/ { + 0% { + opacity: 0; + } + 40% { + opacity: 1; + } + 80% { + opacity: 0; + } + 100% { + opacity: 0; + } + } + + .arrows path.a1 { + animation-delay: -1s; + -webkit-animation-delay: -1s; + } + + .arrows path.a2 { + animation-delay: -0.5s; + -webkit-animation-delay: -0.5s; + } + + .arrows path.a3 { + animation-delay: 0s; + -webkit-animation-delay: 0s; + } .text-ctn { margin-left: 2vw; @@ -115,12 +210,12 @@ position: relative; padding-top: 60vh; - @media (max-width: $lg-size) { - font-size: 2.2rem; + @media (max-width: 1800px) { + font-size: 3rem; margin-top: 1rem; margin-bottom: unset; - // width: 100vw; line-height: 1.5em; + padding-top: 50vh; } } .subhome { @@ -141,7 +236,6 @@ } .homepage-content-containerwhite { background-color: $outside-background-color; - // height: 35vh; height: fit-content; padding: 10px 60px 10px 60px; text-align: center; @@ -165,7 +259,6 @@ font-size: 2rem; margin-top: 1rem; color: $title-text-color; - // width: 100vw; line-height: 1.5em; } } @@ -174,7 +267,7 @@ font-size: 0.875rem; font-weight: 400; font-family: $primary-font-family; - color: $body-text-color; + color: $title-text-color; justify-content: center; margin: 20px 60px 20px 60px; @@ -182,7 +275,6 @@ font-size: 0.8rem; margin-top: 1rem; color: $title-text-color; - // width: 100vw; line-height: 1.5em; } .homepagebody_aboutusline1 { @@ -199,7 +291,7 @@ } .homepagebody_aboutuslines { margin-top: 1rem; - margin-bottom: 4rem; + margin-bottom: 3.5rem; } .homepagebody_subtitle { @@ -225,7 +317,6 @@ text-align: center; } .aboutus-card-main-container:hover { - // background: linear-gradient(to top,$gradient-background-end,$gradient-background-start); background: $gradient-background-end; cursor: pointer; } @@ -269,7 +360,12 @@ .undplogocontainer { display: flex; justify-content: center; - padding: 20px; + padding-top: 40px; + + .undplogo { + width: 21.875rem; + height: 7.5rem; + } } } .aboutusicon { @@ -280,13 +376,16 @@ } .homepage-image-content-container { background-color: $outside-background-color; - // height: 35vh; padding-left: 60px; text-align: right; margin-top: 5px; @media (max-width: $lg-size) { } + .forest-image { + height: 33.75rem; + width: 52.5rem; + } ul { margin-top: 20px; @@ -296,15 +395,6 @@ line-height: 40px; } - // img { - // max-height: 50vh; - // min-height: 50vh; - - // @media (max-width: $lg-size) { - // max-height: 500px; - // } - // } - .title { font-size: 2rem; font-weight: 700; @@ -319,7 +409,6 @@ font-size: 2rem; margin-top: 1rem; color: $title-text-color; - // width: 100vw; line-height: 1.5em; } } @@ -328,7 +417,7 @@ font-size: 0.875rem; font-weight: 400; font-family: $primary-font-family; - color: $body-text-color; + color: $title-text-color; justify-content: left; margin: 20px 10px 20px 60px; @@ -336,14 +425,12 @@ font-size: 0.8rem; margin-top: 1rem; color: $title-text-color; - // width: 100vw; line-height: 1.5em; } } } .homepage-resources-content-container { background-color: $outside-background-color; - // height: 35vh; height: fit-content; padding-left: 60px; @@ -366,6 +453,8 @@ padding: 50px; justify-content: center; text-align: center; + width: 28.125rem; + height: 23.438rem; } .resource-image-container { display: flex; @@ -386,7 +475,6 @@ font-size: 2rem; margin-top: 1rem; color: $title-text-color; - // width: 100vw; line-height: 1.5em; } } @@ -394,98 +482,15 @@ font-size: 0.875rem; font-weight: 400; font-family: $primary-font-family; - color: $body-text-color; - // justify-content: left; + color: $title-text-color; margin: 20px 60px 20px 60px; @media (max-width: $lg-size) { font-size: 0.8rem; margin-top: 1rem; color: $title-text-color; - // width: 100vw; line-height: 1.5em; } } } - .homepage-footer-container { - background-color: $background-color-dark; - padding-top: 15px; - min-height: 200px; - height: 100%; - align-items: center; - // margin-bottom: 2vw; - // padding-left: 2rem; - padding: 0rem 2rem 0 1rem; - cursor: pointer; - - .footertext { - color: $dark-text-color; - padding: 5px 60px 20px 60px; - font-size: 0.875rem; - } - .footertext-bottom { - color: $dark-text-color; - padding: 100px 60px 28px 60px; - font-size: 0.875rem; - font-family: 'MuseoSans'; - font-weight: 100; - - .cc { - margin: 5px; - } - } - .footertext-link-container { - display: flex; - color: $dark-text-color; - padding: 100px 60px 20px 60px; - font-size: 0.75rem; - text-decoration: underline; - justify-content: right; - } - .footertext-links { - padding-left: 30px; - } - - .divider { - margin: 2px 60px 2px 60px; - width: calc(100% - 120px) !important; - min-width: calc(100% - 120px); - } - .logocontainer { - display: flex; - margin-top: 60px; - - .title { - font-family: 'MuseoSans'; - font-size: 2rem; - color: $dark-text-color; - font-weight: 700; - margin-right: 0.5rem; - } - - .title-sub { - font-weight: 100 !important; - font-family: 'MuseoSans'; - font-size: 2rem; - color: $dark-text-color; - - // padding-bottom: 3px; - } - - .logo { - height: 97px; - img { - object-fit: cover; - height: auto; - height: 70%; - margin-left: 60px; - } - } - .footer-country-name { - font-size: 0.875rem; - color: $dark-text-color; - font-family: 'MuseoSans'; - } - } - } } diff --git a/web/src/Pages/Homepage/homepage.tsx b/web/src/Pages/Homepage/homepage.tsx index bcc7e244c..d68b6cf66 100644 --- a/web/src/Pages/Homepage/homepage.tsx +++ b/web/src/Pages/Homepage/homepage.tsx @@ -1,32 +1,58 @@ -import { Button, Col, Divider, Form, Input, message, Row, Select, Statistic } from 'antd'; +import { Button, Col, Row } from 'antd'; import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import i18next from 'i18next'; import sliderLogo from '../../Assets/Images/logo-slider.png'; -import undpLogo from '../../Assets/Images/undp.png'; -import forest from '../../Assets/Images/forest.png'; -import resources from '../../Assets/Images/resources.png'; +import undpLogo from '../../Assets/Images/undp.webp'; +import undpLogofall from '../../Assets/Images/undp.png'; +import forest from '../../Assets/Images/forest.webp'; +import forestfall from '../../Assets/Images/forest.png'; +import resources from '../../Assets/Images/resources.webp'; +import resourcesfall from '../../Assets/Images/resources.png'; +import LayoutFooter from '../../Components/Footer/layout.footer'; +import ImgWithFallback from '../../Components/ImgwithFallback/ImgWithFallback'; import './homepage.scss'; -import { BarChart, Gem, Calculator, CcCircle } from 'react-bootstrap-icons'; +import { BarChart, Gem, Calculator } from 'react-bootstrap-icons'; const Homepage = () => { const { i18n, t } = useTranslation(['common', 'homepage']); const navigate = useNavigate(); + const [Visible, setVisible] = useState(true); + + const controlDownArrow = () => { + if (window.scrollY > 150) { + setVisible(false); + } else { + setVisible(true); + } + }; const handleLanguageChange = (lang: string) => { i18n.changeLanguage(lang); }; + const handleClickScroll = () => { + const element = document.getElementById('scrollhome'); + if (element) { + // 👇 Will scroll smoothly to the top of the next section + element.scrollIntoView({ behavior: 'smooth' }); + } + }; + useEffect(() => { if (localStorage.getItem('i18nextLng')!.length > 2) { i18next.changeLanguage('en'); } + window.addEventListener('scroll', controlDownArrow); + return () => { + window.removeEventListener('scroll', controlDownArrow); + }; }, []); return (
-
+
@@ -63,13 +89,26 @@ const Homepage = () => {
{t('homepage:lorem')}
+ + {Visible && ( + + )} +
-
{t('homepage:aboutustitle')}
+
+ {t('homepage:aboutustitle')} +
{t('homepage:aboutusline1')}
{t('homepage:Keyfeatures')}
@@ -133,7 +172,14 @@ const Homepage = () => { {t('homepage:aboutusline3')}
Developed in Partnership with
- undp logo + + {/* undp logo */}
@@ -156,7 +202,14 @@ const Homepage = () => {
- forest + + {/* forest */}
@@ -167,7 +220,14 @@ const Homepage = () => {
- resources + + {/* resources */}
{t('homepage:resource')}
@@ -183,63 +243,7 @@ const Homepage = () => {
-
- - -
-
- slider-logo -
-
-
-
{'CARBON'}
-
{'REGISTRY'}
-
-
- {process.env.COUNTRY_NAME || 'Antarctic Region'} -
-
-
- -
- - - -
{t('homepage:footertext1')}
- -
- - -
- {process.env.COUNTRY_NAME || 'Antarctic Region'} - -
- - -
navigate('/cookie')} className="footertext-links"> - {t('homepage:Cookie')} -
-
navigate('/codeconduct')} className="footertext-links"> - {t('homepage:codeconduct')} -
-
navigate('/terms')} className="footertext-links"> - {t('homepage:terms')} -
-
navigate('/privacy')} className="footertext-links"> - {t('homepage:privacy')} -
- -
-
- - {/* - -
-
Contact Us
-
{t('homepage:name')}
-
- -
*/} +
); }; diff --git a/web/src/Pages/Login/login.scss b/web/src/Pages/Login/login.scss index 1058ab8eb..e166f7b2d 100644 --- a/web/src/Pages/Login/login.scss +++ b/web/src/Pages/Login/login.scss @@ -22,7 +22,6 @@ font-size: 0.7rem; color: $logo-text-color; padding-left: 6px; - // padding-bottom: 3px; } .country-name { @@ -32,7 +31,6 @@ } .login-language-selection-container { - position: absolute; bottom: 14px; left: 2rem; @@ -66,13 +64,25 @@ .ant-select { width: 100px; - // margin-left: 10px; } } + .container-image { + background-image: + linear-gradient(to bottom, rgba(196, 196, 196, 0)0%, rgba(4, 104, 177, 0.6) 0.01%), + url('../../Assets/Images/factory.webp'); + background-image: linear-gradient( + to bottom, + rgba(196, 196, 196, 0) 0% + rgba(4, 104, 177, 0.6) 0.01% + ), + image-set( + '../../Assets/Images/factory.png' type('image/png'), + '../../Assets/Images/factory.webp' type('image/webp') + ); + background-size: cover; + } .login-img-container { - background: linear-gradient(180deg, rgba(196, 196, 196, 0) 0%, rgba(4, 104, 177, 0.6) 0.01%), - url('../../Assets/Images/factory.png') left 10% bottom 20% / cover no-repeat border-box; height: 100vh; background-position-x: 90%; background-position-y: 100%; @@ -88,7 +98,7 @@ justify-content: center; } - span { + .text-ctn { margin-left: 2vw; margin-bottom: 8vh; font-family: $secondary-font-family; @@ -101,7 +111,6 @@ font-size: 2.2rem; margin-top: 1rem; margin-bottom: unset; - // width: 100vw; line-height: 1.5em; } } @@ -112,10 +121,6 @@ align-items: center; display: flex; - @media (max-width: $lg-size) { - // width: 100vw;; - } - img { margin-top: 20px; object-fit: cover; @@ -137,7 +142,6 @@ text-align: center; } } - } .login-text-contents { @@ -145,12 +149,10 @@ flex-direction: column; margin-top: 5vh; - @media (max-width: $lg-size) { text-align: center; } - .login-text-signin { font-size: 2.4rem; line-height: 2rem; @@ -191,7 +193,6 @@ } .ant-form-item-explain-error { - // padding-left: 1rem; margin-bottom: 0.2rem; } @@ -272,7 +273,6 @@ height: 2.8rem; font-size: 0.8rem; border-radius: 0.3rem; - // border: 1px solid $line-colors; padding-left: 0.8rem; box-shadow: none; } @@ -300,8 +300,6 @@ } .ant-btn-primary { - // background-color: $login-red; - // color: $login-white; background: linear-gradient(to right, $gradient-background-start, $gradient-background-end); border: none; height: 2.4rem; @@ -314,6 +312,4 @@ border: none; color: $background-color; } - - -} \ No newline at end of file +} diff --git a/web/src/Pages/Login/login.tsx b/web/src/Pages/Login/login.tsx index c74f68a36..8ef9dbfd5 100644 --- a/web/src/Pages/Login/login.tsx +++ b/web/src/Pages/Login/login.tsx @@ -2,7 +2,6 @@ import { Button, Col, Form, Input, message, Row, Select } from 'antd'; import React, { useContext, useEffect, useState } from 'react'; import { useConnection } from '../../Context/ConnectionContext/connectionContext'; import './login.scss'; -// import sha1 from 'sha1'; import countryLogo from '../../Assets/Images/logo-slider.png'; import { useTranslation } from 'react-i18next'; import i18next from 'i18next'; @@ -77,11 +76,13 @@ const Login = () => {
-
- - {t('login:carbon')}
{t('login:credit')}
- {t('login:management')} -
+
+
+ + {t('login:carbon')}
{t('login:credit')}
+ {t('login:management')} +
+
@@ -90,7 +91,13 @@ const Login = () => {
- country-logo + country-logo { + navigate('/'); + }} + />
diff --git a/web/src/Pages/PrivacyPolicy/privacyPolicy.scss b/web/src/Pages/PrivacyPolicy/privacyPolicy.scss index 60a7d3e8a..9550fc1ab 100644 --- a/web/src/Pages/PrivacyPolicy/privacyPolicy.scss +++ b/web/src/Pages/PrivacyPolicy/privacyPolicy.scss @@ -52,22 +52,117 @@ font-family: 'MuseoSans'; } } - .privacytitle { - font-size: 2rem; - font-weight: 700; - font-family: $primary-font-family; - color: $title-text-color; - text-align: center; - justify-content: center; - text-transform: uppercase; - padding: 50px 50px 10px 50px; + .privacy-body-container { + text-align: left; + justify-content: left; + background-color: white; + margin-top: 5px; + margin-bottom: 5px; + padding-bottom: 60px; - @media (max-width: $lg-size) { + .privacy-body { + color: $title-text-color; + padding: 20px 200px 20px 200px; + } + p { + margin: 30px; + } + table { + text-align: left; + width: auto; + padding-top: 30px; + } + .table-row3 { + text-align: center; + } + .table-row1 { + padding: 20px; + } + .privacy-body-contact { + font-weight: 700; + color: $title-text-color; + text-align: left; + padding: 20px 200px 20px 200px; + } + .privacy-sub { + text-align: center; + justify-content: center; + padding-bottom: 30px; + font-weight: 700; + color: $title-text-color; + } + .privacytitle { font-size: 2rem; - margin-top: 1rem; + font-weight: 700; + font-family: $primary-font-family; + color: $title-text-color; + text-align: center; + justify-content: center; + text-transform: uppercase; + padding: 50px 50px 10px 50px; + + @media (max-width: $lg-size) { + font-size: 2rem; + margin-top: 1rem; + color: $title-text-color; + line-height: 1.5em; + } + } + .privacy-subtitle { + display: flex; + text-align: left; + justify-content: left; + background-color: rgba(255, 255, 255, 0.8); + color: $title-text-color; + padding-top: 30px; + font-size: 20px; + font-weight: 700; + padding-left: 200px; + } + .privacy-subtitle-summary { + display: flex; + text-align: left; + justify-content: left; + background-color: rgba(255, 255, 255, 0.8); + color: $title-text-color; + padding-top: 30px; + font-size: 20px; + font-weight: 700; + padding-left: 230px; + } + .privacy-subline { + padding-top: 20px; + padding-left: 200px; + font-weight: 700; color: $title-text-color; - // width: 100vw; - line-height: 1.5em; + font-size: 16px; + } + + .privacy-card-subtitle { + text-decoration: underline; + color: $title-text-color; + font-size: 20px; + font-weight: 700; + padding-bottom: 20px; + } + .privacy-card-subtitle-text { + margin: 10px 60px 10px 60px; + } + .privacy-card-container { + text-align: left; + margin: 30px 60px 30px 60px; + padding: 30px; + border-style: solid; + border-color: $common-form-input-border; + border-width: 1px; + table { + color: $title-text-color; + align-items: center; + + td { + padding-right: 20px; + } + } } } } diff --git a/web/src/Pages/PrivacyPolicy/privacyPolicy.tsx b/web/src/Pages/PrivacyPolicy/privacyPolicy.tsx index 00f04ee39..df3710deb 100644 --- a/web/src/Pages/PrivacyPolicy/privacyPolicy.tsx +++ b/web/src/Pages/PrivacyPolicy/privacyPolicy.tsx @@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next'; import i18next from 'i18next'; import sliderLogo from '../../Assets/Images/logo-slider.png'; import './privacyPolicy.scss'; +import { CcCircle } from 'react-bootstrap-icons'; +import LayoutFooter from '../../Components/Footer/layout.footer'; const PrivacyPolicy = () => { const { i18n, t } = useTranslation(['common', 'homepage']); const navigate = useNavigate(); @@ -19,9 +21,9 @@ const PrivacyPolicy = () => { } }, []); return ( -
+
- +
navigate('/')} className="privacy-header-container">
slider-logo @@ -36,11 +38,1131 @@ const PrivacyPolicy = () => {
- - -
This is Privacy Policy Page
- -
+
+ + +
PRIVACY NOTICE
+
Last updated February 02, 2023
+
+ This privacy notice for United Nations Development Programme ("Company",{' '} + "we", "us", and "our") describes how and why we might collect, + store, use, and/or share ("process")your information when you use our services + ("Services"), such as when you: +
    +
  • + Visit our website at iverifyit.com, or any website of ours that links to this + privacy notice +
  • +
  • + Engage with us in other related ways ― including any sales, marketing, or events +
  • +
+

+ Questions or concerns? Reading this privacy notice will help you understand + your privacy rights and choices. If you do not agree with our policies and + practices, please do not use our Services. If you still have any questions or + concerns, please contact us at nce.digital@undp.org. +

+
+ +
+ + +
SUMMARY OF KEY POINTS
+
+

+ + This summary provides key points from our privacy notice, but you can find out + more details about any of these topics by clicking the link following each key + point or by using our table of contents below to find the section you are looking + for. You can also click here to go directly to our table of + contents. + +

+

+ What personal information do we process?When you visit, use, or navigate our + Services, we may process personal information depending on how you interact with + United Nations Development Programme and the Services, the choices you make, and the + products and features you use. Click here to learn more. +

+

+ Do we process any sensitive personal information? We do not process sensitive + personal information. +

+

+ Do you receive any information from third parties? We do not receive any + information from third parties. +
+

+

+ How do you process my information? We process your information to provide, + improve, and administer our Services, communicate with you, for security and fraud + prevention, and to comply with law. We may also process your information for other + purposes with your consent. We process your information only when we have a valid + legal reason to do so. Click here to learn more. +

+

+ In what situations and with which parties do we share personal information?{' '} + We may share information in specific situations and with specific third parties. + Click here to learn more. +

+

+ How do we keep your information safe? We have organizational and technical + processes and procedures in place to protect your personal information. However, no + electronic transmission over the internet or information storage technology can be + guaranteed to be 100% secure, so we cannot promise or guarantee that hackers, + cybercriminals, or other unauthorized third parties will not be able to defeat our + security and improperly collect, access, steal, or modify your information.{' '} + Click here to learn more. +

+

+ What are your rights? Depending on where you are located geographically, the + applicable privacy law may mean you have certain rights regarding your personal + information. Click here to learn more. +

+

+ How do I exercise my rights? The easiest way to exercise your rights is by + filling out our data subject request form available + + {' '} + here + + , or by contacting us. We will consider and act upon any request in accordance with + applicable data protection laws. +

+

+ Want to learn more about what United Nations Development Programme does with any + information we collect? Click here to review the notice in full. +

+
+ +
+ + +
+ TABLE OF CONTENTS +
+
+
    +
  1. + {' '} + WHAT INFORMATION DO WE COLLECT? +
  2. +
  3. + {' '} + HOW DO WE PROCESS YOUR INFORMATION? +
  4. +
  5. + {' '} + + WHAT LEGAL BASES DO WE RELY ON TO PROCESS YOUR PERSONAL INFORMATION? + +
  6. +
  7. + {' '} + WHEN AND WITH WHOM DO WE SHARE YOUR PERSONAL INFORMATION? +
  8. +
  9. + {' '} + DO WE USE COOKIES AND OTHER TRACKING TECHNOLOGIES? +
  10. +
  11. + {' '} + HOW LONG DO WE KEEP YOUR INFORMATION? +
  12. +
  13. + {' '} + HOW DO WE KEEP YOUR INFORMATION SAFE? +
  14. +
  15. + {' '} + DO WE COLLECT INFORMATION FROM MINORS? +
  16. +
  17. + {' '} + WHAT ARE YOUR PRIVACY RIGHTS? +
  18. +
  19. + {' '} + CONTROLS FOR DO-NOT-TRACK FEATURES +
  20. +
  21. + {' '} + DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS? +
  22. +
  23. + {' '} + DO WE MAKE UPDATES TO THIS NOTICE? +
  24. +
  25. + {' '} + HOW CAN YOU CONTACT US ABOUT THIS NOTICE? +
  26. +
  27. + {' '} + + HOW CAN YOU REVIEW, UPDATE, OR DELETE THE DATA WE COLLECT FROM YOU? + +
  28. +
+
+ +
+ + +
+ 1. WHAT INFORMATION DO WE COLLECT? +
+
+

+ Personal information you disclose to us +

+

+ + In Short: We collect personal information that you provide to us + +

+

+ We collect personal information that you voluntarily provide to us when you register + on the Services, express an interest in obtaining information about us or our + products and Services, when you participate in activities on the Services, or + otherwise when you contact us. +

+

+ Personal Information Provided by You. The personal information that we + collect depends on the context of your interactions with us and the Services, the + choices you make, and the products and features you use. The personal information we + collect may include the following: +
+

+
    +
  • names
  • +
  • phone numbers
  • +
  • email addresses
  • +
  • usernames
  • +
  • passwords
  • +
  • contact preferences
  • +
+

+ Sensitive Information. We do not process sensitive information. +

+

+ All personal information that you provide to us must be true, complete, and + accurate, and you must notify us of any changes to such personal information. +

+

+ Information automatically collected +

+

+ + In Short: Some information — such as your Internet Protocol (IP) address + and/or browser and device characteristics — is collected automatically when you + visit our Services. + +

+

+ We automatically collect certain information when you visit, use, or navigate the + Services. This information does not reveal your specific identity (like your name or + contact information) but may include device and usage information, such as your IP + address, browser and device characteristics, operating system, language preferences, + referring URLs, device name, country, location, information about how and when you + use our Services, and other technical information. This information is primarily + needed to maintain the security and operation of our Services, and for our internal + analytics and reporting purposes. +

+

+ Like many businesses, we also collect information through cookies and similar + technologies. +

+

The information we collect includes:

+
    +
  • + Log and Usage Data. Log and usage data is service-related, diagnostic, + usage, and performance information our servers automatically collect when you + access or use our Services and which we record in log files. Depending on how you + interact with us, this log data may include your IP address, device information, + browser type, and settings and information about your activity in the Services + (such as the date/time stamps associated with your usage, pages and files viewed, + searches, and other actions you take such as which features you use), device event + information (such as system activity, error reports (sometimes called "crash + dumps"), and hardware settings). +
  • +
  • + Device Data. We collect device data such as information about your + computer, phone, tablet, or other device you use to access the Services. Depending + on the device used, this device data may include information such as your IP + address (or proxy server), device and application identification numbers, + location, browser type, hardware model, Internet service provider and/or mobile + carrier, operating system, and system configuration information. +
  • +
  • + Location Data. We collect location data such as information about your + device's location, which can be either precise or imprecise. How much information + we collect depends on the type and settings of the device you use to access the + Services. For example, we may use GPS and other technologies to collect + geolocation data that tells us your current location (based on your IP address). + You can opt out of allowing us to collect this information either by refusing + access to the information or by disabling your Location setting on your device. + However, if you choose to opt out, you may not be able to use certain aspects of + the Services. +
  • +
+
+ +
+ + +
+ 2. HOW DO WE PROCESS YOUR INFORMATION? +
+
+

+ + In Short: We process your information to provide, improve, and administer + our Services, communicate with you, for security and fraud prevention, and to + comply with law. We may also process your information for other purposes with your + consent. + +

+

+ + We process your personal information for a variety of reasons, depending on how + you interact with our Services, including: + +

+
    +
  • + + To facilitate account creation and authentication and otherwise manage user + accounts. + {' '} + We may process your information so you can create and log in to your account, as + well as keep your account in working order. +
  • +
  • + To request feedback. We may process your information when necessary to + request feedback and to contact you about your use of our Services. +
  • +
  • + To send you marketing and promotional communications. We may process the + personal information you send to us for our marketing purposes, if this is in + accordance with your marketing preferences. You can opt out of our marketing + emails at any time. For more information, see "WHAT ARE YOUR PRIVACY RIGHTS?" + below. +
  • +
  • + To deliver targeted advertising to you. We may process your information to + develop and display personalized content and advertising tailored to your + interests, location, and more. +
  • +
  • + To protect our Services. We may process your information as part of our + efforts to keep our Services safe and secure, including fraud monitoring and + prevention. +
  • +
  • + To identify usage trends. We may process information about how you use our + Services to better understand how they are being used so we can improve them. +
  • +
  • + To determine the effectiveness of our marketing and promotional campaigns.{' '} + We may process your information to better understand how to provide marketing and + promotional campaigns that are most relevant to you. +
  • +
  • + To save or protect an individual's vital interest. We may process your + information when necessary to save or protect an individual’s vital interest, such + as to prevent harm. +
  • +
+
+ +
+ + +
+ 3. WHAT LEGAL BASES DO WE RELY ON TO PROCESS YOUR INFORMATION? +
+
+

+ + In Short:We only process your personal information when we believe it is + necessary and we have a valid legal reason (i.e., legal basis) to do so under + applicable law, like with your consent, to comply with laws, to provide you with + services to enter into or fulfill our contractual obligations, to protect your + rights, or to fulfill our legitimate business interests. + +

+

+ + + If you are located in the EU or UK, this section applies to you. + + +

+

+ The General Data Protection Regulation (GDPR) and UK GDPR require us to explain the + valid legal bases we rely on in order to process your personal information. As such, + we may rely on the following legal bases to process your personal information: +

+
    +
  • + Consent. We may process your information if you have given us permission + (i.e., consent) to use your personal information for a specific purpose. You can + withdraw your consent at any time. Click here to + learn more. +
  • +
  • + Legitimate Interests. We may process your information when we believe it is + reasonably necessary to achieve our legitimate business interests and those + interests do not outweigh your interests and fundamental rights and freedoms. For + example, we may process your personal information for some of the purposes + described in order to: +
  • +
      +
    • + Send users information about special offers and discounts on our products and + services +
    • +
    • + Develop and display personalized and relevant advertising content for our users +
    • +
    • + Analyze how our services are used so we can improve them to engage and retain + users +
    • +
    • Support our marketing activities
    • +
    • Diagnose problems and/or prevent fraudulent activities
    • +
    • + Understand how our users use our products and services so we can improve user + experience +
    • +
    +
  • + Legal Obligations. We may process your information where we believe it is + necessary for compliance with our legal obligations, such as to cooperate with a + law enforcement body or regulatory agency, exercise or defend our legal rights, or + disclose your information as evidence in litigation in which we are involved. +
  • +
  • + Vital Interests. We may process your information where we believe it is + necessary to protect your vital interests or the vital interests of a third party, + such as situations involving potential threats to the safety of any person. +
  • +
+

+ In legal terms, we are generally the “data controller” under European data + protection laws of the personal information described in this privacy notice, since + we determine the means and/or purposes of the data processing we perform. This + privacy notice does not apply to the personal information we process as a “data + processor” on behalf of our customers. In those situations, the customer that we + provide services to and with whom we have entered into a data processing agreement + is the “data controller” responsible for your personal information, and we merely + process your information on their behalf in accordance with your instructions. If + you want to know more about our customers' privacy practices, you should read their + privacy policies and direct any questions you have to them. +

+

+ + + If you are located in Canada, this section applies to you. + + +

+

+ We may process your information if you have given us specific permission (i.e., + express consent) to use your personal information for a specific purpose, or in + situations where your permission can be inferred (i.e., implied consent). You can + withdraw your consent at any time. Click here to learn more. +

+

+ In some exceptional cases, we may be legally permitted under applicable law to + process your information without your consent, including, for example: +

+
    +
  • + If collection is clearly in the interests of an individual and consent cannot be + obtained in a timely way +
  • +
  • For investigations and fraud detection and prevention
  • +
  • For business transactions provided certain conditions are met
  • +
  • + If it is contained in a witness statement and the collection is necessary to + assess, process, or settle an insurance claim +
  • +
  • + For identifying injured, ill, or deceased persons and communicating with next of + kin +
  • +
  • + If we have reasonable grounds to believe an individual has been, is, or may be + victim of financial abuse +
  • +
  • + If it is reasonable to expect collection and use with consent would compromise the + availability or the accuracy of the information and the collection is reasonable + for purposes related to investigating a breach of an agreement or a contravention + of the laws of Canada or a province +
  • +
  • + If disclosure is required to comply with a subpoena, warrant, court order, or + rules of the court relating to the production of records +
  • +
  • + If it was produced by an individual in the course of their employment, business, + or profession and the collection is consistent with the purposes for which the + information was produced +
  • +
  • + If the collection is solely for journalistic, artistic, or literary purposes +
  • +
  • + If the information is publicly available and is specified by the regulations +
  • +
+
+ +
+ + +
+ 4. WHEN AND WITH WHOM DO WE SHARE YOUR PERSONAL INFORMATION? +
+
+

+ + In Short:We may share information in specific situations described in this + section and/or with the following third parties. + +

+

We may need to share your personal information in the following situations:

+
    +
  • + Business Transfers. We may share or transfer your information in connection + with, or during negotiations of, any merger, sale of company assets, financing, or + acquisition of all or a portion of our business to another company. +
  • +
+
+ +
+ + +
+ 5. DO WE USE COOKIES AND OTHER TRACKING TECHNOLOGIES? +
+
+

+ + In Short: We may use cookies and other tracking technologies to collect and + store your information. + +

+

+ We may use cookies and similar tracking technologies (like web beacons and pixels) + to access or store information. Specific information about how we use such + technologies and how you can refuse certain cookies is set out in our Cookie Notice. +

+
+ +
+ + +
+ 6. HOW LONG DO WE KEEP YOUR INFORMATION? +
+
+

+ + In Short: We keep your information for as long as necessary to fulfill the + purposes outlined in this privacy notice unless otherwise required by law. + +

+

+ We will only keep your personal information for as long as it is necessary for the + purposes set out in this privacy notice, unless a longer retention period is + required or permitted by law (such as tax, accounting, or other legal requirements). + No purpose in this notice will require us keeping your personal information for + longer than the period of time in which users have an account with us. +

+

+ When we have no ongoing legitimate business need to process your personal + information, we will either delete or anonymize such information, or, if this is not + possible (for example, because your personal information has been stored in backup + archives), then we will securely store your personal information and isolate it from + any further processing until deletion is possible. +

+
+ +
+ + +
+ 7. HOW DO WE KEEP YOUR INFORMATION SAFE? +
+
+

+ + In Short: We aim to protect your personal information through a system of + organizational and technical security measures. + +

+

+ We have implemented appropriate and reasonable technical and organizational security + measures designed to protect the security of any personal information we process. + However, despite our safeguards and efforts to secure your information, no + electronic transmission over the Internet or information storage technology can be + guaranteed to be 100% secure, so we cannot promise or guarantee that hackers, + cybercriminals, or other unauthorized third parties will not be able to defeat our + security and improperly collect, access, steal, or modify your information. Although + we will do our best to protect your personal information, transmission of personal + information to and from our Services is at your own risk. You should only access the + Services within a secure environment. +

+
+ +
+ + +
+ 8. DO WE COLLECT INFORMATION FROM MINORS? +
+
+

+ + In Short: We do not knowingly collect data from or market to children under + 18 years of age. + +

+

+ We do not knowingly solicit data from or market to children under 18 years of age. + By using the Services, you represent that you are at least 18 or that you are the + parent or guardian of such a minor and consent to such minor dependent’s use of the + Services. If we learn that personal information from users less than 18 years of age + has been collected, we will deactivate the account and take reasonable measures to + promptly delete such data from our records. If you become aware of any data we may + have collected from children under age 18, please contact us at info@panos.org.zm. +

+
+ +
+ + +
+ 9. WHAT ARE YOUR PRIVACY RIGHTS? +
+
+

+ + In Short: In some regions, such as the European Economic Area (EEA), United + Kingdom (UK), and Canada, you have rights that allow you greater access to and + control over your personal information. You may review, change, or terminate your + account at any time. + +

+

+ In some regions (like the EEA, UK, and Canada), you have certain rights under + applicable data protection laws. These may include the right (i) to request access + and obtain a copy of your personal information, (ii) to request rectification or + erasure; (iii) to restrict the processing of your personal information; and (iv) if + applicable, to data portability. In certain circumstances, you may also have the + right to object to the processing of your personal information. You can make such a + request by contacting us by using the contact details provided in the section “HOW + CAN YOU CONTACT US ABOUT THIS NOTICE?” below. +

+

+ We will consider and act upon any request in accordance with applicable data + protection laws. +

+

+ If you are located in the EEA or UK and you believe we are unlawfully processing + your personal information, you also have the right to complain to your local data + protection supervisory authority. You can find their contact details here: + + https://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm. + +

+

+ If you are located in Switzerland, the contact details for the data protection + authorities are available here: + + {' '} + https://www.edoeb.admin.ch/edoeb/en/home.html. + +

+ +
+

+ + Withdrawing your consent: + {' '} + If we are relying on your consent to process your personal information, which may + be express and/or implied consent depending on the applicable law, you have the + right to withdraw your consent at any time. You can withdraw your consent at any + time by contacting us by using the contact details provided in the section{' '} + HOW CAN YOU CONTACT US ABOUT THIS NOTICE?" below. +

+
+ +

+ However, please note that this will not affect the lawfulness of the processing + before its withdrawal, nor when applicable law allows, will it affect the processing + of your personal information conducted in reliance on lawful processing grounds + other than consent. +

+

+ + Opting out of marketing and promotional communications: + {' '} + You can unsubscribe from our marketing and promotional communications at any time by + clicking on the unsubscribe link in the emails that we send, replying “STOP” or + “UNSUBSCRIBE” to the SMS messages that we send, or by contacting us using the + details provided in the section{' '} + HOW CAN YOU CONTACT US ABOUT THIS NOTICE?" below. You will + then be removed from the marketing lists — however, we may still communicate with + you, for example to send you service-related messages that are necessary for the + administration and use of your account, to respond to service requests, or for other + non-marketing purposes. +

+

+ Account Information +

+

+ If you would at any time like to review or change the information in your account or + terminate your account, you can: +

+
    +
  • Log in to your account settings and update your user account.
  • +
+

+ Upon your request to terminate your account, we will deactivate or delete your + account and information from our active databases. However, we may retain some + information in our files to prevent fraud, troubleshoot problems, assist with any + investigations, enforce our legal terms and/or comply with applicable legal + requirements. +

+

+ + Cookies and similar technologies: + {' '} + Most Web browsers are set to accept cookies by default. If you prefer, you can + usually choose to set your browser to remove cookies and to reject cookies. If you + choose to remove cookies or reject cookies, this could affect certain features or + services of our Services. To opt out of interest-based advertising by advertisers on + our Services visit{' '} + http://www.aboutads.info/choices/. +

+
+ +
+ + +
+ 10. CONTROLS FOR DO-NOT-TRACK FEATURES +
+
+

+ Most web browsers and some mobile operating systems and mobile applications include + a Do-Not-Track ("DNT") feature or setting you can activate to signal your privacy + preference not to have data about your online browsing activities monitored and + collected. At this stage no uniform technology standard for recognizing and + implementing DNT signals has been finalized. As such, we do not currently respond to + DNT browser signals or any other mechanism that automatically communicates your + choice not to be tracked online. If a standard for online tracking is adopted that + we must follow in the future, we will inform you about that practice in a revised + version of this privacy notice. +

+
+ +
+ + +
+ 11. DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS? +
+
+

+ + In Short: Yes, if you are a resident of California, you are granted + specific rights regarding access to your personal information. + +

+

+ California Civil Code Section 1798.83, also known as the "Shine The Light" law, + permits our users who are California residents to request and obtain from us, once a + year and free of charge, information about categories of personal information (if + any) we disclosed to third parties for direct marketing purposes and the names and + addresses of all third parties with which we shared personal information in the + immediately preceding calendar year. If you are a California resident and would like + to make such a request, please submit your request in writing to us using the + contact information provided below. +

+

+ If you are under 18 years of age, reside in California, and have a registered + account with Services, you have the right to request removal of unwanted data that + you publicly post on the Services. To request removal of such data, please contact + us using the contact information provided below and include the email address + associated with your account and a statement that you reside in California. We will + make sure the data is not publicly displayed on the Services, but please be aware + that the data may not be completely or comprehensively removed from all our systems + (e.g., backups, etc.). +

+

+ CCPA Privacy Notice +

+

The California Code of Regulations defines a "resident" as:

+
    +
  1. + every individual who is in the State of California for other than a temporary or + transitory purpose and +
  2. +
  3. + every individual who is domiciled in the State of California who is outside the + State of California for a temporary or transitory purpose +
  4. +
+

All other individuals are defined as "non-residents."

+

+ If this definition of "resident" applies to you, we must adhere to certain rights + and obligations regarding your personal information. +

+

+ What categories of personal information do we collect? +

+

+ We have collected the following categories of personal information in the past + twelve (12) months: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryExamplesCollected
A. Identifiers + Contact details, such as real name, alias, postal address, telephone or mobile + contact number, unique personal identifier, online identifier, Internet Protocol + address, email address, and account name + YES
+ B. Personal information categories listed in the California Customer Records + statute + + Name, contact information, education, employment, employment history, and + financial information + YES
+ C. Protected classification characteristics under California or federal law + Gender and date of birthNO
D. Commercial information + Transaction information, purchase history, financial details, and payment + information + NO
E. Biometric informationFingerprints and voiceprintsNO
F. Internet or other similar network activity + Browsing history, search history, online behavior, interest data, and + interactions with our and other websites, applications, systems, and + advertisements + YES
G. Geolocation dataDevice locationYES
+ H. Audio, electronic, visual, thermal, olfactory, or similar information + + Images and audio, video or call recordings created in connection with our + business activities + NO
I. Professional or employment-related information + Business contact details in order to provide you our services at a business + level or job title, work history, and professional qualifications if you apply + for a job with us + NO
J. Education InformationStudent records and directory informationNO
+ K. Inferences drawn from other personal information + + Inferences drawn from any of the collected personal information listed above to + create a profile or summary about, for example, an individual’s preferences and + characteristics + NO
+

+ We may also collect other personal information outside of these categories instances + where you interact with us in person, online, or by phone or mail in the context of: +

+
    +
  • Receiving help through our customer support channels;
  • +
  • Participation in customer surveys or contests; and
  • +
  • + Facilitation in the delivery of our Services and to respond to your inquiries +
  • +
+

+ How do we use and share your personal information? +

+

+ + More information about our data collection and sharing practices can be found in + this privacy notice. + +

+

+ You may contact us Social Media Profiles, + or by referring to the contact details at the bottom of this document. +

+

+ If you are using an authorized agent to exercise your right to opt out we may deny a + request if the authorized agent does not submit proof that they have been validly + authorized to act on your behalf. +

+

+ Will your information be shared with anyone else? +

+

+ We may disclose your personal information with our service providers pursuant to a + written contract between us and each service provider. Each service provider is a + for-profit entity that processes the information on our behalf. +

+

+ We may use your personal information for our own business purposes, such as for + undertaking internal research for technological development and demonstration. This + is not considered to be "selling" of your personal information. +

+

+ United Nations Development Programme has not disclosed or sold any personal + information to third parties for a business or commercial purpose in the preceding + twelve (12) months. United Nations Development Programme will not sell personal + information in the future belonging to website visitors, users, and other consumers. +

+

+ Your rights with respect to your personal data +

+

+ Right to request deletion of the data — Request to delete +

+

+ You can ask for the deletion of your personal information. If you ask us to delete + your personal information, we will respect your request and delete your personal + information, subject to certain exceptions provided by law, such as (but not limited + to) the exercise by another consumer of his or her right to free speech, our + compliance requirements resulting from a legal obligation, or any processing that + may be required to protect against illegal activities. +

+

+ Right to be informed — Request to know +

+

Depending on the circumstances, you have a right to know:

+
    +
  • whether we collect and use your personal information;
  • +
  • the categories of personal information that we collect;
  • +
  • the purposes for which the collected personal information is used;
  • +
  • whether we sell your personal information to third parties;
  • +
  • + the categories of personal information that we sold or disclosed for a business + purpose; +
  • +
  • + the categories of third parties to whom the personal information was sold or + disclosed for a business purpose; and +
  • +
  • + the business or commercial purpose for collecting or selling personal information. +
  • +
+

+ In accordance with applicable law, we are not obligated to provide or delete + consumer information that is de-identified in response to a consumer request or to + re-identify individual data to verify a consumer request. +

+

+ Right to Non-Discrimination for the Exercise of a Consumer’s Privacy Rights +

+

We will not discriminate against you if you exercise your privacy rights.

+

+ Verification process +

+

+ Upon receiving your request, we will need to verify your identity to determine you + are the same person about whom we have the information in our system. These + verification efforts require us to ask you to provide information so that we can + match it with information you have previously provided us. For instance, depending + on the type of request you submit, we may ask you to provide certain information so + that we can match the information you provide with the information we already have + on file, or we may contact you through a communication method (e.g., phone or email) + that you have previously provided to us. We may also use other verification methods + as the circumstances dictate. +

+

+ We will only use personal information provided in your request to verify your + identity or authority to make the request. To the extent possible, we will avoid + requesting additional information from you for the purposes of verification. + However, if we cannot verify your identity from the information already maintained + by us, we may request that you provide additional information for the purposes of + verifying your identity and for security or fraud-prevention purposes. We will + delete such additionally provided information as soon as we finish verifying you. +

+

+ Other privacy rights +

+
    +
  • You may object to the processing of your personal information.
  • +
  • + You may request correction of your personal data if it is incorrect or no longer + relevant, or ask to restrict the processing of the information. +
  • +
  • + You can designate an authorized agent to make a request under the CCPA on your + behalf. We may deny a request from an authorized agent that does not submit proof + that they have been validly authorized to act on your behalf in accordance with + the CCPA. +
  • +
  • + You may request to opt out from future selling of your personal information to + third parties. Upon receiving an opt-out request, we will act upon the request as + soon as feasibly possible, but no later than fifteen (15) days from the date of + the request submission. +
  • +
+

+ To exercise these rights, you can contact us Social Media Profiles, or by referring + to the contact details at the bottom of this document. If you have a complaint about + how we handle your data, we would like to hear from you. +

+
+ +
+ + +
+ 12. DO WE MAKE UPDATES TO THIS NOTICE? +
+
+

+ + In Short: Yes, we will update this notice as necessary to stay compliant + with relevant laws. + +

+

+ We may update this privacy notice from time to time. The updated version will be + indicated by an updated "Revised" date and the updated version will be effective as + soon as it is accessible. If we make material changes to this privacy notice, we may + notify you either by prominently posting a notice of such changes or by directly + sending you a notification. We encourage you to review this privacy notice + frequently to be informed of how we are protecting your information. +

+
+ +
+ + +
+ 13. HOW CAN YOU CONTACT US ABOUT THIS NOTICE? +
+
+

+ If you have questions or comments about this notice, you may contact our Data + Protection Officer (DPO) by email at info@panos.org.zm, or by post to: +

+

+ We cannot guarantee the Site will be available at all times. We may experience + hardware, software, or other problems or need to perform maintenance related to the + Site, resulting in interruptions, delays, or errors. We reserve the right to change, + revise, update, suspend, discontinue, or otherwise modify the Site at any time or + for any reason without notice to you. You agree that we have no liability whatsoever + for any loss, damage, or inconvenience caused by your inability to access or use the + Site during any downtime or discontinuance of the Site. Nothing in these privacys of + Use will be construed to obligate us to maintain and support the Site or to supply + any corrections, updates, or releases in connection therewith. +

+

+ United Nations Development Programme +
+ 1 United Nations Plaza +
+ New York, New York, United States +

+

+ + If you are a resident in the European Economic Area, the "data controller" of your + personal information is United Nations Development Programme. United Nations + Development Programme has appointed DPO to be its representative in the EEA. You + can contact them directly regarding the processing of your information by United + Nations Development Programme, or by post to: + +

+
+ +
+ + +
+ 14. HOW CAN YOU REVIEW, UPDATE, OR DELETE THE DATA WE COLLECT FROM YOU? +
+
+

+ Based on the applicable laws of your country, you may have the right to request + access to the personal information we collect from you, change that information, or + delete it in some circumstances. To request to review, update, or delete your + personal information, please submit a request form by clicking{' '} + + here. + +

+

+ This privacy policy was created using Termly's{' '} + + Privacy Policy Generator. + +

+
+ +
+
+
); }; diff --git a/web/src/Pages/ProgrammeManagement/programmeManagement.tsx b/web/src/Pages/ProgrammeManagement/programmeManagement.tsx index 22d5460b7..7ec7a6f08 100644 --- a/web/src/Pages/ProgrammeManagement/programmeManagement.tsx +++ b/web/src/Pages/ProgrammeManagement/programmeManagement.tsx @@ -26,6 +26,7 @@ import { getStageEnumVal, getStageTagType, ProgrammeStage, + sumArray, } from '../../Definitions/InterfacesAndType/programme.definitions'; import { CheckboxChangeEvent } from 'antd/lib/checkbox'; import { useUserContext } from '../../Context/UserInformationContext/userInformationContext'; @@ -196,16 +197,29 @@ const ProgrammeManagement = () => { sorter: true, align: 'right' as const, render: (item: any) => { - return item ? addCommSep(Number(item)) : '-'; + return item ? addCommSep(sumArray(item)) : '-'; }, }, { title: t('programme:certifiers'), - dataIndex: 'certifier', - key: 'certifier', + dataIndex: 'certifierId', + key: 'certifierId', align: 'left' as const, + sorter: true, render: (item: any, itemObj: any) => { - const elements = item.map((obj: any) => { + if (item === null) { + return; + } + const cMap: any = {}; + for (const c of itemObj.certifier) { + cMap[c.companyId] = c; + } + + const elements = item.map((id: any) => { + const obj = cMap[id]; + if (!obj) { + return; + } return (
@@ -249,12 +263,15 @@ const ProgrammeManagement = () => { }); } - let sort; + let sort: any; if (sortOrder && sortField) { sort = { key: sortField, order: sortOrder, }; + if (sortField === 'certifierId') { + sort.nullFirst = sortOrder === 'ASC'; + } } else { sort = { key: 'programmeId', @@ -372,7 +389,12 @@ const ProgrammeManagement = () => { onPressEnter={onSearch} placeholder={'Search by programme name'} allowClear - onChange={(e) => setSearchText(e.target.value)} + // onChange={(e) => setSearchText(e.target.value)} + onChange={(e) => + e.target.value === '' + ? setSearch(e.target.value) + : setSearchText(e.target.value) + } onSearch={setSearch} style={{ width: 265 }} /> diff --git a/web/src/Pages/ProgrammeView/programmeView.scss b/web/src/Pages/ProgrammeView/programmeView.scss index a0e346857..60181e780 100644 --- a/web/src/Pages/ProgrammeView/programmeView.scss +++ b/web/src/Pages/ProgrammeView/programmeView.scss @@ -182,25 +182,25 @@ } .created-step { - background-color: rgba(#B0BEC5, 0.23); - color: #949599; + color: #FF8F56; + background-color: rgba(#FEF1AD, 0.5); font-size: 1rem; padding: 0.4rem 0.4rem 0.2rem 0.4rem; } .auth-step { color: #16B1FF; - background-color: rgba(#B9E2F4, 0.56); + background-color: rgba(#B9E2F4, 0.5); } .reject-step { - color: #FF4D4F; - background-color: rgba(#FFA6A6, 0.5); + color: #FF9900; + background-color: rgba(#FEB39F, 0.3); } .issue-step { - color: #FFA070; - background-color: rgba(#FFA070, 0.35); + color: #4F9D83; + background-color: rgba(#80FF00, 0.12); } .cert-step { @@ -210,23 +210,23 @@ .revoke-step { color: rgba(#FF00C7, 0.56); - background-color: rgba(#FF9FDE, 0.4); + background-color: rgba(#FF9FDE, 0.5); } .transfer-step { - color: #FF8F56; - background-color: rgba(#FEF1AD, 0.5); + background-color: rgba(#D7DFE2, 0.5); + color: #949599; } .retire-step { - color: #976ED7; - background-color: rgba(#B7A4FE, 0.5); + color: #FF4D4F; + background-color: rgba(#FFD3D3, 0.5); padding: 0.5rem 0.5rem 0.3rem 0.5rem; } .freeze-step { - color: rgba(#FF00C7, 0.56); - background-color: rgba(#FF9FDE, 0.4); + color: rgba(#976ED7, 1); + background-color: rgba(#B7A4FE, 0.3); } .region-list { diff --git a/web/src/Pages/ProgrammeView/programmeView.tsx b/web/src/Pages/ProgrammeView/programmeView.tsx index 28e1a857a..c5d99b7ce 100644 --- a/web/src/Pages/ProgrammeView/programmeView.tsx +++ b/web/src/Pages/ProgrammeView/programmeView.tsx @@ -44,6 +44,7 @@ import { addCommSep, addSpaces, CompanyRole, + CreditTransferStage, getFinancialFields, getGeneralFields, getRetirementTypeString, @@ -51,6 +52,8 @@ import { getStageTagType, Programme, ProgrammeStage, + RetireType, + sumArray, TxType, TypeOfMitigation, UnitField, @@ -82,6 +85,10 @@ import ProgrammeTransferForm from '../../Components/Models/ProgrammeTransferForm import ProgrammeRetireForm from '../../Components/Models/ProgrammeRetireForm'; import ProgrammeRevokeForm from '../../Components/Models/ProgrammeRevokeForm'; import OrganisationStatus from '../../Components/Organisation/OrganisationStatus'; +import Loading from '../../Components/Loading/Loading'; +import { CompanyState } from '../../Definitions/InterfacesAndType/companyManagement.definitions'; +import { ProgrammeTransfer } from '../../Casl/entities/ProgrammeTransfer'; +import TimelineBody from '../../Components/TimelineBody/TimelineBody'; mapboxgl.accessToken = 'pk.eyJ1IjoicGFsaW5kYSIsImEiOiJjbGMyNTdqcWEwZHBoM3FxdHhlYTN4ZmF6In0.KBvFaMTjzzvoRCr1Z1dN_g'; @@ -96,7 +103,7 @@ const ProgrammeView = () => { const [historyData, setHistoryData] = useState([]); const { i18n, t } = useTranslation(['view']); const [loadingHistory, setLoadingHistory] = useState(false); - const [loadingAll, setLoadingAll] = useState(false); + const [loadingAll, setLoadingAll] = useState(true); const mapContainerRef = useRef(null); const [openModal, setOpenModal] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false); @@ -134,15 +141,104 @@ const ProgrammeView = () => { const dt = [ numIsExist(d.creditEst) - numIsExist(d.creditIssued), numIsExist(d.creditIssued) - - numIsExist(d.creditTransferred) - - numIsExist(d.creditRetired) - + sumArray(d.creditTransferred) - + sumArray(d.creditRetired) - frozen, - numIsExist(d.creditTransferred), - numIsExist(d.creditRetired), + sumArray(d.creditTransferred), + sumArray(d.creditRetired), frozen, ]; return dt; }; + + const getCenter = (list: any[]) => { + let count = 0; + let lat = 0; + let long = 0; + for (const l of list) { + if (l === null || l === 'null') { + continue; + } + count += 1; + lat += l[0]; + long += l[1]; + } + return [lat / count, long / count]; + }; + + const drawMap = () => { + // const address = state.record?.programmeProperties.geographicalLocation.join(', ') || ''; + if (!mapContainerRef || !mapContainerRef.current) { + return; + } + setTimeout(async () => { + // let mapd: any = undefined; + + let mapd: any; + if (data?.geographicalLocationCordintes && data?.geographicalLocationCordintes.length > 0) { + mapd = new mapboxgl.Map({ + container: mapContainerRef.current || '', + style: 'mapbox://styles/mapbox/streets-v11', + center: getCenter(data?.geographicalLocationCordintes) as LngLatLike, + zoom: 4, + }); + + for (const iloc in data?.geographicalLocationCordintes) { + // const popup = new mapboxgl.Popup() + // .setText(state.record?.programmeProperties.geographicalLocation[iloc]) + // .addTo(mapd); + + if (data?.geographicalLocationCordintes[iloc] !== null) { + new mapboxgl.Marker({ + color: locationColors[(Number(iloc) + 1) % locationColors.length], + }) + .setLngLat(data?.geographicalLocationCordintes[iloc] as LngLatLike) + .addTo(mapd); + } + // .setPopup(popup); + } + } else { + for (const address of data!.programmeProperties.geographicalLocation) { + const response = await Geocoding({ accessToken: mapboxgl.accessToken }) + .forwardGeocode({ + query: address, + autocomplete: false, + limit: 1, + types: ['region', 'district'], + countries: [process.env.COUNTRY_CODE || 'NG'], + }) + .send(); + + if ( + !response || + !response.body || + !response.body.features || + !response.body.features.length + ) { + console.error('Invalid response:'); + console.error(response); + return; + } + const feature = response.body.features[0]; + if (mapContainerRef.current) { + if (mapd === undefined) { + mapd = new mapboxgl.Map({ + container: mapContainerRef.current || '', + style: 'mapbox://styles/mapbox/streets-v11', + center: feature.center as LngLatLike, + zoom: 4, + }); + } + + // const popup = new mapboxgl.Popup().setText(address).addTo(mapd); + new mapboxgl.Marker().setLngLat(feature.center as LngLatLike).addTo(mapd); + // .setPopup(popup); + } + } + } + }, 1000); + }; + const genPieData = (d: Programme) => { // ['Authorised', 'Issued', 'Transferred', 'Retired', 'Frozen'] @@ -160,9 +256,9 @@ const ProgrammeView = () => {
{isBase64(cert.logo) ? ( - + certifier logo ) : cert.logo ? ( - + certifier logo ) : cert.name ? (
{cert.name.charAt(0).toUpperCase()}
) : ( @@ -179,11 +275,206 @@ const ProgrammeView = () => { setCerts(c); }; - const getProgrammeHistory = async (programmeId: number) => { + const getProgrammeById = async (programmeId: number) => { + try { + const response: any = await post('national/programme/query', { + page: 1, + size: 2, + filterAnd: [ + { + key: 'programmeId', + operation: '=', + value: programmeId, + }, + ], + }); + if (response.data && response.data.length > 0) { + const d = response.data[0]; + setData(d); + navigate('.', { state: { record: d } }); + } + } catch (error: any) { + console.log('Error in getting programme', error); + message.open({ + type: 'error', + content: error.message, + duration: 3, + style: { textAlign: 'right', marginRight: 15, marginTop: 10 }, + }); + } + setLoadingAll(false); + }; + + const addElement = (e: any, time: number, hist: any) => { + if (!hist[time]) { + hist[time] = []; + } + hist[time].push(e); + }; + + const formatString = (langTag: string, vargs: any[]) => { + const str = t(langTag); + const parts = str.split('{}'); + let insertAt = 1; + for (const arg of vargs) { + parts.splice(insertAt, 0, arg); + insertAt += 2; + } + return parts.join(''); + }; + + const getTxActivityLog = (transfers: ProgrammeTransfer[], txDetails: any) => { + const hist: any = {}; + for (const transfer of transfers) { + txDetails[transfer.requestId!] = transfer; + const createdTime = Number(transfer.createdTime ? transfer.createdTime : transfer.txTime!); + let d: any; + if (!transfer.isRetirement) { + d = { + status: 'process', + title: t('view:tlInitTitle'), + subTitle: DateTime.fromMillis(createdTime).toFormat(dateTimeFormat), + description: ( + + ), + icon: ( + + + + ), + }; + } else { + d = { + status: 'process', + title: t('view:tlRetInit'), + subTitle: DateTime.fromMillis(createdTime).toFormat(dateTimeFormat), + description: ( + + ), + icon: ( + + + + ), + }; + } + + addElement(d, createdTime, hist); + + if ( + transfer.status === CreditTransferStage.Rejected || + transfer.status === CreditTransferStage.NotRecognised + ) { + const dx: any = { + status: 'process', + title: t(transfer.isRetirement ? 'view:tlRetRejectTitle' : 'view:tlRejectTitle'), + subTitle: DateTime.fromMillis(Number(transfer.txTime!)).toFormat(dateTimeFormat), + description: ( + + ), + icon: ( + + + + ), + }; + addElement(dx, Number(transfer.txTime!), hist); + } else if (transfer.status === CreditTransferStage.Cancelled) { + const systemCancel = transfer.txRef && transfer.txRef.indexOf('#SUSPEND_AUTO_CANCEL#') >= 0; + const dx: any = { + status: 'process', + title: t(transfer.isRetirement ? 'view:tlRetCancelTitle' : 'view:tlTxCancelTitle'), + subTitle: DateTime.fromMillis(Number(transfer.txTime!)).toFormat(dateTimeFormat), + description: ( + + ), + icon: ( + + + + ), + }; + addElement(dx, Number(transfer.txTime!), hist); + } + } + return hist; + }; + + const getProgrammeHistory = async (programmeId: string) => { setLoadingHistory(true); try { - const response: any = await get(`national/programme/getHistory?programmeId=${programmeId}`); + const historyPromise = get(`national/programme/getHistory?programmeId=${programmeId}`); + const transferPromise = get( + `national/programme/transfersByProgrammeId?programmeId=${programmeId}` + ); + + const [response, transfers] = await Promise.all([historyPromise, transferPromise]); + const txDetails: any = {}; + const txList = await getTxActivityLog(transfers.data, txDetails); const certifiedTime: any = {}; const activityList: any[] = []; for (const activity of response.data) { @@ -191,11 +482,16 @@ const ProgrammeView = () => { if (activity.data.txType === TxType.CREATE) { el = { status: 'process', - title: 'Programme Created', + title: t('view:tlCreate'), subTitle: DateTime.fromMillis(activity.data.txTime).toFormat(dateTimeFormat), - description: `The programme was created with a valuation of ${addCommSep( - activity.data.creditEst - )} ${creditUnit} credits.`, + description: ( + + ), icon: ( @@ -205,18 +501,21 @@ const ProgrammeView = () => { } else if (activity.data.txType === TxType.AUTH) { el = { status: 'process', - title: `Authorised`, + title: t('view:tlAuth'), subTitle: DateTime.fromMillis(activity.data.txTime).toFormat(dateTimeFormat), - description: `The programme was authorised for ${addCommSep( - activity.data.creditEst - )} ${creditUnit} credits until ${DateTime.fromMillis( - activity.data.endTime * 1000 - ).toFormat(dateFormat)} with the Serial Number ${ - activity.data.serialNo - } by the ${getTxRefValues(activity.data.txRef, 1)} via ${getTxRefValues( - activity.data.txRef, - 3 - )}`, + description: ( + + ), icon: ( @@ -226,14 +525,19 @@ const ProgrammeView = () => { } else if (activity.data.txType === TxType.ISSUE) { el = { status: 'process', - title: `Issued`, + title: t('view:tlIssue'), subTitle: DateTime.fromMillis(activity.data.txTime).toFormat(dateTimeFormat), - description: `The programme was issued ${addCommSep( - activity.data.creditChange - )} ${creditUnit} credits by the ${getTxRefValues( - activity.data.txRef, - 1 - )} via ${getTxRefValues(activity.data.txRef, 3)}`, + description: ( + + ), icon: ( @@ -243,12 +547,15 @@ const ProgrammeView = () => { } else if (activity.data.txType === TxType.REJECT) { el = { status: 'process', - title: `Rejected`, + title: t('view:tlReject'), subTitle: DateTime.fromMillis(activity.data.txTime).toFormat(dateTimeFormat), - description: `The programme was rejected by the ${getTxRefValues( - activity.data.txRef, - 1 - )} via ${getTxRefValues(activity.data.txRef, 3)}`, + description: ( + + ), icon: ( @@ -258,17 +565,21 @@ const ProgrammeView = () => { } else if (activity.data.txType === TxType.TRANSFER) { el = { status: 'process', - title: `Credit Transferred`, + title: t('view:tlTransfer'), subTitle: DateTime.fromMillis(activity.data.txTime).toFormat(dateTimeFormat), - description: `${addCommSep( - activity.data.creditChange - )} ${creditUnit} credits of this programme were transferred to ${getTxRefValues( - activity.data.txRef, - 5 - )} by ${getTxRefValues(activity.data.txRef, 1)} via ${getTxRefValues( - activity.data.txRef, - 3 - )}`, + description: ( + + ), icon: ( @@ -276,14 +587,25 @@ const ProgrammeView = () => { ), }; } else if (activity.data.txType === TxType.REVOKE) { + const type = getTxRefValues(activity.data.txRef, 4); + let revokeComp = undefined; + if (type === 'SUSPEND_REVOKE') { + revokeComp = getTxRefValues(activity.data.txRef, 5); + } el = { status: 'process', - title: `Certification Revoked`, + title: t('view:tlRevoke'), subTitle: DateTime.fromMillis(activity.data.txTime).toFormat(dateTimeFormat), - description: `The certification of this programme was revoked by ${getTxRefValues( - activity.data.txRef, - 1 - )} via ${getTxRefValues(activity.data.txRef, 3)}`, + description: ( + + ), icon: ( @@ -293,12 +615,15 @@ const ProgrammeView = () => { } else if (activity.data.txType === TxType.CERTIFY) { el = { status: 'process', - title: `Certified`, + title: t('view:tlCertify'), subTitle: DateTime.fromMillis(activity.data.txTime).toFormat(dateTimeFormat), - description: `The programme was certified by ${getTxRefValues( - activity.data.txRef, - 1 - )} via ${getTxRefValues(activity.data.txRef, 3)}`, + description: ( + + ), icon: ( @@ -310,18 +635,27 @@ const ProgrammeView = () => { certifiedTime[cid] = DateTime.fromMillis(activity.data.txTime).toFormat('dd LLLL yyyy'); } } else if (activity.data.txType === TxType.RETIRE) { + const reqID = getTxRefValues(activity.data.txRef, 7); + const tx = reqID ? txDetails[reqID!] : undefined; + const crossCountry = tx ? tx.toCompanyMeta?.countryName : undefined; el = { status: 'process', - title: `Retired`, + title: t('view:tlRetire'), subTitle: DateTime.fromMillis(activity.data.txTime).toFormat(dateTimeFormat), - description: `${addCommSep( - activity.data.creditChange - )} ${creditUnit} credits of this programme were retired as ${getRetirementTypeString( - getTxRefValues(activity.data.txRef, 5) - )?.toLowerCase()} by ${getTxRefValues(activity.data.txRef, 1)} via ${getTxRefValues( - activity.data.txRef, - 3 - )}`, + description: ( + + ), icon: ( @@ -331,44 +665,69 @@ const ProgrammeView = () => { } else if (activity.data.txType === TxType.FREEZE) { el = { status: 'process', - title: `Credits freezed`, + title: t('view:tlFrozen'), subTitle: DateTime.fromMillis(activity.data.txTime).toFormat(dateTimeFormat), - description: `${addCommSep( - activity.data.creditFrozen.reduce((a: any, b: any) => a + b, 0) - )} credits were frozen due to the deactivation of ${getTxRefValues( - activity.data.txRef, - 4 - )} by ${getTxRefValues(activity.data.txRef, 1)} via ${getTxRefValues( - activity.data.txRef, - 3 - )}`, + description: ( + a + b, 0)), + creditUnit, + getTxRefValues(activity.data.txRef, 4), + getTxRefValues(activity.data.txRef, 1), + ])} + remark={getTxRefValues(activity.data.txRef, 3)} + via={activity.data.userName} + /> + ), icon: ( - + ), }; - } else { + } else if (activity.data.txType === TxType.UNFREEZE) { el = { status: 'process', - title: activity.data.currentStage, + title: t('view:tlUnFrozen'), subTitle: DateTime.fromMillis(activity.data.txTime).toFormat(dateTimeFormat), - description: ``, + description: ( + + ), icon: ( - - + + ), }; } if (el) { + const toDelete = []; + for (const txT in txList) { + if (activity.data.txTime > txT) { + activityList.unshift(...txList[txT]); + toDelete.push(txT); + } else { + break; + } + } + toDelete.forEach((e) => delete txList[e]); activityList.unshift(el); } } + for (const txT in txList) { + activityList.unshift(...txList[txT]); + } + setHistoryData(activityList); setLoadingHistory(false); setCertTimes(certifiedTime); @@ -383,6 +742,7 @@ const ProgrammeView = () => { }); setLoadingHistory(false); } + return null; }; const updateProgrammeData = (response: any) => { @@ -437,7 +797,7 @@ const ProgrammeView = () => { } else { error = response.message; } - await getProgrammeHistory(Number(data?.programmeId)); + await getProgrammeHistory(data?.programmeId as string); return error; } catch (e: any) { error = e.message; @@ -526,7 +886,7 @@ const ProgrammeView = () => { error = response.message; } - await getProgrammeHistory(Number(data?.programmeId)); + await getProgrammeHistory(data?.programmeId as string); setConfirmLoading(false); return error; @@ -564,84 +924,26 @@ const ProgrammeView = () => { console.log(state); navigate('/programmeManagement', { replace: true }); } else { - getProgrammeHistory(state.record.programmeId); - setData(state.record); - - // const address = state.record?.programmeProperties.geographicalLocation.join(', ') || ''; - setTimeout(async () => { - // let mapd: any = undefined; - - let mapd: any; - if ( - state.record?.geographicalLocationCordintes && - state.record?.geographicalLocationCordintes.length > 0 - ) { - mapd = new mapboxgl.Map({ - container: mapContainerRef.current || '', - style: 'mapbox://styles/mapbox/streets-v11', - center: state.record?.geographicalLocationCordintes[0] as LngLatLike, - zoom: 4, - }); - - for (const iloc in state.record?.geographicalLocationCordintes) { - // const popup = new mapboxgl.Popup() - // .setText(state.record?.programmeProperties.geographicalLocation[iloc]) - // .addTo(mapd); - - if (state.record?.geographicalLocationCordintes[iloc] !== null) { - new mapboxgl.Marker({ - color: locationColors[locationColors.length % (Number(iloc) + 1)], - }) - .setLngLat(state.record?.geographicalLocationCordintes[iloc] as LngLatLike) - .addTo(mapd); - } - // .setPopup(popup); - } - } else { - for (const address of state.record?.programmeProperties.geographicalLocation) { - const response = await Geocoding({ accessToken: mapboxgl.accessToken }) - .forwardGeocode({ - query: address, - autocomplete: false, - limit: 1, - types: ['region', 'district'], - countries: [process.env.COUNTRY_CODE || 'NG'], - }) - .send(); - - if ( - !response || - !response.body || - !response.body.features || - !response.body.features.length - ) { - console.error('Invalid response:'); - console.error(response); - return; - } - const feature = response.body.features[0]; - if (mapContainerRef.current) { - if (mapd === undefined) { - mapd = new mapboxgl.Map({ - container: mapContainerRef.current || '', - style: 'mapbox://styles/mapbox/streets-v11', - center: feature.center as LngLatLike, - zoom: 4, - }); - } - - // const popup = new mapboxgl.Popup().setText(address).addTo(mapd); - new mapboxgl.Marker().setLngLat(feature.center as LngLatLike).addTo(mapd); - // .setPopup(popup); - } - } + if (!state.record) { + if (state.id) { + getProgrammeById(state.id); } - }, 1000); + } else { + setLoadingAll(false); + setData(state.record); + } } }, []); + useEffect(() => { + if (data) { + getProgrammeHistory(data.programmeId); + drawMap(); + } + }, [data]); + if (!data) { - return
; + return ; } const pieChartData = getPieChartData(data); @@ -664,9 +966,9 @@ const ProgrammeView = () => {
{isBase64(ele.company.logo) ? ( - + company logo ) : ele.company.logo ? ( - + company logo ) : ele.company.name ? (
{ele.company.name.charAt(0).toUpperCase()}
) : ( @@ -960,7 +1262,7 @@ const ProgrammeView = () => { calculations.constantVersion = data.constantVersion; return loadingAll ? ( - + ) : (
@@ -990,7 +1292,7 @@ const ProgrammeView = () => {
{elements}
- {data.currentStage !== ProgrammeStage.AwaitingAuthorization ? ( + {getStageEnumVal(data.currentStage) === ProgrammeStage.Authorised ? (
@@ -1085,9 +1387,11 @@ const ProgrammeView = () => { 0 && (
{(userInfoState?.companyRole === CompanyRole.GOVERNMENT || - data.companyId + (data.companyId .map((e) => Number(e)) - .includes(userInfoState!.companyId)) && ( + .includes(userInfoState!.companyId) && + userInfoState!.companyState !== + CompanyState.SUSPENDED.valueOf())) && ( + {userInfoState?.userRole !== Role.Root && ( + + )}