diff --git a/.env.local b/.env.local index fb79879b..548fde8a 100644 --- a/.env.local +++ b/.env.local @@ -13,31 +13,34 @@ HASHKEY = 028ffbae928d7191c0017f298260995f901d78eabde1436c0af0423459cc3715832136 # CRMURL = "devcrm2016.llcdt.gov.wales/DVPINS" # //New Dev oauth ESNR -# CLIENT_ID = 9b834097-03d7-409b-b038-ecfec31a394a -# CLIENT_SECRET = .Jf8Q~tKMObCJjhh_hy3J5AIctFrBalU4FS~tdxk -# RELAY_ROOT = https://ar-ns-lz-pedw-dev-uks-01.servicebus.windows.net/ar-hc-lz-pedw-dev-uks-01/ -# RELAYURI = "ar-ns-lz-pedw-dev-uks-01.servicebus.windows.net" -# RELAYPATH = "ar-hc-lz-pedw-dev-uks-01" -# CRMURL = "devcrm2016.llcdt.gov.wales/DVPINS" +CLIENT_ID = 9b834097-03d7-409b-b038-ecfec31a394a +CLIENT_SECRET = .Jf8Q~tKMObCJjhh_hy3J5AIctFrBalU4FS~tdxk +RELAY_ROOT = https://ar-ns-lz-pedw-dev-uks-01.servicebus.windows.net/ar-hc-lz-pedw-dev-uks-01/ +RELAYURI = "ar-ns-lz-pedw-dev-uks-01.servicebus.windows.net" +RELAYPATH = "ar-hc-lz-pedw-dev-uks-01" +CRMURL = "devcrm2016.llcdt.gov.wales/DVPINS" +CRMURL_VERSION = "v8.2" # //New Test -# CLIENT_ID = d40b9373-96c2-4b04-8bd8-c2a2fe6444c1 -# CLIENT_SECRET = 6hT8Q~glL~jloWpGmvXw3UY37QFwjUE0r._jobEM -# RELAY_ROOT = https://ar-ns-lz-pedw-test-uks-01.servicebus.windows.net/ar-hc-lz-pedw-test-uks-01/ -# RELAYURI = "ar-ns-lz-pedw-test-uks-01.servicebus.windows.net" -# RELAYPATH = "ar-hc-lz-pedw-test-uks-01" -# CRMURL = "tscrm2016.llcdt.gov.wales/UATPINS" +CLIENT_ID = d40b9373-96c2-4b04-8bd8-c2a2fe6444c1 +CLIENT_SECRET = 6hT8Q~glL~jloWpGmvXw3UY37QFwjUE0r._jobEM +RELAY_ROOT = https://ar-ns-lz-pedw-test-uks-01.servicebus.windows.net/ar-hc-lz-pedw-test-uks-01/ +RELAYURI = "ar-ns-lz-pedw-test-uks-01.servicebus.windows.net" +RELAYPATH = "ar-hc-lz-pedw-test-uks-01" +CRMURL = "tscrm2016.llcdt.gov.wales/UATPINS" +CRMURL_VERSION = "v8.2" -# //New PreProd -CLIENT_ID = ab6c4678-b31a-4eb3-b429-e039120e488a -CLIENT_SECRET = V288Q~P1lFMvvX9Xnkx2pLixK~GOREH983Owqc1E -RELAY_ROOT = https://ar-ns-lz-pedw-ppe-uks-01.servicebus.windows.net/ar-hc-lz-pedw-ppe-uks-01/ -RELAYURI = "ar-ns-lz-pedw-ppe-uks-01.servicebus.windows.net" -RELAYPATH = "ar-hc-lz-pedw-ppe-uks-01" -# RELAY_ROOT = https://ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net/ar-hc-lz-pedw-ppe-ukw-01/ -# RELAYURI = "ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net" -# RELAYPATH = "ar-hc-lz-pedw-ppe-ukw-01" -CRMURL = "ppcrm2016.llcdt.gov.wales/PPPINSWales" +# # //New PreProd +# CLIENT_ID = ab6c4678-b31a-4eb3-b429-e039120e488a +# CLIENT_SECRET = V288Q~P1lFMvvX9Xnkx2pLixK~GOREH983Owqc1E +# RELAY_ROOT = https://ar-ns-lz-pedw-ppe-uks-01.servicebus.windows.net/ar-hc-lz-pedw-ppe-uks-01/ +# RELAYURI = "ar-ns-lz-pedw-ppe-uks-01.servicebus.windows.net" +# RELAYPATH = "ar-hc-lz-pedw-ppe-uks-01" +# # RELAY_ROOT = https://ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net/ar-hc-lz-pedw-ppe-ukw-01/ +# # RELAYURI = "ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net" +# # RELAYPATH = "ar-hc-lz-pedw-ppe-ukw-01" +# CRMURL = "ppcrm2016.llcdt.gov.wales/PPPINSWales" +# CRMURL_VERSION = "v8.2" # //Prod Oauth WGO @@ -48,6 +51,57 @@ CRMURL = "ppcrm2016.llcdt.gov.wales/PPPINSWales" # RELAYPATH = "prod-pedw-hc" # CRMURL = "crm2016.llc.gov.wales/PEDW" +# //New Prod +# CLIENT_ID = 3e4141a3-1fbd-454e-98f0-7f3a4f14a3cc +# CLIENT_SECRET = tFk8Q~E.R-HFjROTu1sRKqwb_ONzy05uPYoF6awR +# RELAY_ROOT = https://ar-ns-lz-pedw-prod-uks-01.servicebus.windows.net/ar-hc-lz-pedw-prod-uks-01/ +# RELAYURI = "ar-ns-lz-pedw-prod-uks-01.servicebus.windows.net" +# RELAYPATH = "ar-hc-lz-pedw-prod-uks-01" +# # RELAY_ROOT = https://ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net/ar-hc-lz-pedw-prod-ukw-01/ +# # RELAYURI = "ar-ns-lz-pedw-prod-ukw-01.servicebus.windows.net" +# # RELAYPATH = "ar-hc-lz-pedw-prod-ukw-01" +# CRMURL = "crm2016.llc.gov.wales/PEDW" +# CRMURL_VERSION = "v8.2" + + + +# D365 cnr WGO +# CLIENT_ID = 3125ac03-91ac-46a7-a166-0086f0ed90b3 +# CLIENT_SECRET = bFq8Q~9VX5Ra3lKnGGIXsB.G6-ZGMx7LqyqXjdr4 +# RELAY_ROOT = https://ar-ns-lz-pedw-d365cnr-uks-01.servicebus.windows.net/ar-hc-lz-pedw-d365cnr-uks-01/ +# RELAYURI = "ar-ns-lz-pedw-d365cnr-uks-01.servicebus.windows.net" +# RELAYPATH = "ar-hc-lz-pedw-d365cnr-uks-01" +# CRMURL = "dv-pedw.crm11.dynamics.com" +# CRMURL_VERSION = "v9.2" + +# D365 Dev WGO +# CLIENT_ID = 9b834097-03d7-409b-b038-ecfec31a394a +# CLIENT_SECRET = .Jf8Q~tKMObCJjhh_hy3J5AIctFrBalU4FS~tdxk +# RELAY_ROOT = https://ar-ns-lz-pedw-d365dev-uks-01.servicebus.windows.net/ar-hc-lz-pedw-d365dev-uks-01/ +# RELAYURI = "ar-ns-lz-pedw-d365dev-uks-01.servicebus.windows.net" +# RELAYPATH = "ar-hc-lz-pedw-d365dev-uks-01" +# CRMURL = "dv-pedw.crm11.dynamics.com" +# CRMURL_VERSION = "v9.2" + +# D365 Test WGO +# CLIENT_ID = d40b9373-96c2-4b04-8bd8-c2a2fe6444c1 +# CLIENT_SECRET = 6hT8Q~glL~jloWpGmvXw3UY37QFwjUE0r._jobEM +# RELAY_ROOT = https://ar-ns-lz-pedw-d365test-uks-01.servicebus.windows.net/ar-hc-lz-pedw-d365test-uks-01/ +# RELAYURI = "ar-ns-lz-pedw-d365test-uks-01.servicebus.windows.net" +# RELAYPATH = "ar-hc-lz-pedw-d365test-uks-01" +# CRMURL = "ts-pedw.crm11.dynamics.com" +# CRMURL_VERSION = "v9.2" + +# D365 PP WGO +# CLIENT_ID = ab6c4678-b31a-4eb3-b429-e039120e488a +# CLIENT_SECRET = V288Q~P1lFMvvX9Xnkx2pLixK~GOREH983Owqc1E +# RELAY_ROOT = https://ar-ns-lz-pedw-d365ppe-uks-01.servicebus.windows.net/ar-hc-lz-pedw-d365ppe-uks-01/ +# RELAYURI = "ar-ns-lz-pedw-d365ppe-uks-01.servicebus.windows.net" +# RELAYPATH = "ar-hc-lz-pedw-d365ppe-uks-01" +# CRMURL = "wg-pp-pedw.crm11.dynamics.com" +# CRMURL_VERSION = "v9.2" + + //GOOGLE_TAG_MANAGER = GTM-T78CBC3 GOOGLE_TAG_MANAGER = G-GTWW3JT03Z @@ -58,8 +112,9 @@ SHOWMAPS = false API_ROOT = "http://localhost:3000" CY_API_ROOT = "http://cymru.local:3000" I18N_DOMAIN = "cymru.local" -DOCAPI_OFFLINE = "12:00:00,14:00:00" -SHOWFILTERDOCS = false +DOCAPI_OFFLINE = "07:00:00,14:00:00" +# DOCAPI_OFFLINE = "01:00:00,04:00:00" +SHOWFILTERDOCS = true // Next Auth vars @@ -68,18 +123,35 @@ NEXTAUTH_URL_INTERNAL = $API_ROOT NEXTAUTH_SECRET = SuperSecret // CNR sql server -# DATABASE_URL = sqlserver://sql-srv-plt-pedw-cnr-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword +DATABASE_URL = sqlserver://sql-srv-plt-pedw-cnr-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword // dev test sql server # DATABASE_URL = sqlserver://pedw-dev-test-sql-svr.database.windows.net:1433;database=PEDWDev;user=RobBondSQL;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword -# DATABASE_URL = sqlserver://sql-srv-plt-pedw-dev-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword + DATABASE_URL = sqlserver://sql-srv-plt-pedw-dev-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword + +// test sql server + DATABASE_URL = sqlserver://sql-srv-plt-pedw-test-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword + // Pre prod sql server -DATABASE_URL = sqlserver://sql-srv-plt-pedw-ppe-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustservercertificate=true;hostnameincertificate=*.database.windows.net;logintimeout=30;authentication=SqlPassword +# DATABASE_URL = sqlserver://sql-srv-plt-pedw-ppe-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustservercertificate=true;hostnameincertificate=*.database.windows.net;logintimeout=30;authentication=SqlPassword // Prod sql server # DATABASE_URL = sqlserver://pp-pedw-sql.database.windows.net:1433;database=PEDWPreProd;user=RobBondSQL;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword +// Prod sql server +# DATABASE_URL = sqlserver://sql-srv-plt-pedw-prod-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword + +// D365 CNR sql server +# DATABASE_URL = sqlserver://sql-srv-plt-pedw-d365cnr-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword + +// D365 Test sql server +# DATABASE_URL = sqlserver://sql-srv-plt-pedw-d365test-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword + + +// D365 PPE sql server +# DATABASE_URL = sqlserver://sql-srv-plt-pedw-d365ppe-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword + SECRET = SuperSecret @@ -91,24 +163,24 @@ AZURE_TENANT_ID = $TENANT AZURE_CLIENT_SECRET = $CLIENT_SECRET // CNR key -# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwcnruks" -# AZURE_STORAGE_ACCOUNT_KEY = hRffHq4IwctpaTKdS/U33Xq6nyTDF1t++WJgIwsw7gzstktZin/rRwbTIAL9KKqesVsK7EHrTTG6+ASt4+7skw== -# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwcnruks.blob.core.windows.net +AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwcnruks" +AZURE_STORAGE_ACCOUNT_KEY = hRffHq4IwctpaTKdS/U33Xq6nyTDF1t++WJgIwsw7gzstktZin/rRwbTIAL9KKqesVsK7EHrTTG6+ASt4+7skw== +AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwcnruks.blob.core.windows.net // new dev key -# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwdevuks" -# AZURE_STORAGE_ACCOUNT_KEY = MmrsrCjBfop9pI2oImsP/+lJUWU2DrXsAB8nq5bOVROTmPovSGxjCjKw9+TAFr00k8WDUK9r6mVx+AStcMLyAA== -# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwdevuks.blob.core.windows.net +AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwdevuks" +AZURE_STORAGE_ACCOUNT_KEY = MmrsrCjBfop9pI2oImsP/+lJUWU2DrXsAB8nq5bOVROTmPovSGxjCjKw9+TAFr00k8WDUK9r6mVx+AStcMLyAA== +AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwdevuks.blob.core.windows.net // New test key # AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwtestuks" -# AZURE_STORAGE_ACCOUNT_KEY = bqJAz5pYQVGee8dnVhlsqW+xHwinNTjgY2AmojNyB+d4uAotMR20WcO4Op13yB+9dFaXAkkjtIA4+AStHT/IYg== +# AZURE_STORAGE_ACCOUNT_KEY = 5NdDO9GGlbTFLHFOXZRw8H2C0Ow5bFTvHwUHEbStyzg2mbd1uwHSeBSvM/dkN+HfdwrFzqAkwL5P+AStdO6cig== # AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwtestuks.blob.core.windows.net // New preprod key -AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwppeuks" -AZURE_STORAGE_ACCOUNT_KEY = B4GRKr+cA9b9mNM/GHFivzhzflJIZltG8bMmooXlYvGYySPAspTTHistAT30sHipnR+hh6dqUc9q+ASttAjPRQ== -AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwppeuks.blob.core.windows.net +# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwppeuks" +# AZURE_STORAGE_ACCOUNT_KEY = B4GRKr+cA9b9mNM/GHFivzhzflJIZltG8bMmooXlYvGYySPAspTTHistAT30sHipnR+hh6dqUc9q+ASttAjPRQ== +# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwppeuks.blob.core.windows.net // PROD key @@ -116,6 +188,34 @@ AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwppeuks.blob.core.windows.net # AZURE_STORAGE_ACCOUNT_KEY = koZ46STRqapSa2Dod8MLL6aju1eYLhhyJ6DXpCLMTnFLJvbDT6CHUv/jwr5U/Ta8a60MWzwToHQy+AStADO7sg== # AZURE_PEDW_STORAGE_ENDPOINT = https://pedwprod.blob.core.windows.net +# // New PROD key +# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwproduks" +# AZURE_STORAGE_ACCOUNT_KEY = OfV9MFqXcLLPam9GhEdMWadIOwIzhc5hd5TUqzIuQoswFdtV4ybaemjatSv+Kht0Ny1qQKjLplwY+AStUTZJrg== +# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwproduks.blob.core.windows.net + +# D365 CNR key +# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwd365cnruks" +# AZURE_STORAGE_ACCOUNT_KEY = nEwCp+4FuVf/9a4DOyKuQO2yRRV4Ypigg20ch1YaiL09YmEhWMEO5pNLpYtI3WnQCkIqeu1CUqwM+AStCLW01g== +# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwd365cnruks.blob.core.windows.net + + +# D365 DEV key +# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwd365devuks" +# AZURE_STORAGE_ACCOUNT_KEY = XzwQxuZXSwYrVgemDUD2ymTpYTIfWqhypbmEALw1ZEEP0M4D3b3Gh28clSymxwFu7Gu9mhwkW4/V+AStZ6RF8Q== +# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwd365devuks.blob.core.windows.net + + +# D365 TEST key +# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwd365testuks" +# AZURE_STORAGE_ACCOUNT_KEY = 5NdDO9GGlbTFLHFOXZRw8H2C0Ow5bFTvHwUHEbStyzg2mbd1uwHSeBSvM/dkN+HfdwrFzqAkwL5P+AStdO6cig== +# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwd365testuks.blob.core.windows.net + + +# D365 PP key +# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwd365ppeuks" +# AZURE_STORAGE_ACCOUNT_KEY = xQz4FWDnQDE09uPOCj1t7axRiquvZG9vbxL88ywzVsJnTsAgewrHnuAGkxGPBCp2xGE53ffYOme6+AStSr8OoQ== +# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwd365ppeuks.blob.core.windows.net + AZURE_PEDW_CONTAINER = "pedwapplications" AZURE_PEDW_QUEUE_ENDPOINT = https://$AZURE_STORAGE_ACCOUNT_NAME.queue.core.windows.net @@ -126,7 +226,7 @@ NOTIFY_API_KEY = pedwnextauthlivekey-90f93710-9dae-423e-aea1-11eccd0dc132-5c0ed1 SHOW_DEBUG = true - + // DEV Connection string //NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=4b7568b3-70bd-4591-9dc2-267c0a6501d1;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=447d38b5-5d40-4238-8fb3-0ef4741b3db1 @@ -146,4 +246,8 @@ NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=a41b91bf-f7dc-4f44-bb66-a67df4eee43 //NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=61f358c2-8075-4164-b897-17bc6dd31fee;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=0c297349-396b-4dc6-8b56-85f5c680adc6 -HIDEERRORLOGS = false \ No newline at end of file +HIDEERRORLOGS = false + +SHOWSIPS = true + +ALLOWED_IPS=::1,203.0.113.42,198.51.100.17 diff --git a/.gitignore b/.gitignore index fbab4145..ad55dc2c 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,6 @@ public/swagger.json certificates/cert-key.pem certificates/cert.pem gitclean.sh - -dockerfile -Jenkinsfile +pages/admin/logcheck.js +pages/admin/logchecker.js +pages/baracuda.html diff --git a/actions/azurestorage.js b/actions/azurestorage.js index 6b1791c1..b9280e4d 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -210,31 +210,31 @@ export const getBlobs = async (containerName, casefolderID) => { "/api/file/downloadblob?container=" + containerName + "&casefolderID=" + - casefolderID + + encodeURIComponent(casefolderID) + "&blobname=" + - blob.name, + encodeURIComponent(blob.name), "hashedfilepath": hashAPIPath( "/api/file/downloadblob?container=" + containerName + "&casefolderID=" + - casefolderID + + encodeURIComponent(casefolderID) + "&blobname=" + - blob.name.split("/")[2] + encodeURIComponent(blob.name.split("/")[2]) ), "deletepath": "/api/file/deleteblob?container=" + containerName + "&casefolderID=" + - casefolderID + + encodeURIComponent(casefolderID) + "&blobname=" + - blob.name.split("/")[2], + encodeURIComponent(blob.name.split("/")[2]), "hasheddeletepath": hashAPIPath( "/api/file/deleteblob?container=" + containerName + "&casefolderID=" + - casefolderID + + encodeURIComponent(casefolderID) + "&blobname=" + - blob.name.split("/")[2] + encodeURIComponent(blob.name.split("/")[2]) ), "hashgetblobs": hashAPIPath( "/api/file/getbloblist?container=" + @@ -309,7 +309,7 @@ export const createRepBlob = async (formContent, containerName, caseref) => { const tags = { containerid: containerName, - caseID: formContent.caseRef, + caseID: formContent.ticketnumber, blobType: "Representation", }; @@ -317,7 +317,7 @@ export const createRepBlob = async (formContent, containerName, caseref) => { const withTags = await blockBlobClient.setTags(tags); const withMeta = await blockBlobClient.setMetadata(tags); - return formContent.pinswg_name; + return formContent.ticketnumber; }; export const createAppealPDFBlob = async ( @@ -432,7 +432,6 @@ export const createRepPDFBlob = async ( containerid: containerName, caseID: caseID, blobType: "Representation PDF", - documentLocationId: whichLocation, ishareLocation: whichLocation, documentLocationId: whichLocation, }; @@ -624,7 +623,6 @@ export const uploadFile = async (formContent, containerName, foldername) => { caseID: foldername.split("/")[0], documentType: files[prop][0].fieldName.split(".")[1], blobType: "RepresentationFile", - documentLocationId: whichLocation, ishareLocation: whichLocation, documentLocationId: whichLocation, }; @@ -1244,17 +1242,17 @@ export const getProgressBlobs = async (containerName, caseReference) => { "/api/file/downloadblob?container=" + containerName + "&casefolderID=" + - caseReference + + encodeURIComponent(caseReference) + "&blobname=" + - blob.name.split("/")[1] + encodeURIComponent(blob.name.split("/")[1]) ), "hasheddeletepath": hashAPIPath( "/api/file/deleteblob?container=" + containerName + "&casefolderID=" + - caseReference + + encodeURIComponent(caseReference) + "&blobname=" + - blob.name.split("/")[1] + encodeURIComponent(blob.name.split("/")[1]) ), "hashgetblobs": hashAPIPath( "/api/file/getbloblist?container=" + @@ -1415,20 +1413,20 @@ export const getRepsFilesBlobs = async ( "/api/file/downloadblob?container=" + containerName + "&casefolderID=" + - blob.name.split("/")[0] + - "/" + - blob.name.split("/")[1] + + encodeURIComponent( + blob.name.split("/")[0] + "/" + blob.name.split("/")[1] + ) + "&blobname=" + - blob.name.split("/")[3], + encodeURIComponent(blob.name.split("/")[3]), "hashedfilepath": hashAPIPath( "/api/file/downloadblob?container=" + containerName + "&casefolderID=" + - blob.name.split("/")[0] + - "/" + - blob.name.split("/")[1] + + encodeURIComponent( + blob.name.split("/")[0] + "/" + blob.name.split("/")[1] + ) + "&blobname=" + - blob.name + encodeURIComponent(blob.name.split("/")[3]) ), "deletepath": "/api/file/deleteblob?container=" + @@ -1443,11 +1441,11 @@ export const getRepsFilesBlobs = async ( "/api/file/deleteblob?container=" + containerName + "&casefolderID=" + - blob.name.split("/")[0] + - "/" + - blob.name.split("/")[1] + + encodeURIComponent( + blob.name.split("/")[0] + "/" + blob.name.split("/")[1] + ) + "&blobname=" + - blob.name.split("/")[3] + encodeURIComponent(blob.name.split("/")[3]) ), "hashgetblobs": hashAPIPath( "/api/file/getbloblist?container=" + @@ -1459,7 +1457,7 @@ export const getRepsFilesBlobs = async ( ), }); } - //console.log("blobObj:", blobObj); + console.log("blobObj:", blobObj); return blobObj; }; @@ -1702,3 +1700,100 @@ export const listBlobHierarchical = async ( } } }; + +function formatTimeSince(date) { + if (!date) return "Unknown date"; + + const now = new Date(); + const diffMs = now - date; + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays < 7) { + return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`; + } else { + const diffWeeks = Math.floor(diffDays / 7); + return `${diffWeeks} week${diffWeeks !== 1 ? "s" : ""} ago`; + } +} + +function formatDateDisplay(date) { + return date + ? `${date.toLocaleString("en-GB")} (${formatTimeSince(date)})` + : "Unknown"; +} + +export async function listBlobHierarchicalForUser(containerClient) { + const blobNames = []; + for await (const blob of containerClient.listBlobsFlat()) { + const lastModifiedDate = blob.properties?.lastModified || null; + const createdOnDate = blob.properties?.createdOn || null; + + let displayDate = ""; + + if (createdOnDate && lastModifiedDate) { + if ( + createdOnDate.toDateString() === lastModifiedDate.toDateString() + ) { + // Same day — show just one + displayDate = `Date: ${formatDateDisplay(lastModifiedDate)}`; + } else { + // Different days — show both + displayDate = `Created: ${formatDateDisplay( + createdOnDate + )}, Modified: ${formatDateDisplay(lastModifiedDate)}`; + } + } else if (lastModifiedDate) { + displayDate = `Modified: ${formatDateDisplay(lastModifiedDate)}`; + } else if (createdOnDate) { + displayDate = `Created: ${formatDateDisplay(createdOnDate)}`; + } else { + displayDate = "Unknown date"; + } + + blobNames.push({ + name: blob.name, + size: blob.properties.contentLength || 0, + lastModified: lastModifiedDate + ? lastModifiedDate.toISOString() + : null, + createdOn: createdOnDate ? createdOnDate.toISOString() : null, + displayDate, + }); + } + return blobNames; +} + +export async function listContainersForUser( + blobServiceClient, + containerNamePrefix, + emailAddress +) { + const options = { + includeDeleted: false, + includeMetadata: true, + includeSystem: true, + prefix: containerNamePrefix, + }; + + const results = []; + + for await (const containerItem of blobServiceClient.listContainers( + options + )) { + const containerClient = blobServiceClient.getContainerClient( + containerItem.name + ); + const blobNames = await listBlobHierarchicalForUser( + containerClient + ).catch(console.error); + + if (blobNames && blobNames.length > 0) { + results.push({ + container: containerItem.name, + blobs: blobNames, + }); + } + } + + return results; // array of non-empty containers +} diff --git a/actions/index.js b/actions/index.js index 072e4fa0..f0fbafe0 100644 --- a/actions/index.js +++ b/actions/index.js @@ -389,7 +389,7 @@ export const getDNSCoords = () => { }); }; -export const getDNSList = async (searchString) => { +export const getDNSList = (searchString) => { return axios .get(BASE_URL + "/api/endpoint/getdnslist_api") .then((res) => res.data) @@ -544,23 +544,42 @@ export const getBasicSearchDetailsPaged = async ( appealTypeName, caseReference, primaryIdAttribute, - incidentID + incidentIDs ) => { - //console.log("check appealtype:", appealType, caseReference); + if (!incidentIDs || incidentIDs.length === 0) return []; - return axios - .get( - "/api/endpoint/getbasicsearchdetailspaged_api?appealTypeName=" + - appealTypeName + - "&primaryIdAttribute=" + - primaryIdAttribute + - "&incidentID=" + - incidentID - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + console.log("=======", primaryIdAttribute); + // Build OData filter correctly + // const filter = incidentIDs + // .map((id) => `_${primaryIdAttribute}s_value eq ${id}`) + // .join(" or "); + + const filter = incidentIDs + .map((id) => { + const suffix = + primaryIdAttribute === "pinswg_sipscase" + ? `_${primaryIdAttribute}_value` + : `_${primaryIdAttribute}s_value`; + + return `${suffix} eq ${id}`; + }) + .join(" or "); + + const url = `/api/endpoint/getbasicsearchdetailspaged_api?appealTypeName=${appealTypeName}&primaryIdAttribute=${primaryIdAttribute}&incidentID=${encodeURIComponent( + filter + )}`; + + console.log( + `Fetching ${incidentIDs.length} incidents for appeal type: ${appealTypeName}` + ); + + try { + const res = await axios.get(url); + return res.data.value || []; // ensure returning an array + } catch (error) { + consoleLogger(error); + return []; + } }; export const getSearchDocumentDetails = (incidentID) => { @@ -828,6 +847,19 @@ export const getMyCases = (loggedInUserId) => { }); }; +export const getMyInvolvements = async (loggedInUserId) => { + try { + const res = await axios.get( + BASE_URL + + "/api/endpoint/getmyinvolvements_api?loggedInUserId=" + + loggedInUserId + ); + return res.data; + } catch (error) { + consoleLogger(error); + } +}; + export const getMyLPACases = (lpaid) => { return axios .get(BASE_URL + "/api/endpoint/getmylpacases_api?lpaid=" + lpaid) @@ -1492,23 +1524,20 @@ export const deleteMyRepresentationsFromBlob = ( }); }; -export const deleteWatchedCases = (watchedCaseID) => { +export const deleteWatchedCases = async (watchedCaseID) => { var queryUrl = - "/api/endpoint/deletewatchedcasesproxy_api?watchedCaseID=" + - watchedCaseID; + "/api/endpoint/deletewatchedcases_api?watchedCaseID=" + watchedCaseID; var config = { - method: "get", + method: "delete", url: queryUrl, }; - return axios(config) - .then((res) => { - //console.log("deleted ", res.data); - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + try { + const res = await axios(config); + return res.data; + } catch (error) { + consoleLogger(error); + } }; export const createAccount = (formValues) => { @@ -1813,9 +1842,9 @@ export const deleteBlob = async ( "/api/file/deleteblob?container=" + containerName + "&casefolderID=" + - casefolderID + + encodeURIComponent(casefolderID) + "&blobname=" + - blobName + + encodeURIComponent(blobName) + deleteblobhash ); return res.data; @@ -1836,11 +1865,9 @@ export const deleteRepBlob = async ( "/api/file/deleteblob?container=" + containerName + "&casefolderID=" + - casefolderID + - "/" + - filenamePrefix + + encodeURIComponent(casefolderID + "/" + filenamePrefix) + "&blobname=" + - blobName + + encodeURIComponent(blobName) + deleteblobhash ); return res.data; @@ -1894,14 +1921,14 @@ export const getProgressFromBlob = async (containerName, casereference) => { const res = await axios.get( BASE_URL + "/api/file/getprogressobjblob?container=" + - containerName + + encodeURIComponent(containerName) + "&casefolderID=" + - casereference + + encodeURIComponent(casereference) + hashAPIPath( "/api/file/getprogressobjblob?container=" + - containerName + + encodeURIComponent(containerName) + "&casefolderID=" + - casereference + encodeURIComponent(casereference) ) ); return res.data; @@ -2209,3 +2236,92 @@ export const getAppealPDFDocument = async (incidentid) => { return error.response; }); }; +export const getNewAppeals = async (searchString) => { + try { + const res = await axios.get(BASE_URL + "/api/admin/getnewappeals_api"); + return res.data; + } catch (error) { + consoleLogger(error); + return error.response; + } +}; + +export const getNewAppealsPage = async ( + searchString, + pageNumber, + orderBy, + fieldSort, + showNumberOfRecords +) => { + return axios + .get( + "/api/admin/getnewappeals_api?searchString=" + + searchString + + "&pageNumber=" + + pageNumber + + "&orderby=" + + orderBy + + "&fieldSort=" + + fieldSort + + "&showNumberOfRecords=" + + showNumberOfRecords + ) + .then((res) => { + return res.data; + }) + .catch((error) => { + consoleLogger(error); + return error.response; + }); +}; + +export const getNewDocumentsPaged = async ( + pageNumber, + orderBy, + fieldSort, + showNumberOfRecords, + documentType, + documentOrigin, + selectedWeeks +) => { + console.log( + "/api/admin/getlatestdocuments_api?pageNumber=" + + pageNumber + + "&orderby=" + + orderBy + + "&fieldSort=" + + fieldSort + + "&showNumberOfRecords=" + + showNumberOfRecords + + "&documentType=" + + documentType + + "&numberWeeks=" + + selectedWeeks + + "&documentOrigin=" + + documentOrigin + ); + return axios + .get( + "/api/admin/getlatestdocuments_api?pageNumber=" + + pageNumber + + "&orderby=" + + orderBy + + "&fieldSort=" + + fieldSort + + "&showNumberOfRecords=" + + showNumberOfRecords + + "&documentType=" + + documentType + + "&numberWeeks=" + + selectedWeeks + + "&documentOrigin=" + + documentOrigin + ) + .then((res) => { + return res.data; + }) + .catch((error) => { + consoleLogger(error); + return error.response; + }); +}; diff --git a/actions/selectQueryTypes.js b/actions/selectQueryTypes.js index d72777a7..75a4a51c 100644 --- a/actions/selectQueryTypes.js +++ b/actions/selectQueryTypes.js @@ -4,7 +4,7 @@ export const getSelectQuery = (appealTypeName) => { return "&$select=pinswg_detailedeiascreeningrequired,pinswg_speacialistcaseprocess,modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddresscounty,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,_pinswg_associatedlpa_value,pinswg_casedecisiondate,pinswg_dateeventrequested,pinswg_decision,pinswg_procedure,pinswg_questionnaireduedate,statuscode,pinswg_startdateoftheevent,pinswg_typeofevent,pinswg_startdate,pinswg_otherpartiesstatement,pinswg_finalcommentsduedate,pinswg_proofsofevidencewrittenstatementsofeviden,pinswg_speacialistcaseprocess,pinswg_statementduedate"; break; case "pinswg_callinss77s": - return "&$select=pinswg_eiarequired,modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddresscounty,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,_pinswg_associatedlpa_value,pinswg_casedecisiondate,pinswg_dateeventrequested,pinswg_decision,pinswg_finalcommentsduedate,pinswg_procedure,pinswg_proofsofevidencewrittenstatementsofeviden,pinswg_questionnaireduedate,pinswg_startdate,statuscode,pinswg_dateexportedtoiss,pinswg_startdateofevent,pinswg_typeofevent,pinswg_statementsduedate"; + return "&$select=pinswg_eiarequired,modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddresscounty,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,_pinswg_associatedlpa_value,pinswg_casedecisiondate,pinswg_dateeventrequested,pinswg_decision,pinswg_finalcommentsduedate,pinswg_procedure,pinswg_proofsofevidencewrittenstatementsofeviden,pinswg_questionnaireduedate,pinswg_startdate,statuscode,pinswg_dateexportedtoiss,pinswg_startdateofevent,pinswg_typeofevent,pinswg_statementsduedate,pinswg_otherpartiesstatement"; break; case "pinswg_commonlands": return "&$select=pinswg_detailedeiascreeningrequired,modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddresscounty,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,_pinswg_associatedlpa_value,pinswg_casedecisiondate,pinswg_dateeventrequested,pinswg_decision,pinswg_finalcommentsduedate,pinswg_otherpartiesstatement,pinswg_procedure,pinswg_proofsofevidencewrittenstatementsofeviden,pinswg_questionnaireduedate,pinswg_relevantauthorityname,pinswg_startdate,statuscode,pinswg_starttimeoftheevent,pinswg_typeofevent,pinswg_statementsduedate"; diff --git a/components/admin/tabs/accounts.js b/components/admin/tabs/accounts.js new file mode 100644 index 00000000..8d802860 --- /dev/null +++ b/components/admin/tabs/accounts.js @@ -0,0 +1,40 @@ +// components/admin/tabs/AccountsTab.js +import Link from "next/link"; + +export default function AccountsTab({ userData }) { + return ( +
+

User Accounts

+

Number of accounts created: {userData.length}

+ +
+
+
+ Name +
+
+ Last accessed +
+
+ + {userData.map((user) => ( +
+
+ + {user.email} + +
+ {user.id} +
+
+ Last accessed: {user.created} +
+
+ ))} +
+
+ ); +} diff --git a/components/admin/tabs/appeals.js b/components/admin/tabs/appeals.js new file mode 100644 index 00000000..bfa2115e --- /dev/null +++ b/components/admin/tabs/appeals.js @@ -0,0 +1,568 @@ +// components/admin/tabs/AppealsTab.js +import useTranslation from "next-translate/useTranslation"; +import { useCallback, useState, useEffect } from "react"; +import { JSONPath as jsonpath } from "jsonpath-plus"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import PaginationControl from "../../../components/search/pagination"; +import { connect } from "react-redux"; + +import { + setSearchResults, + setSearchDetails, +} from "../../../store/searchOutput/action"; + +import { getDetailsProxy, getSearchDetailsPaged } from "../../utils"; +import { getAdvancedSearchPaged, getNewAppealsPage } from "../../../actions"; +import { + setCurrentPage, + setCurrentReference, +} from "../../../store/currentView/action"; + +function AppealsTab(props) { + const [searchLoaded, setSearchLoaded] = useState(false); + const [showSpinnerState, setShowSpinnerState] = useState(false); + const [selectedOption, setSelectedOption] = useState(10); + const [orderByState, setOrderbyState] = useState("createdon"); + const [fieldSortState, setfieldSortState] = useState("desc"); + const [loginToWatch, setLoginToWatch] = useState("true"); + + const searchDetailsObj = props.searchResultsObj.searchDetailsObj || {}; + const resultsArr = props.searchResultsObj.searchResultsObj.value || []; + const searchResultsObj = props.searchResultsObj.searchResultsObj; + + const { t } = useTranslation(); + const router = useRouter(); + const myportal = false; + const advancedSearch = false; + const searchString = ""; + + const spinnerState = () => { + setShowSpinnerState((prev) => !prev); + }; + + const pagesCount = Math.ceil(searchResultsObj["@odata.count"] / 10); + + let currentPage = 0; + try { + currentPage = decodeURI(searchResultsObj["@odata.nextLink"]).split( + "skiptoken=" + )[1]; + currentPage = + typeof currentPage !== "undefined" + ? parseInt( + decodeURI(currentPage) + .split('pagenumber="')[1] + .slice( + 0, + decodeURI(currentPage) + .split('pagenumber="')[1] + .indexOf('"') + ) - 1 + ) + : pagesCount; + } catch (e) { + currentPage = pagesCount; + } + + const getSearchPageResults = useCallback( + (pageNumber, orderBy, fieldSort) => { + getNewAppealsPage( + searchString, + pageNumber, + orderBy, + fieldSort, + selectedOption + ).then((data) => { + props.setSearchResults(data); // ✅ use props, not import + return getSearchDetailsPaged(data).then((details) => { + props.setSearchDetails(details); // ✅ use props, not import + setShowSpinnerState(false); + }); + }); + }, + [ + advancedSearch, + searchString, + selectedOption, + props.setSearchDetails, + props.setSearchResults, + ] + ); + + useEffect(() => { + if (searchLoaded) { + if (selectedOption) { + setShowSpinnerState(true); + getSearchPageResults(1, orderByState, fieldSortState); + } + } else { + setSearchLoaded(true); + } + }, [ + selectedOption, + fieldSortState, + orderByState, + searchLoaded, + getSearchPageResults, + router.query, + ]); + + const ResultsView = (resultsArr) => { + return ( + <> +
+
+
+ +
+
+ {t("search:searchresults-site-address-label")} +
+
+ +
+
+ +
+
+ +
+
+ {/* {t("search:searchresults-status-label")} */} + +
+ +
+ Origin +
+
+ {showSpinnerState == true ? ( +
+
+ {t("common:spinner-fetching-data")} + { +
+
+ ) : ( + resultsArr.map((item, key) => { + let detailsObj = jsonpath({ + path: + '$..[?(@ && @.ticketnumber=="' + + item.ticketnumber + + '")]', + json: searchDetailsObj, + eval: true, + }); + + detailsObj = detailsObj[0] || {}; + return ( +
+
+
+ {item.pinswg_publishtoweb == true ? ( + { + setCurrentReference({ + "ticketnumber": + item.ticketnumber, + "currentReference": + item.title, + "currentType": + "searchResultsObj", + "incidentid": + item.incidentid, + "appealType": + item.pinswg_appealcasetype, + "showLoginCheck": + props.showLoginCheck, + "specialistProcess": + detailsObj.hasOwnProperty( + "pinswg_speacialistcaseprocess" + ) + ? detailsObj.pinswg_speacialistcaseprocess + : detailsObj.hasOwnProperty( + "pinswg_specialistcaseprocess" + ) + ? detailsObj.pinswg_specialistcaseprocess + : "", + }); + }} + > + + {t( + "search:searchresults-case-reference-label" + )} + : + + {item.title} + + ) : ( + item.title + )} + +
+ {new Date( + item.createdon + ).toLocaleString()} +
+
+ + {t( + "search:searchresults-site-address-label" + )} + : + + {_.has( + detailsObj, + "pinswg_projectlocation" + ) + ? detailsObj.pinswg_projectlocation != + null + ? detailsObj.pinswg_projectlocation.indexOf( + ";" + ) < 0 + ? detailsObj.pinswg_projectlocation + : router.locale == "cy" + ? detailsObj.pinswg_projectlocation.split( + ";" + )[1] + : detailsObj.pinswg_projectlocation.split( + ";" + )[0] + : "" + : ""} + {_.has( + detailsObj, + "pinswg_siteaddressline1" + ) + ? detailsObj.pinswg_siteaddressline1 + : ""} + + {/* {detailsObj.pinswg_siteaddressline1 != + null &&
} */} + + {_.has( + detailsObj, + "pinswg_siteaddressline1" + ) && + detailsObj.pinswg_siteaddressline1 != + null &&
} + + {_.has( + detailsObj, + "pinswg_siteaddressline1" + ) + ? detailsObj.pinswg_siteaddressline2 + : ""} + + {_.has( + detailsObj, + "pinswg_siteaddressline2" + ) && + detailsObj.pinswg_siteaddressline2 != + null &&
} + {_.has( + detailsObj, + "pinswg_siteaddresstown" + ) + ? detailsObj.pinswg_siteaddresstown + : ""} + {_.has( + detailsObj, + "pinswg_siteaddresstown" + ) && + detailsObj.pinswg_siteaddresstown != + null &&
} + + {_.has( + detailsObj, + "pinswg_siteaddresscounty" + ) + ? detailsObj.pinswg_siteaddresscounty + : ""} + {_.has( + detailsObj, + "pinswg_siteaddresscounty" + ) && + detailsObj.pinswg_siteaddresscounty != + null &&
} + {_.has( + detailsObj, + "pinswg_siteaddresspostcode" + ) + ? detailsObj.pinswg_siteaddresspostcode + : ""} +
+
+ + {t( + "search:searchresults-applicant-label" + )} + : + + {item.pinswg_appellantagent == 846040001 + ? item.pinswg_appellantfirstname + + " " + + item.pinswg_appellantlastname + : _.has(detailsObj, [ + "_pinswg_appellant_value@OData.Community.Display.V1.FormattedValue", + ]) + ? detailsObj[ + "_pinswg_appellant_value@OData.Community.Display.V1.FormattedValue" + ] + : ""} +
+
+ + {t( + "search:searchresults-authority-label" + )} + : + + {_.has(item, [ + "_pinswg_associatedlpa_value@OData.Community.Display.V1.FormattedValue", + ]) + ? router.locale == "cy" + ? jsonpath({ + path: + '$..[?(@ && @.value=="' + + item[ + "_pinswg_associatedlpa_value@OData.Community.Display.V1.FormattedValue" + ] + + '")].value_cy', + json: transLookup, + eval: true, + }) + : item[ + "_pinswg_associatedlpa_value@OData.Community.Display.V1.FormattedValue" + ] || "N/A" + : ""} +
+
+ + {t( + "search:searchresults-case-type-label" + )} + : + + {router.locale == "cy" + ? jsonpath({ + path: + '$..[?(@ && @.value=="' + + item[ + "pinswg_appealcasetype@OData.Community.Display.V1.FormattedValue" + ] + + '")].value_cy', + json: transLookup, + eval: true, + }) + : item[ + "pinswg_appealcasetype@OData.Community.Display.V1.FormattedValue" + ] || "N/A"} +
+
+ + {t( + "search:searchresults-status-label" + )} + : + + + {router.locale == "cy" + ? jsonpath({ + path: + '$..[?(@ && @.value=="' + + item[ + "statuscode@OData.Community.Display.V1.FormattedValue" + ] + + '")].value_cy', + json: transLookup, + eval: true, + }) + : item[ + "statuscode@OData.Community.Display.V1.FormattedValue" + ] || "N/A"} +
+
+ {item[ + "caseorigincode@OData.Community.Display.V1.FormattedValue" + ] || "N/A"} +
+
+ ); + }) + )} +
+ + + ); + }; + + return ( +
+

Appeals

+ {searchResultsObj["@odata.count"] > 5 && ( +
+
+ +
+
+ )} + {resultsArr.length > 0 ? ( + ResultsView(resultsArr) + ) : ( +
+
+

{t("search:searchresults-no-records-label")}

+
+
+ )} +
+ ); +} + +const mapDispatchToProps = (dispatch) => { + return { + setCurrentReference: (currentReference) => { + dispatch(setCurrentReference(currentReference)); + }, + setSearchResults: (searchResultsObj) => { + dispatch(setSearchResults(searchResultsObj)); + }, + setSearchDetails: (searchDetailsObj) => { + dispatch(setSearchDetails(searchDetailsObj)); + }, + setCurrentPage: (currentPage) => { + dispatch(setCurrentPage(currentPage)); + }, + }; +}; + +const mapStateToProps = (state) => { + return { + searchResultsObj: state.searchResultsObj, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(AppealsTab); diff --git a/components/admin/tabs/documents.js b/components/admin/tabs/documents.js new file mode 100644 index 00000000..94bc359a --- /dev/null +++ b/components/admin/tabs/documents.js @@ -0,0 +1,1214 @@ +import { JSONPath as jsonpath } from "jsonpath-plus"; +import useTranslation from "next-translate/useTranslation"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { connect } from "react-redux"; +import transLookup from "../../../data/lookuptranslations.json"; +import { formatDates } from "../../utils"; +import PaginationControl from "../../../components/case/pagination"; +import DocumentLink from "../../utils/downloads"; +import { useDownloadQueue } from "../../utils/downloadmanager"; + +import { sendGAEvent } from "@next/third-parties/google"; + +import { + getSearchDocumentDetails, + getSearchDocumentDetailsPaged, + getSearchDocumentTypes, + getNewDocumentsPaged, +} from "../../../actions"; +import { setCurrentPage } from "../../../store/currentView/action"; + +import { setDocumentDetails } from "../../../store/searchOutput/action"; +const getDocumentTypes = [ + { + "Value": 846040009, + "Label": "Main Party - Pre-Submission Docs", + }, + { + "Value": 846040000, + "Label": "Main Party - Submission Document", + }, + { + "Value": 846040001, + "Label": "Main Party - Questionnaire", + }, + { + "Value": 846040002, + "Label": "Main Party - Statements", + }, + { + "Value": 846040003, + "Label": "Main Party - Comments", + }, + { + "Value": 846040004, + "Label": "Main Party - Written Statements of Evidence", + }, + { + "Value": 846040010, + "Label": "Main Party - Environmental Statements", + }, + { + "Value": 846040005, + "Label": "Interested Party Representations", + }, + { + "Value": 846040006, + "Label": "Correspondence", + }, + { + "Value": 846040008, + "Label": "Inspector Notes", + }, + { + "Value": 846040007, + "Label": "Final Decision/Report", + }, + { + "Value": 846040011, + "Label": "Post Decision Correspondence", + }, + { + "Value": 846040012, + "Label": "High Court Challenge Correspondence", + }, + { + "Value": 846040013, + "Label": "Event - Correspondence", + }, + { + "Value": 846040014, + "Label": "Event - Documents", + }, + { + "Value": 846040015, + "Label": "Pre-Application - General", + }, + { + "Value": 846040016, + "Label": "Pre-Application - EIA Screening", + }, + { + "Value": 846040017, + "Label": "Pre-Application - EIA Scoping", + }, + { + "Value": 846040018, + "Label": "EIA Screening", + }, + { + "Value": 846040019, + "Label": "EIA Scoping", + }, + { + "Value": 846040020, + "Label": "Pre-Application Services Documents", + }, + { + "Value": 846040021, + "Label": "Notification", + }, + { + "Value": 846040022, + "Label": + "Submission - Environmental Statement - written statement and NTS", + }, + { + "Value": 846040023, + "Label": "Submission - Environmental Statement - Appendices", + }, + { + "Value": 846040024, + "Label": "Submission - Environmental Statement - Figures", + }, + { + "Value": 846040025, + "Label": "Submission - Draft Infrastructure Consent Orders", + }, + { + "Value": 846040026, + "Label": "Submission - Compulsory Acquisition Information", + }, + { + "Value": 846040027, + "Label": "Submission - Application Documents", + }, + { + "Value": 846040028, + "Label": "Examination Notices", + }, + { + "Value": 846040029, + "Label": "Local/Marine Impact Reports", + }, + { + "Value": 846040030, + "Label": "Submission - Further Information", + }, + { + "Value": 846040031, + "Label": "Formal Variation requests", + }, + { + "Value": 846040032, + "Label": "Further Information - Interested Party Representations", + }, + { + "Value": 846040033, + "Label": "Hearing Statements", + }, + { + "Value": 846040034, + "Label": "Events - Recordings", + }, + { + "Value": 846040035, + "Label": "Internal Correspondence", + }, + { + "Value": 846040036, + "Label": "Consultation Response", + }, +]; + +const getOrigin = [ + { + "Value": 846040000, + "Label": "MDU", + }, + { + "Value": 846040001, + "Label": "Portal", + }, + { + "Value": 846040002, + "Label": "Manual", + }, +]; + +const DocumentsTab = (props) => { + let { t, lang } = useTranslation(); + const firstRender = useRef(false); + const { + incidentid, + caseReference, + searchResultsObj, + searchDetailsObj, + documentDetailsObj, + setDocumentDetails, + setCurrentPage, + docsOffline, + showFilteredDocs, + } = props; + + const router = useRouter(); + const { locale } = router; + + var documentDetailsArr = documentDetailsObj || []; + const { startDownload, getStatus } = useDownloadQueue(3); + const [selectedOption, setSelectedOption] = useState(10); + const [selectedWeeks, setSelectedWeeks] = useState(1); + const [documentTypes, setDocumentTypes] = useState(getDocumentTypes); + const [documentOrigin, setDocumentOrigin] = useState(getOrigin); + const [selectedDocumentType, setSelectedDocumentType] = useState("all"); + const [selectedDocumentOrigin, setSelectedDocumentOrigin] = useState("all"); + const [selectAll, setSelectAll] = useState(false); + const [selectOriginAll, setSelectOriginAll] = useState(false); + + const [checkedItems, setCheckedItems] = useState( + documentTypes.reduce((acc, item) => { + acc[item.pinswg_isharedocumentlocations] = false; + return acc; + }, {}) + ); + + const [checkedOriginItems, setCheckedOriginItems] = useState( + documentOrigin.reduce((acc, item) => { + acc[item.pinswg_origin] = false; + return acc; + }, {}) + ); + + const handleCheckboxChange = (e) => { + const { name, checked } = e.target; + + setCheckedItems((prev) => ({ + ...prev, + [name]: checked, + })); + setSelectAll(false); + }; + + const handleCheckboxOriginChange = (e) => { + const { name, checked } = e.target; + + setCheckedOriginItems((prev) => ({ + ...prev, + [name]: checked, + })); + setSelectOriginAll(false); + }; + + const handleSelectAllChange = (e) => { + const { checked } = e.target; + setSelectAll(checked); // Set the 'Select All' state + if (checked) { + // If 'Select All' is checked, check all the other checkboxes + + setCheckedItems( + documentTypes.reduce((acc, item) => { + acc[item.pinswg_isharedocumentlocations] = true; + return acc; + }, {}) + ); + } else { + clearCheckboxes(); + } + }; + + const handleSelectOriginAllChange = (e) => { + const { checked } = e.target; + setSelectOriginAll(checked); // Set the 'Select All' state + if (checked) { + // If 'Select All' is checked, check all the other checkboxes + + setCheckedOriginItems( + documentOrigin.reduce((acc, item) => { + acc[item.pinswg_origin] = true; + return acc; + }, {}) + ); + } else { + clearCheckboxes(); + } + }; + + const clearCheckboxes = () => { + setSelectAll(false); + setSelectOriginAll(false); + setCheckedItems( + documentTypes.reduce((acc, item) => { + acc[item.pinswg_isharedocumentlocations] = false; + return acc; + }, {}), + + setSelectedDocumentType("all"), + setCurrentPage(1) + ); + setCheckedOriginItems( + documentOrigin.reduce((acc, item) => { + acc["origin_" + item.pinswg_origin] = false; + return acc; + }, {}), + setSelectedDocumentOrigin("all") + ); + }; + let ShowDocLinks = false; + let checkShowDocLinks = docsOffline != false ? true : false; + + const getDocLink = (docsOffline) => { + var startTime = docsOffline.split(",")[0]; + var endTime = docsOffline.split(",")[1]; + + // console.log(startTime); + // console.log(endTime); + + var currentDate = new Date(); + + var startDate = new Date(currentDate.getTime()); + startDate.setHours(startTime.split(":")[0]); + startDate.setMinutes(startTime.split(":")[1]); + startDate.setSeconds(startTime.split(":")[2]); + + var endDate = new Date(currentDate.getTime()); + endDate.setHours(endTime.split(":")[0]); + endDate.setMinutes(endTime.split(":")[1]); + endDate.setSeconds(endTime.split(":")[2]); + + // console.log(startDate); + // console.log(endDate); + // console.log(currentDate); + + var valid = startDate < currentDate && endDate > currentDate; + ShowDocLinks = valid; + }; + + checkShowDocLinks && getDocLink(docsOffline); + + const FilteredDocumentView = (documentDetailsArr) => { + documentDetailsArr = documentDetailsArr.value; + + return ( + <> + {ShowDocLinks && ( +
+ + + + Notice + + + {t("case:documents-not-available-label")} + +
+ )} + +
+
+ {" "} +
+

+ {documentDetailsObj["@odata.count"] > 1 + ? t("case:documents-count-label-1") + : t("case:documents-count-label-1a")}{" "} + {documentDetailsObj["@odata.count"]}{" "} + {documentDetailsObj["@odata.count"] > 1 + ? t("case:documents-count-label-2") + : t("case:documents-count-label-2a")} +

+
+ +
+

+ Document Type +

+
+ {" "} + <> +
+ + +
+ + {documentTypes.map((item, key) => { + return ( +
+ + +
+ ); + })} +
{" "} +

+ Document Origin +

+
+ {" "} + <> +
+ + +
+ + {documentOrigin.map((item, key) => { + return ( +
+ + +
+ ); + })} +
{" "} +
+ +
+ {documentDetailsObj["@odata.count"] > 5 && ( +
+
+ +
+
+ )} +
+
+ +
+
+
+ {_.isEmpty(documentDetailsObj.value) ? ( +
+
+

+ {/* {showDocumentSearchState == false + ? t("case:documents-checking-label") + : t( + "case:documents-non-available-label" + )} */} + No documents found +

+
+
+ ) : ( + <> +
+
+
+ +
+
+ {/* {t("case:documents-doc-type-label")} */} + +
+
+ {/* {t("case:documents-date-published-label")} */} + +
+
+ {/* {t("case:documents-date-published-label")} */} + Case +
+
+ {/* {t("case:documents-date-published-label")} */} + Origin +
+
+ {/* {t("case:documents-date-published-label")} */} + Published +
+ + {/*
+ {t("case:documents-size-label")} +
*/} +
+ {showSpinnerState == true ? ( +
+
+ {t( + "common:spinner-fetching-data" + )} + { +
+
+ ) : ( + documentDetailsArr.map((item, key) => { + let detailsObj = jsonpath({ + path: + "$..[?(@ && @._pinswg_documentids_value=='" + + incidentid + + "')]", + json: item, + eval: true, + }); + detailsObj = item; + return ( +
+
+ {showDownloadingState === + key && ( +
+ {/* { */} +
+ )} + + {!ShowDocLinks ? ( + detailsObj.pinswg_publishtoweb ? ( + // { + // toggleDownLoadLink( + // key + // ), + // sendGAEvent( + // "event", + // "DownloadedFile", + // { + // caseReference: + // props.caseReference, + // filename: + // detailsObj.pinswg_name, + // } + // ); + // }} + // > + // + // {t( + // "case:documents-name-label" + // )} + // : + // + // {_.has( + // detailsObj, + // "pinswg_name" + // ) + // ? detailsObj.pinswg_name + // : ""} + // + + ) : ( + <> + + {t( + "case:documents-name-label" + )} + : + + {_.has( + detailsObj, + "pinswg_name" + ) + ? detailsObj.pinswg_name + : ""} + + ) + ) : ( + <> + + {t( + "case:documents-name-label" + )} + : + + {_.has( + detailsObj, + "pinswg_name" + ) + ? detailsObj.pinswg_name + : ""} + + )} +
+
+ + {t( + "case:documents-doc-type-label" + )} + : + + + {router.locale == "cy" + ? jsonpath({ + path: + '$..[?(@ && @.value=="' + + detailsObj[ + "pinswg_isharedocumentlocations@OData.Community.Display.V1.FormattedValue" + ] + + '")].value_cy', + json: transLookup, + eval: true, + }) + : detailsObj[ + "pinswg_isharedocumentlocations@OData.Community.Display.V1.FormattedValue" + ] || + t( + "case:summary-no-date-entered-label" + )} +
+
+ + {t( + "case:documents-date-published-label" + )} + : + + + {_.has( + detailsObj, + "createdon" + ) + ? new Date( + detailsObj.createdon + ).toLocaleString() + : ""} +
+
+ {detailsObj.publishtoweb == + true ? ( + + { + detailsObj[ + "_pinswg_documentids_value@OData.Community.Display.V1.FormattedValue" + ] + } + + ) : ( + detailsObj[ + "_pinswg_documentids_value@OData.Community.Display.V1.FormattedValue" + ] + )} +
+
+ { + detailsObj[ + "pinswg_origin@OData.Community.Display.V1.FormattedValue" + ] + } +
+
+ { + detailsObj[ + "pinswg_publishtoweb@OData.Community.Display.V1.FormattedValue" + ] + } +
+ + {/*
+ + {t("case:documents-size-label")}: + + 92 +
*/} +
+ ); + }) + )} +
+ + + )} +
+
+ + ); + }; + + const getDocumentResults = useCallback( + (pageNumber, orderBy, fieldSort) => { + getNewDocumentsPaged( + pageNumber, + orderBy, + fieldSort, + selectedOption, + selectedDocumentType, + selectedDocumentOrigin, + selectedWeeks + ) + .then((data) => data) + .then((data) => { + props.setDocumentDetails(data); + setShowSpinnerState(false); + }); + }, + [ + props.incidentid, + selectedOption, + selectedDocumentType, + selectedWeeks, + selectedDocumentOrigin, + props.setDocumentDetails, + ] + ); + + const getDocumentDetails = async () => { + let docObj = await getSearchDocumentDetailsPaged( + incidentid, + pageNumber, + orderBy, + fieldSort + ).then((data) => { + props.setDocumentDetails(data); + return data; + }); + + return docObj; + }; + + const pagesCount = Math.ceil(documentDetailsObj["@odata.count"] / 10); + + let currentPage = 0; + + currentPage = decodeURI(documentDetailsObj["@odata.nextLink"]).split( + "skiptoken=" + )[1]; + currentPage = + typeof currentPage != "undefined" + ? parseInt( + decodeURI(currentPage) + .split('pagenumber="')[1] + .slice( + 0, + decodeURI(currentPage) + .split('pagenumber="')[1] + .indexOf('"') + ) - 1 + ) + : pagesCount; + + const [showSpinnerState, setShowSpinnerState] = useState(false); + + const [showDownloadingState, setShowDownloadingState] = useState(null); + + const toggleDownLoadLink = (i) => { + setShowDownloadingState(i); + + const downloadTime = setTimeout(hideDownloadCover, 5000); + downloadTime; + }; + + function hideDownloadCover() { + setShowDownloadingState(null); + } + + let spinnerState = () => { + showSpinnerState == true + ? setShowSpinnerState(false) + : setShowSpinnerState(true); + }; + + const [showDocumentSearchState, setDocumentSearchState] = useState(false); + const [orderByState, setOrderbyState] = useState("createdon"); + const [fieldSortState, setfieldSortState] = useState("desc"); + + let documentSearchState = () => { + showDocumentSearchState == true + ? setDocumentSearchState(false) + : setDocumentSearchState(true); + }; + + const sortDataBy = (whichField) => { + setOrderbyState(whichField); + setfieldSortState("asc"); + + getDocumentResults(1, whichField, fieldSortState); + whichField == orderByState + ? fieldSortState == "desc" + ? setfieldSortState("asc") + : setfieldSortState("desc") + : setfieldSortState("asc"); + setCurrentPage(1); + setShowSpinnerState(true); + }; + + useEffect(() => { + if (selectedOption) { + setShowSpinnerState(true); + getDocumentResults(1, orderByState, fieldSortState); + } + + if (selectedWeeks) { + setShowSpinnerState(true); + getDocumentResults(1, orderByState, fieldSortState); + } + + if (selectedDocumentType) { + setShowSpinnerState(true); + getDocumentResults( + 1, + orderByState, + fieldSortState, + selectedDocumentType + ); + } + + const docDetailsObj = async () => { + let docObj = await getNewDocumentsPaged( + pageNumber, + orderBy, + fieldSort, + selectedOption, + selectedDocumentType, + selectedWeeks + ).then((data) => { + props.setDocumentDetails(data); + setDocumentSearchState(true); + + return data; + }); + + return docObj; + }; + + //const searchDocumentResultsObj = docDetailsObj(); + }, [ + incidentid, + props.setDocumentDetails, + selectedOption, + selectedDocumentType, + fieldSortState, + orderByState, + getDocumentTypes, + getOrigin, + getDocumentResults, + lang, + selectedWeeks, + ]); + + function countFieldValue(obj, field, targetValue) { + let count = 0; + + function traverse(node) { + if (Array.isArray(node)) { + node.forEach(traverse); + } else if (node !== null && typeof node === "object") { + // If this object has the field, check it + if (node.hasOwnProperty(field) && node[field] === targetValue) { + count++; + } + // Keep traversing children + Object.values(node).forEach(traverse); + } + } + + traverse(obj); + return count; + } + + return ( +
+
+

+ {t("case:documents-heading-label")} +

+ {/*
+ MDU :{" "} + {countFieldValue( + documentDetailsObj, + "pinswg_origin", + 846040000 + )}{" "} + / Portal :{" "} + {countFieldValue( + documentDetailsObj, + "pinswg_origin", + 846040001 + )} +
*/} +
+
+ +
+
+ {FilteredDocumentView(documentDetailsArr)} +
+
+ ); +}; + +const mapStateToProps = (state) => { + return { + ...state, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + setDocumentDetails: (detailsObj) => { + dispatch(setDocumentDetails(detailsObj)); + }, + setCurrentPage: (currentPage) => { + dispatch(setCurrentPage(currentPage)); + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(DocumentsTab); diff --git a/components/admin/tabs/storage.js b/components/admin/tabs/storage.js new file mode 100644 index 00000000..fcbfb15f --- /dev/null +++ b/components/admin/tabs/storage.js @@ -0,0 +1,266 @@ +// components/admin/tabs/StorageTab.js +import { useState } from "react"; +import Link from "next/link"; +import { bytesToSize } from "../../../components/utils"; +import { + deleteMyRepresentationsFromBlob, + deleteAwaitingSubmissionsFromBlob, + sendRepCompleteMessage, +} from "../../../actions"; + +import { formatBlobDates } from "../utils/adminHelper"; + +export default function StorageTab({ data, repCompleteCount }) { + const [showRepJson, setShowRepJson] = useState(true); + const [showTmpFile, setShowTmpFile] = useState(true); + const [listData, setListData] = useState(data); + + const toggleAll = () => { + const newState = !(showRepJson || showTmpFile); + setShowRepJson(newState); + setShowTmpFile(newState); + }; + + return ( +
+

Storage Account Contents

+ {repCompleteCount > 0 && ( +

Number of Orphaned Reps: {repCompleteCount}

+ )} + + {/* Toggle buttons */} +
+ + + +
+ + {/* Render users & containers */} + {listData.map((user) => ( +
+ {user.containers.map((c) => ( +
+
+

+ {c.container} -{" "} + + {user.email} + +

+ {/* folders */} + {c.folders.map((folder) => ( +
+

{folder.folderPath || "Root"}

+ + {/* Buttons for TMP + Rep actions go here (same logic as before) */} + + {folder.hasTmpFile && ( + <> + + + )} + {folder.hasRepJson && ( +
+ {" "} + + {folder.repComplete && ( + + )} + {folder.repComplete && ( + + )} +
+ )} +
    + {folder.blobs.map((blob) => ( +
  • + + {blob.name} ( + {bytesToSize( + blob.contentLength || + blob.size + )} + ) + {" "} + {blob.lastModified && ( + + {/* { + blob.displayDate + } */} + {formatBlobDates( + blob + )} + + )} +
  • + ))} +
+
+ ))} +
+
+ ))} +
+ ))} +
+ ); +} diff --git a/components/admin/utils/adminHelper.js b/components/admin/utils/adminHelper.js new file mode 100644 index 00000000..87223cf1 --- /dev/null +++ b/components/admin/utils/adminHelper.js @@ -0,0 +1,81 @@ +// components/admin/utils/adminStorage.js + +/** + * Get the folder path up to the last slash. + * Used for grouping blobs by "case folder". + */ +export function getUpToLastSlash(url) { + let lastSlash = url.lastIndexOf("/"); + + url = url.substring(0, lastSlash); + + url = + url.lastIndexOf("/files") > 0 + ? url.substring(0, url.lastIndexOf("/files")) + : url; + return url; +} + +/** + * Convert a stream (Azure blob download) to a string. + */ +export async function streamToString(readableStream) { + return new Promise((resolve, reject) => { + const chunks = []; + readableStream.on("data", (data) => { + chunks.push(data.toString()); + }); + readableStream.on("end", () => { + resolve(chunks.join("")); + }); + readableStream.on("error", reject); + }); +} + +/** + * Format how long ago a date occurred. + */ +export function formatTimeSince(date) { + if (!date) return "Unknown date"; + + const now = new Date(); + const diffMs = now - date; + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays < 7) { + return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`; + } else { + const diffWeeks = Math.floor(diffDays / 7); + return `${diffWeeks} week${diffWeeks !== 1 ? "s" : ""} ago`; + } +} + +/** + * Format Azure blob created/modified dates into a readable string. + */ +export function formatBlobDates(blob) { + const created = blob.createdOn ? new Date(blob.createdOn) : null; + const modified = blob.lastModified ? new Date(blob.lastModified) : null; + + if (!created && !modified) return "Unknown"; + + // If both exist and are the same + if (created && modified && created.getTime() === modified.getTime()) { + return `${created.toLocaleString("en-GB")} (${formatTimeSince( + created + )})`; + } + + // If modified exists and is newer than created + if (created && modified && modified.getTime() > created.getTime()) { + return `Created: ${created.toLocaleString("en-GB")} (${formatTimeSince( + created + )}), Modified: ${modified.toLocaleString("en-GB")} (${formatTimeSince( + modified + )})`; + } + + // Only one date exists + const date = modified || created; + return `${date.toLocaleString("en-GB")} (${formatTimeSince(date)})`; +} diff --git a/components/admin/utils/prisma.js b/components/admin/utils/prisma.js new file mode 100644 index 00000000..ed7b9045 --- /dev/null +++ b/components/admin/utils/prisma.js @@ -0,0 +1,14 @@ +// lib/prisma.js +import { PrismaClient } from "@prisma/client"; + +const globalForPrisma = globalThis; + +export const prisma = + globalForPrisma.prisma || + new PrismaClient({ + log: ["query", "error", "warn"], + }); + +if (process.env.NODE_ENV !== "production") { + globalForPrisma.prisma = prisma; +} diff --git a/components/admin/utils/serverside.js b/components/admin/utils/serverside.js new file mode 100644 index 00000000..9bb35048 --- /dev/null +++ b/components/admin/utils/serverside.js @@ -0,0 +1,166 @@ +// components/admin/utils/serverStorage.js +import { DefaultAzureCredential } from "@azure/identity"; +import { BlobServiceClient } from "@azure/storage-blob"; +import { PrismaClient } from "@prisma/client"; +import CryptoJS from "crypto-js"; +import { getUpToLastSlash, streamToString } from "../utils/adminHelper"; +import { listContainersForUser } from "../../../actions/azurestorage"; + +const prisma = global.prisma || new PrismaClient(); + +/** + * Generate HMAC hash for blob API link. + */ +function hashAPIPath(queryPath, WORDKEY) { + let hashlink = CryptoJS.HmacSHA256( + queryPath, + CryptoJS.enc.Hex.parse(WORDKEY) + ); + return hashlink.toString(CryptoJS.enc.Hex); +} + +/** + * Fetch blob structure + users for admin storage page. + */ +export async function fetchAdminStorageData() { + const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME; + const creds = new DefaultAzureCredential(); + const blobServiceClient = new BlobServiceClient( + `https://${accountName}.blob.core.windows.net`, + creds + ); + + const prisma = global.prisma || new PrismaClient(); + + const users = await prisma.user.findMany(); + + let data = await Promise.all( + users.map(async (user) => { + const containers = await listContainersForUser( + blobServiceClient, + `${user.id}`, + user.email + ); + + const containersWithHashes = await Promise.all( + containers.map(async (container) => { + const groupedByFolder = await container.blobs.reduce( + async (accP, blob) => { + const acc = await accP; + const folderPath = + getUpToLastSlash(blob.name.trim()) || ""; + + const hashedfilepath = hashAPIPath( + `/api/file/downloadblob?container=${ + container.container + }&casefolderID=${folderPath}&blobname=${blob.name + .substring(blob.name.lastIndexOf("/") + 1) + .trim()}`, + process.env.HASHKEY + ); + + const fileNameOnly = blob.name.substring( + blob.name.lastIndexOf("/") + 1 + ); + const isTmpFile = fileNameOnly.startsWith("TMP-"); + + const blobWithHash = { + ...blob, + hashedfilepath, + isTmpFile, + }; + + let folderGroup = acc.find( + (f) => f.folderPath === folderPath + ); + if (!folderGroup) { + folderGroup = { + folderPath, + blobs: [], + hasRepJson: false, + repJsonBlob: null, + repComplete: false, + hasTmpFile: false, + }; + acc.push(folderGroup); + } + + folderGroup.blobs.push(blobWithHash); + + if (isTmpFile) { + folderGroup.hasTmpFile = true; + } + + // rep.json handling + if (blob.name.toLowerCase().endsWith("rep.json")) { + folderGroup.hasRepJson = true; + folderGroup.repJsonBlob = blobWithHash; + + const containerClient = + blobServiceClient.getContainerClient( + container.container + ); + const blockBlobClient = + containerClient.getBlockBlobClient( + blob.name + ); + + try { + const downloadResponse = + await blockBlobClient.download(0); + const downloaded = await streamToString( + downloadResponse.readableStreamBody + ); + const json = JSON.parse(downloaded); + folderGroup.repComplete = Boolean( + json.repComplete + ); + } catch (err) { + console.error( + "Error reading rep.json", + err + ); + folderGroup.repComplete = false; + } + } + + return acc; + }, + Promise.resolve([]) + ); + + return { ...container, folders: groupedByFolder }; + }) + ); + + return containersWithHashes.length > 0 + ? { + id: user.id, + email: user.email, + containers: containersWithHashes, + } + : null; + }) + ); + + data = data.filter(Boolean); + + let userData = users + .filter((user) => user.email && user.email.trim() !== "") + .map((user) => ({ + id: user.id, + email: user.email, + created: user.emailVerified?.toLocaleString("en-GB") || "", + })); + + let repCompleteCount = 0; + data.forEach((user) => { + user.containers.forEach((container) => { + container.folders.forEach((folder) => { + if (folder.repComplete === true) repCompleteCount++; + }); + }); + }); + + return { data, userData, repCompleteCount }; +} diff --git a/components/case/documents.js b/components/case/documents.js index 315b15b3..35f6db4a 100644 --- a/components/case/documents.js +++ b/components/case/documents.js @@ -17,9 +17,11 @@ import { import { setCurrentPage } from "../../store/currentView/action"; import { setDocumentDetails } from "../../store/searchOutput/action"; +import DocumentLink from "../utils/downloads"; +import { useDownloadQueue } from "../utils/downloadmanager"; const DocumentDetails = (props) => { - let { t } = useTranslation(); + let { t, lang } = useTranslation(); const firstRender = useRef(false); const { incidentid, @@ -37,7 +39,7 @@ const DocumentDetails = (props) => { const { locale } = router; var documentDetailsArr = documentDetailsObj || []; - + const { startDownload, getStatus } = useDownloadQueue(3); const [selectedOption, setSelectedOption] = useState(10); const [documentTypes, setDocumentTypes] = useState([{}]); const [selectedDocumentType, setSelectedDocumentType] = useState("all"); @@ -287,12 +289,9 @@ const DocumentDetails = (props) => { {/* {t("case:documents-date-published-label")} */}
@@ -584,8 +583,8 @@ const DocumentDetails = (props) => { '$..[?(@ && @.value=="' + item.pinswg_isharedocumentlocationsLabel + '")].value_cy', - jsonL: transLookup, - sanbox: {}, + json: transLookup, + eval: true, }) : item.pinswg_isharedocumentlocationsLabel || t( @@ -611,7 +610,7 @@ const DocumentDetails = (props) => { setCurrentPage(1); }} > - {t("case:documents-filter-button")} + {t("search:filter-documents-label")}
{documentDetailsObj["@odata.count"] > 5 && ( @@ -700,14 +699,9 @@ const DocumentDetails = (props) => { {/* {t("case:documents-date-published-label")} */} +// // ) : ( +// // <> +// // +// // Document name: +// // +// // {detailsObj.pinswg_name || ""} +// // +// // )} + +// // {downloadStatus && ( +// //

+// // {downloadStatus} +// // {/* {progress !== null && ` (${progress}%)`} */} +// //

+// // )} +// // +// // ); +// // } +// import { useState } from "react"; + +// export default function DocumentLink({ +// id, +// detailsObj, +// caseReference, +// startDownload, +// getStatus, +// }) { +// const [downloadStatus, setDownloadStatus] = useState(""); +// const [progress, setProgress] = useState(null); + +// const downloadFile = async () => { +// setDownloadStatus("Downloading"); +// setProgress(0); + +// try { +// const res = await fetch(detailsObj.pinswg_hashlink); +// if (!res.ok) throw new Error("Download failed"); + +// const contentDisposition = res.headers.get("content-disposition"); +// const filename = contentDisposition +// ? contentDisposition.split("filename=")[1] +// : detailsObj.pinswg_name || "document"; + +// const contentLength = res.headers.get("content-length"); +// const totalBytes = contentLength +// ? parseInt(contentLength, 10) +// : null; + +// const reader = res.body.getReader(); +// let receivedBytes = 0; +// const chunks = []; + +// while (true) { +// const { done, value } = await reader.read(); +// if (done) break; + +// chunks.push(value); +// receivedBytes += value.length; + +// if (totalBytes) { +// const percent = Math.round( +// (receivedBytes / totalBytes) * 100 +// ); +// setProgress(percent); +// } +// } + +// const blob = new Blob(chunks); +// const url = window.URL.createObjectURL(blob); + +// const link = document.createElement("a"); +// link.href = url; +// link.download = filename; +// document.body.appendChild(link); +// link.click(); +// document.body.removeChild(link); + +// setDownloadStatus("Download complete!"); +// setProgress(100); + +// window.gtag?.("event", "DownloadedFile", { +// caseReference, +// filename, +// }); +// } catch (err) { +// console.error(err); +// setDownloadStatus("Download failed"); +// setProgress(null); +// } +// }; + +// const handleClick = () => { +// if (getStatus(id) !== "idle") return; // don't double enqueue +// startDownload(id, downloadFile); +// }; + +// const status = getStatus(id); + +// return ( +// <> +// {detailsObj.pinswg_publishtoweb ? ( +// +// ) : ( +// <> +// +// Document name: +// +// {detailsObj.pinswg_name || ""} +// +// )} + +// {status !== "idle" && ( +//

+// {status === "downloading" && progress !== null +// ? `Downloading (${progress}%)` +// : status === "queued" +// ? "Queued" +// : status === "done" +// ? "Download complete!" +// : status === "failed" +// ? "Download failed" +// : ""} +//

+// )} +// +// ); +// } + +import { useState } from "react"; +import useTranslation from "next-translate/useTranslation"; + +export default function DocumentLink({ + id, + detailsObj, + caseReference, + startDownload, + getStatus, +}) { + const [progress, setProgress] = useState(null); + let { t } = useTranslation(); + const downloadFile = async () => { + setProgress(0); + + const res = await fetch(detailsObj.pinswg_hashlink); + if (!res.ok) throw new Error("Download failed"); + + const contentDisposition = res.headers.get("content-disposition"); + const filename = contentDisposition + ? contentDisposition.split("filename=")[1] + : detailsObj.pinswg_name || "document"; + + const contentLength = res.headers.get("content-length"); + const totalBytes = contentLength ? parseInt(contentLength, 10) : null; + + const reader = res.body.getReader(); + let receivedBytes = 0; + const chunks = []; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + chunks.push(value); + receivedBytes += value.length; + + if (totalBytes) { + const percent = Math.round((receivedBytes / totalBytes) * 100); + setProgress(percent); + } + } + + const blob = new Blob(chunks); + const url = window.URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + setProgress(100); + window.gtag?.("event", "DownloadedFile", { caseReference, filename }); + }; + + const handleClick = () => { + if (getStatus(id) !== "idle") return; // avoid double enqueue + startDownload(id, downloadFile); + }; + + const status = getStatus(id); + + return ( +
+ {detailsObj.pinswg_publishtoweb ? ( + + ) : ( + + {detailsObj.pinswg_name || ""} + + )} + + {status !== "idle" && ( +

+ {status === "downloading" && progress !== null + ? t("case:downloading-label") + : status === "queued" + ? t("case:download-queued-label") + : status === "done" + ? t("case:download-complete-label") + : status === "failed" + ? t("case:download-failed-label") + : ""} +

+ )} +
+ ); +} diff --git a/components/utils/index.js b/components/utils/index.js index c1e79203..606cf400 100644 --- a/components/utils/index.js +++ b/components/utils/index.js @@ -34,6 +34,15 @@ export const getFormCollectionByID = (appealTypeID) => { return collectionName[0]; }; +export const getNavigationPropertyByPrimaryAttribute = (primaryAttribute) => { + const collectionName = jsonpath({ + path: "$..[?(@ && @.PrimaryIdAttribute =='" + primaryAttribute + "')]", + json: data, + eval: true, + }); + return collectionName[0]; +}; + export const getFormIDByLogicalName = (appealTypeLogicalName) => { const collectionName = jsonpath({ path: "$..[?(@ && @.LogicalName =='" + appealTypeLogicalName + "')]", @@ -170,29 +179,58 @@ export const getPartSavedDetails = (searchResultsObj) => { * Get the details of a case from the appeal type * @param object searchResultsObj */ -export const getSearchDetailsPaged = (searchResultsObj) => { - let detailsArr = []; +export const getSearchDetailsPaged = async (searchResultsObj) => { searchResultsObj = searchResultsObj.value; - const detailsObj = searchResultsObj.map((searchDetail, index) => { - if (searchDetail.pinswg_appealcasetype == null) { - console.log(searchDetail.title); - } else { - let formMeta = getFormCollectionByID( - searchDetail.pinswg_appealcasetype - ); - // formMeta?.LogicalCollectionName && - // formMeta?.LogicalCollectionName != "pinswg_sipses" && - detailsArr.push( - getBasicSearchDetailsPaged( - formMeta.LogicalCollectionName, - searchDetail.title, - formMeta.PrimaryIdAttribute, - searchDetail.incidentid - ) - ); + + // Group incidents by appeal type + const groupedByAppealType = searchResultsObj.reduce((acc, detail) => { + const appealType = detail.pinswg_appealcasetype; + if (!appealType) { + console.log("No appeal type for:", detail.title); + return acc; } - }); - return Promise.all(detailsArr); + + if (!acc[appealType]) acc[appealType] = []; + acc[appealType].push({ + incidentID: detail.incidentid, + title: detail.title, + }); + return acc; + }, {}); + + const allDetails = []; + + // For each appeal type, call getBasicSearchDetailsPaged once with all incident IDs + for (const [appealType, incidents] of Object.entries(groupedByAppealType)) { + const incidentIDs = incidents.map((i) => i.incidentID); + + // Get the form meta for this appeal type to access primaryIdAttribute + const formMeta = getFormCollectionByID(appealType); + + if (!formMeta || !formMeta.PrimaryIdAttribute) { + console.log(`Missing form metadata for appeal type: ${appealType}`); + continue; + } + + // console.log( + // `Fetching ${incidentIDs.length} incidents for appeal type: ${appealType} with primaryIdAttribute: ${formMeta.PrimaryIdAttribute}` + // ); + + try { + const details = await getBasicSearchDetailsPaged( + formMeta.LogicalCollectionName, // appealTypeName + null, // caseReference is no longer used in batch + formMeta.PrimaryIdAttribute, // primaryIdAttribute + incidentIDs // pass array of incidentIDs + ); + + allDetails.push(...details); + } catch (err) { + consoleLogger(err); + } + } + + return allDetails; }; // export const absoluteUrl = (req, setLocalhost) => { diff --git a/data/collections.json b/data/collections.json index 64358d07..347bae55 100644 --- a/data/collections.json +++ b/data/collections.json @@ -47,27 +47,7 @@ "UrlName": "transportandworkact", "MetadataId": "bc3edc4b-7d82-eb11-aac0-00224800be9c", "appealTypeID": "846040013", - "NavigationProperty": "", - "value": "Transport and Works Act" - }, - { - "LogicalCollectionName": "pinswg_transportandworkacts", - "LogicalName": "pinswg_transportandworkact", - "PrimaryIdAttribute": "pinswg_transportandworkactid", - "UrlName": "transportandworkact", - "MetadataId": "bc3edc4b-7d82-eb11-aac0-00224800be9c", - "appealTypeID": "846040013", - "NavigationProperty": "pinswg_TransportandWorkActIdspinswg_TransportandWorkActIds", - "value": "Transport and Works Act" - }, - { - "LogicalCollectionName": "pinswg_transportandworkacts", - "LogicalName": "pinswg_transportandworksact", - "PrimaryIdAttribute": "pinswg_transportandworkactid", - "UrlName": "transportandworksact", - "MetadataId": "bc3edc4b-7d82-eb11-aac0-00224800be9c", - "appealTypeID": "846040013", - "NavigationProperty": "pinswg_TransportandWorkActIdspinswg_TransportandWorkActIds", + "NavigationProperty": "pinswg_TransportandWorkActIds", "value": "Transport and Works Act" }, { @@ -378,7 +358,7 @@ "UrlName": "sips", "MetadataId": "8d31911b-b8ea-ef11-ab08-00224800be9c", "appealTypeID": "846040002", - "NavigationProperty": "pinswg_SIPSIds", + "NavigationProperty": "pinswg_sipscase", "value": "SIP" } ] diff --git a/i18n.js b/i18n.js index 047546f3..3b680589 100644 --- a/i18n.js +++ b/i18n.js @@ -78,5 +78,6 @@ module.exports = { "/auth/signin": ["common", "common", "auth"], "/auth/verify-request": ["common", "common", "auth"], "/auth/error": ["common", "common", "auth"], + "/admin": ["case"], }, }; diff --git a/locales/cy/case.json b/locales/cy/case.json index 6a06de93..e6a2a62e 100644 --- a/locales/cy/case.json +++ b/locales/cy/case.json @@ -109,10 +109,16 @@ "trying-to-watch-case-redirect-label": "Fe'ch anogir nawr i fewngofnodi", "you-have-stopped-watching-label": "Ydych chi am roi'r gorau i wylio achos ", "do-you-want-to-delete-case-label": "Ydych chi am ddileu achos", + "do-you-want-to-delete-rep-label": "Ydych chi eisiau dileu cynrychiolaeth sy'n aros i'w chyflwyno", "do-you-want-to-delete-case-disclaimer-label": "Ar ôl dileu, bydd yr holl wybodaeth a gyflwynwyd gennych yn cael ei cholli.", "representation-date-passed-label": "Mae'r cyfnod cyflwyno sylwadau ar gyfer yr apêl hon rhwng {{startDate}} a {{endDate}}", "representation-date-passed-additional-label": "Gallwch wneud sylw drwy e-bostio,", "date-raised": "Dyddiad codi", + "download-complete-label": "Lawrlwytho wedi'i gwblhau!", + "downloading-label": "Wrthi'n llwytho i lawr", + "download-failed-label": "Methodd y lawrlwythiad", + "download-queued-label": "Wedi'i giwio", + "docsoffline-reps-label": "Oherwydd cynnal a chadw'r system, ni allwn dderbyn sylwadau ar hyn o bryd. Rhowch gynnig arall arni yn nes ymlaen.", "event-title": "Digwyddiadau", "event-name-label": " Enw", "event-event-name-label": "Enw'r digwyddiad ", diff --git a/locales/cy/myportal.json b/locales/cy/myportal.json index d83cd0b7..1d447e4b 100644 --- a/locales/cy/myportal.json +++ b/locales/cy/myportal.json @@ -52,5 +52,6 @@ "date-raised": "Dyddiad codi", "representation-submitted": "Cyflwynwyd y sylw", "submission-processing-label": "Prosesu cyflwyniad", - "appeal-pending-label": "Yn yr arfaeth" + "appeal-pending-label": "Yn yr arfaeth", + "docsoffline-newappeal-label": "Oherwydd gwaith cynnal a chadw'r system, ni allwn dderbyn apeliadau newydd ar hyn o bryd. Rhowch gynnig arall arni yn nes ymlaen." } \ No newline at end of file diff --git a/locales/en/case.json b/locales/en/case.json index c7cd77d2..64661ad3 100644 --- a/locales/en/case.json +++ b/locales/en/case.json @@ -110,10 +110,16 @@ "trying-to-watch-case-redirect-label": "You will now be prompted to sign in", "you-have-stopped-watching-label": "Do you want to stop watching case ", "do-you-want-to-delete-case-label": "Do you want to delete case ", + "do-you-want-to-delete-rep-label": "Do you want to delete representation awaiting submission", "do-you-want-to-delete-case-disclaimer-label": "Once deleted all information you have submited, will be lost.", "representation-date-passed-label": "The representation period for this appeal is between {{startDate}} and {{endDate}}", "representation-date-passed-additional-label": "You can make a representation by emailing,", "date-raised": "Date raised", + "download-complete-label": "Download complete!", + "downloading-label": "Downloading", + "download-failed-label": "Download failed", + "download-queued-label": "Queued", + "docsoffline-reps-label": "Due to system maintenance, we are unable to accept representations at this time. Please try again later.", "event-title": "Events", "event-name-label": "Name", "event-event-name-label": "Event name", diff --git a/locales/en/myportal.json b/locales/en/myportal.json index 13631904..d2639346 100644 --- a/locales/en/myportal.json +++ b/locales/en/myportal.json @@ -52,5 +52,6 @@ "date-raised": "Date raised", "representation-submitted": "Representation submitted", "submission-processing-label": "Submission processing", - "appeal-pending-label": "Pending" + "appeal-pending-label": "Pending", + "docsoffline-newappeal-label": "Due to system maintenance, we are unable to accept new appeals at this time. Please try again later." } \ No newline at end of file diff --git a/next-env.d.ts b/next-env.d.ts index fd36f949..a4a7b3f5 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/pages/admin/index.js b/pages/admin/index.js new file mode 100644 index 00000000..86ce577a --- /dev/null +++ b/pages/admin/index.js @@ -0,0 +1,216 @@ +// pages/admin/storage.js +import { useState } from "react"; +import Head from "next/head"; +import CookieBanner from "../../components/cookieBanner"; +import Header from "../../components/header"; +import Footer from "../../components/footer"; +import { connect } from "react-redux"; +import { wrapper } from "../../store/store"; + +import StorageTab from "../../components/admin/tabs/storage"; +import AccountsTab from "../../components/admin/tabs/accounts"; +import AppealsTab from "../../components/admin/tabs/appeals"; +import DocumentsTab from "../../components/admin/tabs/documents"; + +import { fetchAdminStorageData } from "../../components/admin/utils/serverside"; +import { getNewAppeals } from "../../actions"; + +import { getSearchDetails } from "../../components/utils"; + +import { + setSearchDetails, + setSearchResults, +} from "../../store/searchOutput/action"; +import { setCurrentPage } from "../../store/currentView/action"; + +const StoragePage = (props) => { + const { + data, + userData, + repCompleteCount, + searchResultsObj, + docsOffline, + showFilteredDocs, + } = props; + const [whichTab, setWhichTab] = useState("documents"); + + return ( +
+ + Admin Browser + + +
+ ); +}; + +export const getServerSideProps = wrapper.getServerSideProps( + (store) => async (ctx) => { + const { query, req, res } = ctx; + // IP restriction check + const ALLOWED_IPS = process.env.ALLOWED_IPS + ? process.env.ALLOWED_IPS.split(",").map((ip) => ip.trim()) + : []; + const LOCALHOST_IPS = ["127.0.0.1", "::1"]; + const forwarded = req.headers["x-forwarded-for"]; + const ip = + typeof forwarded === "string" + ? forwarded.split(",")[0] + : req.socket.remoteAddress; + + if (![...ALLOWED_IPS, ...LOCALHOST_IPS].includes(ip)) { + return { + redirect: { destination: "/403", permanent: false }, + }; + } + + // 🔹 fetch actual data + const { data, userData, repCompleteCount } = + await fetchAdminStorageData(); + + const newAppeals = await getNewAppeals(); + + const searchDetailsObj = await getSearchDetails(newAppeals); + + store.dispatch(setSearchResults(newAppeals)); + store.dispatch(setSearchDetails(searchDetailsObj)); + const state = store.getState(); + + store.dispatch(setCurrentPage(1)); + + return { + props: { + data, + userData, + repCompleteCount, + docsOffline: process.env.DOCAPI_OFFLINE || false, + showFilteredDocs: process.env.SHOWFILTERDOCS || false, + }, + }; + } +); + +const mapDispatchToProps = (dispatch) => { + return { + setCurrentPage: (currentPage) => { + dispatch(setCurrentPage(currentPage)); + }, + }; +}; + +const mapStateToProps = (state) => { + console.log("===============================\n state:", state); + return { + searchResultsObj: state.searchResultsObj, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(StoragePage); diff --git a/pages/advancedsearchresults.js b/pages/advancedsearchresults.js index 05848822..f2135ea9 100644 --- a/pages/advancedsearchresults.js +++ b/pages/advancedsearchresults.js @@ -124,8 +124,8 @@ export const getServerSideProps = wrapper.getServerSideProps( // "=============================== Advanced Search results:", // searchResultsObj // ); - const searchDetailsObj = - (await getSearchDetails(searchResultsObj)) || null; + // const searchDetailsObj = + // (await getSearchDetails(searchResultsObj)) || null; // console.log( // "=============================== Advanced Search results details:", // searchDetailsObj @@ -136,7 +136,7 @@ export const getServerSideProps = wrapper.getServerSideProps( store.dispatch(setShowReps(showReps, showLoginCheck)); store.dispatch(setSearchResults(searchResultsObj)); - store.dispatch(setSearchDetails(searchDetailsObj)); + // store.dispatch(setSearchDetails(searchDetailsObj)); store.dispatch(setSearch(Object.entries(query))); return { props: { diff --git a/pages/api/admin/getlatestdocuments_api.js b/pages/api/admin/getlatestdocuments_api.js new file mode 100644 index 00000000..36b7117c --- /dev/null +++ b/pages/api/admin/getlatestdocuments_api.js @@ -0,0 +1,180 @@ +/** + * @swagger + * /api/endpoint/getbasicsearch_api: + * get: + * tags: [Search] + * description: Basic search + * parameters: + * - name: searchString + * in: query + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ + +import axios from "axios"; +import CryptoJS from "crypto-js"; +import _ from "lodash"; +import { + azureHeadersPagedCustom, + consoleLogger, + getToken, +} from "../../../actions"; +import { number } from "prop-types"; + +const WORDKEY = process.env.HASHKEY; + +const WEBAPI_URL = + process.env.RELAY_ROOT || + "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; + +const hashAPIPath = (queryPath) => { + var hashlink = CryptoJS.HmacSHA256( + queryPath, + CryptoJS.enc.Hex.parse(WORDKEY) + ); + hashlink = hashlink.toString(CryptoJS.enc.Hex); + + //return "&hash=" + hashlink; + + return (queryPath.indexOf("?") > -1 ? "&hash=" : "?hash=") + hashlink; +}; + +const encryptDocReference = (documentRef) => { + var hashlink = CryptoJS.HmacSHA256( + "documents/download/" + documentRef, + CryptoJS.enc.Hex.parse(WORDKEY) + ); + hashlink = hashlink.toString(CryptoJS.enc.Hex); + + var hashStr = + "/api/documents/download/" + documentRef + "?hash=" + hashlink; + + return hashStr; +}; + +export default async function ApiProxy(req, res) { + var pageNumber = req.query.pageNumber || 1; + var token = await getToken(); + + var orderby = req.query.orderby || "createdon"; + var fieldSort = req.query.fieldSort || "desc"; + var showNumberOfRecords = req.query.showNumberOfRecords || 10; + var documentType = req.query.documentType || "all"; + var documentOrigin = req.query.documentOrigin || "all"; + var numberOfWeeks = req.query.numberWeeks || 1; + + var docTypeQueryString = ""; + var docOriginQueryString = ""; + + if (documentType != "all") { + if (documentType.indexOf(",") >= 0) { + const documentTypeArr = documentType.split(","); + + docTypeQueryString = " and ("; + documentTypeArr.forEach(function (item, index) { + console.log(item, index); + + if (item != "all") { + docTypeQueryString += + " pinswg_isharedocumentlocations eq " + item; + + docTypeQueryString += + index < documentTypeArr.length - 1 ? " or " : ""; + } + }); + docTypeQueryString += " )"; + } else { + docTypeQueryString += + " and pinswg_isharedocumentlocations eq " + documentType; + } + } + + if (documentOrigin != "all") { + if (documentOrigin.indexOf(",") >= 0) { + const documentOriginArr = documentOrigin.split(","); + + docOriginQueryString = " and ("; + documentOriginArr.forEach(function (item, index) { + console.log(item, index); + + if (item != "all") { + docOriginQueryString += " pinswg_origin eq " + item; + + docOriginQueryString += + index < documentOriginArr.length - 1 ? " or " : ""; + } + }); + docOriginQueryString += " )"; + } else { + docOriginQueryString += " and pinswg_origin eq " + documentOrigin; + } + } + + function daysAgoISO(days) { + const today = new Date(); + const resultDate = new Date(today); + resultDate.setUTCDate(today.getUTCDate() - days); // subtract weeks + resultDate.setUTCHours(0, 0, 0, 0); // reset time to 00:00:00 + return resultDate.toISOString().replace(/\.000Z$/, "Z"); // format as 'YYYY-MM-DDT00:00:00Z' + } + + var queryUrl = + "pinswg_documents?$select=pinswg_name,createdon,pinswg_publishtoweb,_pinswg_documentids_value,pinswg_isharedocumentlocations,pinswg_isharedocumentreference,pinswg_uploadurl,pinswg_uploadstatus,pinswg_origin&$filter=createdon ge " + + daysAgoISO(numberOfWeeks) + + docTypeQueryString + + docOriginQueryString + + "&$orderby=" + + orderby + + " " + + fieldSort + + "&$count=true" + + "&$expand=pinswg_DocumentIds($select = ticketnumber,pinswg_publishtoweb)" + + (typeof pageNumber != "undefined" + ? "&$skiptoken=" + '' + : ""); + + console.log( + "\n==========================================\n", + "\nnew docs search---- ", + "\n\nQuery url: " + queryUrl, + "\n\nRelay link: " + WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), + "\n==========================================\n" + ); + + var apiResponse = axios + .get( + WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), + azureHeadersPagedCustom(token.access_token, showNumberOfRecords) + ) + .then(({ data }) => { + let flattened = data.value.map((r) => ({ + ...r, + ticketnumber: r.pinswg_DocumentIds?.ticketnumber || null, + publishtoweb: r.pinswg_DocumentIds?.pinswg_publishtoweb || null, + })); + + data.value = flattened; + data.value.forEach(function (element) { + element.pinswg_hashlink = encryptDocReference( + element.pinswg_isharedocumentreference + ); + }); + + var dataStr; + + _.has(data, "@odata.nextLink") == true && + ((dataStr = JSON.stringify(data["@odata.nextLink"])), + (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); + res.status(200).json(data); + }) + .catch((error) => { + consoleLogger(error); + res.status(400).json(error); + }); + + return apiResponse; +} diff --git a/pages/api/admin/getnewappeals_api.js b/pages/api/admin/getnewappeals_api.js new file mode 100644 index 00000000..daa69cb0 --- /dev/null +++ b/pages/api/admin/getnewappeals_api.js @@ -0,0 +1,90 @@ +/** + * @swagger + * /api/endpoint/getbasicsearch_api: + * get: + * tags: [Search] + * description: Basic search + * parameters: + * - name: searchString + * in: query + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ + +import axios from "axios"; +import CryptoJS from "crypto-js"; +import _ from "lodash"; +import { + azureHeadersPagedCustom, + consoleLogger, + getToken, +} from "../../../actions"; + +const WORDKEY = process.env.HASHKEY; + +const WEBAPI_URL = + process.env.RELAY_ROOT || + "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; + +const hashAPIPath = (queryPath) => { + var hashlink = CryptoJS.HmacSHA256( + queryPath, + CryptoJS.enc.Hex.parse(WORDKEY) + ); + hashlink = hashlink.toString(CryptoJS.enc.Hex); + + //return "&hash=" + hashlink; + + return (queryPath.indexOf("?") > -1 ? "&hash=" : "?hash=") + hashlink; +}; + +export default async function ApiProxy(req, res) { + var searchString = req.query.searchString; + var pageNumber = req.query.pageNumber || 1; + var token = await getToken(); + + var orderby = req.query.orderby || "createdon"; + var fieldSort = req.query.fieldSort || "desc"; + var showNumberOfRecords = req.query.showNumberOfRecords || 10; + + var queryUrl = + "incidents?$select=caseorigincode,pinswg_publishtoweb,modifiedon,description,numberofchildincidents,_accountid_value,_customerid_value,_pinswg_associatedlpa_value,_ownerid_value,pinswg_appealcasetype,statuscode,ticketnumber,title, _primarycontactid_value,pinswg_lpareference,pinswg_appellantagent,pinswg_appellantfirstname,pinswg_appellantlastname&$expand=primarycontactid($select=fullname)&$orderby=" + + orderby + + " " + + fieldSort + + "&$count=true" + + (typeof pageNumber != "undefined" + ? "&$skiptoken=" + '' + : ""); + + console.log( + "\n==========================================\n", + "\nnew appeal search ", + "\n\nQuery url: " + queryUrl, + "\n\nRelay link: " + WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), + "\n==========================================\n" + ); + + var apiResponse = axios + .get( + WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), + azureHeadersPagedCustom(token.access_token, showNumberOfRecords) + ) + .then(({ data }) => { + var dataStr; + _.has(data, "@odata.nextLink") == true && + ((dataStr = JSON.stringify(data["@odata.nextLink"])), + (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); + res.status(200).json(data); + }) + .catch((error) => { + consoleLogger(error); + res.status(400).json(error); + }); + + return apiResponse; +} diff --git a/pages/api/documents/download/[id].js b/pages/api/documents/download/[id].js index 044e62b9..cbc8db9c 100644 --- a/pages/api/documents/download/[id].js +++ b/pages/api/documents/download/[id].js @@ -1,110 +1,203 @@ -/** - * @swagger - * /api/documents/download/{id}: - * get: - * tags: - * - Documents - * description: Get document - * parameters: - * - name: id - * in: path - * description: iShare ID - * type: string - * required: true - * default: A41567436 - * - name: hash - * in: query - * description: Hash string - * default: e0a1c9cd2817826a25bca67e60b807eae747cbed769e3ec42cf997541c32be71 - * responses: - * 200: - * description: Success - */ +// /** +// * @swagger +// * /api/documents/download/{id}: +// * get: +// * tags: +// * - Documents +// * description: Get document +// * parameters: +// * - name: id +// * in: path +// * description: iShare ID +// * type: string +// * required: true +// * default: A41567436 +// * - name: hash +// * in: query +// * description: Hash string +// * default: e0a1c9cd2817826a25bca67e60b807eae747cbed769e3ec42cf997541c32be71 +// * responses: +// * 200: +// * description: Success +// */ + +// import axios from "axios"; +// import CryptoJS from "crypto-js"; +// import { getToken, consoleLogger } from "../../../../actions"; + +// const WORDKEY = process.env.HASHKEY; +// const accessTokenEndpoint = process.env.ACCESS_TOKEN_ENDPOINT; +// const tenantId = process.env.TENANT; + +// const WEBAPI_URL = +// process.env.RELAY_ROOT || +// "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; + +// const hashAPIPath = (queryPath) => { +// var hashlink = CryptoJS.HmacSHA256( +// queryPath, +// CryptoJS.enc.Hex.parse(WORDKEY) +// ); +// hashlink = hashlink.toString(CryptoJS.enc.Hex); + +// //return "&hash=" + hashlink; + +// return (queryPath.indexOf("?") > -1 ? "&hash=" : "?hash=") + hashlink; +// }; + +// const azureHeadersPaged = (access_token) => { +// return { +// headers: { +// "OData-MaxVersion": "4.0", +// "OData-Version": "4.0", +// "Accept": "application/json;odata.metadata=none", +// "Prefer": +// 'odata.include-annotations="*",return=representation, odata.maxpagesize=10', +// "Content-Type": "application/json", +// "Authorization": "Bearer " + access_token, +// }, +// }; +// }; + +// export default async function ApiProxy(req, res) { +// var token = await getToken(); + +// var docRef = req.query; + +// var queryUrl = "documents/download/" + docRef.id + "?hash=" + docRef.hash; + +// const configDocument = (access_token) => { +// return { +// headers: { +// "OData-MaxVersion": "4.0", +// "OData-Version": "4.0", +// "Accept": "application/json", +// "Prefer": 'odata.include-annotations="*",return=representation', +// "Content-Type": "application/json", +// "Authorization": "Bearer " + access_token, +// }, +// responseType: "arraybuffer", +// }; +// }; + +// return axios +// .get( +// WEBAPI_URL + queryUrl, // + hashAPIPath(queryUrl), +// configDocument(token.access_token) +// ) +// .then((response) => { +// const bytes = response.data.byteLength; +// console.log(bytes); + +// res.setHeader( +// "content-disposition", +// "attachment; filename=" + +// response.headers["content-disposition"].split( +// "filename=" +// )[1] +// ); +// return res.status(200).send(response.data); +// }) +// .then(() => { +// console.log(docRef.id + "has downloaded"); + +// return "downloadComplete"; +// }) +// .catch((error) => { +// consoleLogger(error); +// res.redirect("/filenotavailable"); +// }); +// } import axios from "axios"; -import CryptoJS from "crypto-js"; import { getToken, consoleLogger } from "../../../../actions"; -const WORDKEY = process.env.HASHKEY; -const accessTokenEndpoint = process.env.ACCESS_TOKEN_ENDPOINT; -const tenantId = process.env.TENANT; - const WEBAPI_URL = process.env.RELAY_ROOT || "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; -const hashAPIPath = (queryPath) => { - var hashlink = CryptoJS.HmacSHA256( - queryPath, - CryptoJS.enc.Hex.parse(WORDKEY) - ); - hashlink = hashlink.toString(CryptoJS.enc.Hex); - - //return "&hash=" + hashlink; - - return (queryPath.indexOf("?") > -1 ? "&hash=" : "?hash=") + hashlink; -}; - -const azureHeadersPaged = (access_token) => { - return { - headers: { - "OData-MaxVersion": "4.0", - "OData-Version": "4.0", - "Accept": "application/json;odata.metadata=none", - "Prefer": - 'odata.include-annotations="*",return=representation, odata.maxpagesize=10', - "Content-Type": "application/json", - "Authorization": "Bearer " + access_token, - }, - }; -}; - -export default async function ApiProxy(req, res) { - var token = await getToken(); - - var docRef = req.query; - - var queryUrl = "documents/download/" + docRef.id + "?hash=" + docRef.hash; - - const configDocument = (access_token) => { - return { - headers: { - "OData-MaxVersion": "4.0", - "OData-Version": "4.0", - "Accept": "application/json", - "Prefer": 'odata.include-annotations="*",return=representation', - "Content-Type": "application/json", - "Authorization": "Bearer " + access_token, - }, - responseType: "arraybuffer", - }; - }; - - return axios - .get( - WEBAPI_URL + queryUrl, // + hashAPIPath(queryUrl), - configDocument(token.access_token) - ) - .then((response) => { - const bytes = response.data.byteLength; - console.log(bytes); - - res.setHeader( - "content-disposition", - "attachment; filename=" + - response.headers["content-disposition"].split( - "filename=" - )[1] - ); - return res.status(200).send(response.data); - }) - .then(() => { - console.log(docRef.id + "has downloaded"); - - return "downloadComplete"; - }) - .catch((error) => { - consoleLogger(error); - res.redirect("/filenotavailable"); - }); +// Retry utility with logging +async function retry(fn, retries = 3, delay = 1000) { + for (let attempt = 1; attempt <= retries; attempt++) { + try { + return { response: await fn(), attempts: attempt }; + } catch (err) { + if (attempt === retries) throw err; + console.log(`Retry ${attempt} failed, retrying in ${delay}ms...`); + await new Promise((res) => setTimeout(res, delay)); + delay *= 2; // exponential backoff + } + } } + +const ApiProxy = async (req, res) => { + const docRef = req.query; + + try { + const token = await getToken(); + const startTime = Date.now(); + + const fetchStream = async () => { + const queryUrl = + "documents/download/" + docRef.id + "?hash=" + docRef.hash; + + return axios.get(WEBAPI_URL + queryUrl, { + headers: { + "OData-MaxVersion": "4.0", + "OData-Version": "4.0", + "Accept": "application/json", + "Prefer": + 'odata.include-annotations="*",return=representation', + "Content-Type": "application/json", + "Authorization": "Bearer " + token.access_token, + }, + responseType: "stream", + }); + }; + + const { response, attempts } = await retry(fetchStream, 3, 1000); + + // Extract filename + const contentDisposition = response.headers["content-disposition"]; + const filename = contentDisposition + ? contentDisposition.split("filename=")[1] + : docRef.id; + + res.setHeader( + "Content-Disposition", + `attachment; filename=${filename}` + ); + res.setHeader("Content-Type", "application/octet-stream"); + + let totalBytes = 0; + + response.data.on("data", (chunk) => { + totalBytes += chunk.length; + }); + + response.data.pipe(res); + + response.data.on("end", () => { + const durationMs = Date.now() - startTime; + console.log( + `[Download Complete] Document: ${filename}, ID: ${docRef.id}, Size: ${totalBytes} bytes, Duration: ${durationMs}ms, Attempts: ${attempts}` + ); + }); + + response.data.on("error", (err) => { + consoleLogger(err); + if (!res.headersSent) res.redirect("/filenotavailable"); + }); + } catch (error) { + consoleLogger(error); + if (!res.headersSent) res.redirect("/filenotavailable"); + } +}; + +export const config = { + api: { + responseLimit: false, + }, +}; + +export default ApiProxy; diff --git a/pages/api/endpoint/getbasicsearch_by_address_api.js b/pages/api/endpoint/getbasicsearch_by_address_api.js index 2005d723..eae961db 100644 --- a/pages/api/endpoint/getbasicsearch_by_address_api.js +++ b/pages/api/endpoint/getbasicsearch_by_address_api.js @@ -306,7 +306,7 @@ export default async function ApiProxy(req, res) { "MetadataId": "8d31911b-b8ea-ef11-ab08-00224800be9c", "appealTypeID": "846040002", "tmpNav": "pinswg_sipsid", - "NavigationProperty": "pinswg_SIPSids", + "NavigationProperty": "pinswg_sipscase", "value": "SIP", }, diff --git a/pages/api/endpoint/getbasicsearchdetailspaged_api.js b/pages/api/endpoint/getbasicsearchdetailspaged_api.js index 2929a751..417203c2 100644 --- a/pages/api/endpoint/getbasicsearchdetailspaged_api.js +++ b/pages/api/endpoint/getbasicsearchdetailspaged_api.js @@ -34,7 +34,7 @@ import CryptoJS from "crypto-js"; import _ from "lodash"; import { azureHeadersPaged, consoleLogger, getToken } from "../../../actions"; import { getSelectQuery } from "../../../actions/selectQueryTypes"; - +import { getNavigationPropertyByPrimaryAttribute } from "../../../components/utils"; const WORDKEY = process.env.HASHKEY; const WEBAPI_URL = @@ -59,15 +59,27 @@ export default async function ApiProxy(req, res) { var incidentID = req.query.incidentID; var token = await getToken(); + let navigationProperty = + getNavigationPropertyByPrimaryAttribute( + primaryIdAttribute + ).NavigationProperty; + + // "?$filter=_" + + // (primaryIdAttribute == "pinswg_sipscase" + // ? "pinswg_sipscase_value" + // : primaryIdAttribute + "s_value ") + + // " eq " + + // incidentID + + var queryUrl = appealTypeName + - "?$filter=_" + - (primaryIdAttribute == "pinswg_sipscase" - ? "pinswg_sipscase_value" - : primaryIdAttribute + "s_value ") + - " eq " + + "?$filter=" + incidentID + - "&$count=true"; + "&$count=true" + + "&$expand=" + + navigationProperty + + "($select=ticketnumber)"; + //" and statuscode eq 1&$count=true"; queryUrl = @@ -86,10 +98,18 @@ export default async function ApiProxy(req, res) { azureHeadersPaged(token.access_token) ) .then(({ data }) => { + let flattened = data.value.map((r) => ({ + ...r, + ticketnumber: r[navigationProperty]?.ticketnumber || null, + })); + + data.value = flattened; + var dataStr; _.has(data, "@odata.nextLink") == true && ((dataStr = JSON.stringify(data["@odata.nextLink"])), (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); + return res.status(200).json(data); }) .catch((error) => { diff --git a/pages/auth/signin2.js b/pages/auth/signin2.js index 0e14448a..569281e7 100644 --- a/pages/auth/signin2.js +++ b/pages/auth/signin2.js @@ -207,6 +207,35 @@ const SignIn = (props) => { }; export async function getServerSideProps(context) { + const { query, req, res } = context; + + // Load allowed IPs from env and trim spaces + const ALLOWED_IPS = process.env.ALLOWED_IPS + ? process.env.ALLOWED_IPS.split(",").map((ip) => ip.trim()) + : []; + + // Always allow localhost addresses + const LOCALHOST_IPS = ["127.0.0.1", "::1"]; + + // Get IP address from headers or socket + const forwarded = req.headers["x-forwarded-for"]; + const ip = + typeof forwarded === "string" + ? forwarded.split(",")[0] + : req.socket.remoteAddress; + + console.log("Visitor IP:", ip); + + // Check whitelist + localhost + if (![...ALLOWED_IPS, ...LOCALHOST_IPS].includes(ip)) { + return { + redirect: { + destination: "/403", // custom "Access Denied" page + permanent: false, + }, + }; + } + if (process.env.SHOWLOGIN == false) { return { redirect: { diff --git a/pages/dns/[developmentName].js b/pages/dns/[developmentName].js index 97c78311..dd4d80da 100644 --- a/pages/dns/[developmentName].js +++ b/pages/dns/[developmentName].js @@ -81,9 +81,16 @@ const CaseHome = (props) => { props.awaitingSubmission.awaitingSubmission } caseReference={ + props.searchResultsObj.searchResultsObj.value[0] + .caseReference + } + ticketnumber={ props.searchResultsObj.searchResultsObj.value[0] .ticketnumber } + // ticketnumber={ + // props.currentView.caseReference.ticketnumber + // } currentType={props.currentType} appealTypeID={ props.searchResultsObj.searchResultsObj.value[0] diff --git a/pages/dnsapplications.js b/pages/dnsapplications.js index b3315c4e..a1315141 100644 --- a/pages/dnsapplications.js +++ b/pages/dnsapplications.js @@ -20,7 +20,7 @@ import { wrapper } from "../store/store"; import ServiceBanner from "../components/servicebanner"; const Home = (props) => { - const { footerLinks, pages, showMap } = props; + const { footerLinks, pages, showMap, showLoginCheck } = props; let { t, lang } = useTranslation(); const router = useRouter(); @@ -105,6 +105,7 @@ const Home = (props) => { showMap={showMap} searchString="DNS" searchResultsObj={props.searchResultsObj} + showLoginCheck={showLoginCheck} />