diff --git a/.gitignore b/.gitignore index 1a70475b0e..74ae0b44e1 100644 --- a/.gitignore +++ b/.gitignore @@ -512,3 +512,5 @@ DigitalLearningSolutions.Web/Views/Shared/Components/SelectList/Default.cshtml DigitalLearningSolutions.Web/Views/Shared/Components/SingleCheckbox/Default.cshtml DigitalLearningSolutions.Web/Views/Shared/Components/TextArea/Default.cshtml DigitalLearningSolutions.Web/Views/Shared/Components/TextInput/Default.cshtml +/DigitalLearningSolutions.Web/appsettings.Test.json +/DigitalLearningSolutions.Web/web.config diff --git a/DigitalLearningSolutions.Data.Migrations/202503111500_AddLastAccessedColumn.cs b/DigitalLearningSolutions.Data.Migrations/202503111500_AddLastAccessedColumn.cs new file mode 100644 index 0000000000..95a0eb6020 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202503111500_AddLastAccessedColumn.cs @@ -0,0 +1,26 @@ +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + + [Migration(202503111500)] + public class AddLastAccessedColumn : Migration + { + public override void Up() + { + Alter.Table("Users").AddColumn("LastAccessed").AsDateTime().Nullable(); + Alter.Table("DelegateAccounts").AddColumn("LastAccessed").AsDateTime().Nullable(); + Alter.Table("AdminAccounts").AddColumn("LastAccessed").AsDateTime().Nullable(); + + Execute.Sql("UPDATE u SET LastAccessed = (SELECT MAX(s.LoginTime) FROM DelegateAccounts da JOIN Sessions s ON da.ID = s.CandidateId WHERE da.UserID = u.ID) FROM users u;"); + Execute.Sql("UPDATE da SET LastAccessed = (SELECT MAX(s.LoginTime) FROM Sessions s WHERE s.CandidateId = da.ID) FROM DelegateAccounts da;"); + Execute.Sql("UPDATE da SET LastAccessed = (SELECT ca.LastAccessed FROM CandidateAssessments ca WHERE ca.ID = da.ID) FROM DelegateAccounts da where da.LastAccessed IS NULL;"); + Execute.Sql("UPDATE AA SET LastAccessed = (SELECT MAX(AdS.LoginTime) FROM AdminSessions AdS WHERE AdS.AdminID = AA.ID) FROM AdminAccounts AA;"); + } + public override void Down() + { + Delete.Column("LastAccessed").FromTable("Users"); + Delete.Column("LastAccessed").FromTable("DelegateAccounts"); + Delete.Column("LastAccessed").FromTable("AdminAccounts"); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202503121538_Alter_SendExpiredTBCReminders.cs b/DigitalLearningSolutions.Data.Migrations/202503121538_Alter_SendExpiredTBCReminders.cs new file mode 100644 index 0000000000..315960fb4f --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202503121538_Alter_SendExpiredTBCReminders.cs @@ -0,0 +1,19 @@ + + +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + + [Migration(202503121538)] + public class Alter_SendExpiredTBCReminders : Migration + { + public override void Up() + { + Execute.Sql(Properties.Resources.TD_5412_Alter_SendExpiredTBCReminders_Up); + } + public override void Down() + { + Execute.Sql(Properties.Resources.TD_5412_Alter_SendExpiredTBCReminders_Down); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/2025041161520_Alter_SendExpiredTBCReminders_AppendCourseName.cs b/DigitalLearningSolutions.Data.Migrations/2025041161520_Alter_SendExpiredTBCReminders_AppendCourseName.cs new file mode 100644 index 0000000000..02bcb19b10 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/2025041161520_Alter_SendExpiredTBCReminders_AppendCourseName.cs @@ -0,0 +1,19 @@ + + +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + + [Migration(2025041161520)] + public class Alter_SendExpiredTBCReminders_AppendCourseName : Migration + { + public override void Up() + { + Execute.Sql(Properties.Resources.TD_5514_Alter_SendExpiredTBCReminders_Up); + } + public override void Down() + { + Execute.Sql(Properties.Resources.TD_5514_Alter_SendExpiredTBCReminders_Down); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202504241517_AddFieldIsPrimaryToSelfAssessmentFrameworksTable.cs b/DigitalLearningSolutions.Data.Migrations/202504241517_AddFieldIsPrimaryToSelfAssessmentFrameworksTable.cs new file mode 100644 index 0000000000..fe1e096135 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202504241517_AddFieldIsPrimaryToSelfAssessmentFrameworksTable.cs @@ -0,0 +1,12 @@ +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + [Migration(202504241517)] + public class AddFieldIsPrimaryToSelfAssessmentFrameworksTable : AutoReversingMigration + { + public override void Up() + { + Alter.Table("SelfAssessmentFrameworks").AddColumn("IsPrimary").AsBoolean().NotNullable().WithDefaultValue(1); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202504281045_Alter_ReorderFrameworkCompetency.cs b/DigitalLearningSolutions.Data.Migrations/202504281045_Alter_ReorderFrameworkCompetency.cs new file mode 100644 index 0000000000..2759e056ac --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202504281045_Alter_ReorderFrameworkCompetency.cs @@ -0,0 +1,19 @@ + + +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + + [Migration(202504281045)] + public class Alter_ReorderFrameworkCompetency : Migration + { + public override void Up() + { + Execute.Sql(Properties.Resources.TD_5447_Alter_ReorderFrameworkCompetency_Up); + } + public override void Down() + { + Execute.Sql(Properties.Resources.TD_5447_Alter_ReorderFrameworkCompetency_Down); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202504281115_UpdateFrameworkCompetenciesOrdering.cs b/DigitalLearningSolutions.Data.Migrations/202504281115_UpdateFrameworkCompetenciesOrdering.cs new file mode 100644 index 0000000000..e64cdd3f6f --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202504281115_UpdateFrameworkCompetenciesOrdering.cs @@ -0,0 +1,21 @@ +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + [Migration(202504281115)] + public class UpdateFrameworkCompetenciesOrdering : ForwardOnlyMigration + { + public override void Up() + { + Execute.Sql(@"WITH Ranked AS ( +    SELECT ID, +            ROW_NUMBER() OVER (PARTITION BY FrameworkID ORDER BY SysStartTime) AS NewOrder +    FROM FrameworkCompetencies + Where FrameworkCompetencyGroupID is null + ) + UPDATE fc + SET fc.Ordering = r.NewOrder + FROM FrameworkCompetencies fc + JOIN Ranked r ON fc.ID = r.ID;"); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202504290900_UpdateNotifications.cs b/DigitalLearningSolutions.Data.Migrations/202504290900_UpdateNotifications.cs new file mode 100644 index 0000000000..a662824279 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202504290900_UpdateNotifications.cs @@ -0,0 +1,14 @@ +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + + [Migration(202504290900)] + public class UpdateNotifications : ForwardOnlyMigration + { + public override void Up() + { + Execute.Sql(@$"UPDATE Notifications SET NotificationName = 'Completed course follow-up feedback requests' where NotificationID = 13"); + } + + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202506110906_AddSqlMaintenanceSolution.cs b/DigitalLearningSolutions.Data.Migrations/202506110906_AddSqlMaintenanceSolution.cs new file mode 100644 index 0000000000..86b2026fc4 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202506110906_AddSqlMaintenanceSolution.cs @@ -0,0 +1,17 @@ +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + [Migration(202506110906)] + public class AddSqlMaintenanceSolution : Migration + { + + public override void Up() + { + Execute.Sql(Properties.Resources.TD_5670_MaintenanceScripts_UP); + } + public override void Down() + { + Execute.Sql(Properties.Resources.TD_5670_MaintenanceScripts_DOWN); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202507030918_CreateOrAlterGetSelfAssessmentReport.cs b/DigitalLearningSolutions.Data.Migrations/202507030918_CreateOrAlterGetSelfAssessmentReport.cs new file mode 100644 index 0000000000..daa85c4f15 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202507030918_CreateOrAlterGetSelfAssessmentReport.cs @@ -0,0 +1,17 @@ +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + [Migration(20250703091)] + public class CreateOrAlterGetSelfAssessmentReport : Migration + { + public override void Up() + { + Execute.Sql(Properties.Resources.TD_5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP); + } + public override void Down() + { + Execute.Sql("DROP PROCEDURE IF EXISTS [dbo].[usp_GetSelfAssessmentReport]"); + Execute.Sql("DROP FUNCTION IF EXISTS [dbo].[GetOtherCentresForSelfAssessmentTVF]"); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202507040803_FixCreateOrAlterGetSelfAssessmentReport.cs b/DigitalLearningSolutions.Data.Migrations/202507040803_FixCreateOrAlterGetSelfAssessmentReport.cs new file mode 100644 index 0000000000..73aab1d8a0 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202507040803_FixCreateOrAlterGetSelfAssessmentReport.cs @@ -0,0 +1,12 @@ +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + [Migration(202507040803)] + public class FixCreateOrAlterGetSelfAssessmentReport : ForwardOnlyMigration + { + public override void Up() + { + Execute.Sql(Properties.Resources.TD_5759_CreateOrAlterSelfAssessmentReportSPandTVF_Fix_UP); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202507111455_UpdatePrivacyNoticeAcceptableUse.cs b/DigitalLearningSolutions.Data.Migrations/202507111455_UpdatePrivacyNoticeAcceptableUse.cs new file mode 100644 index 0000000000..b9dc84ab7e --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202507111455_UpdatePrivacyNoticeAcceptableUse.cs @@ -0,0 +1,430 @@ +using FluentMigrator; +namespace DigitalLearningSolutions.Data.Migrations +{ + [Migration(202507111455)] + public class UpdatePrivacyNoticeAcceptableUse : Migration + { + public override void Up() + { + var PrivacyPolicyNew = @"

PRIVACY NOTICE

This page explains our privacy policy and how we will use and protect any information about you that you give to us or that we collate when you visit this website, or undertake employment with NHS England (NHSE or we/us/our), or participate in any NHSE sponsored training, education and development including via any of our training platform websites (Training).

This privacy notice is intended to provide transparency regarding what personal data NHSE may hold about you, how it will be processed and stored, how long it will be retained and who may have access to your data.

Personal data is any information relating to an identified or identifiable living person (known as the data subject). An identifiable person is one who can be identified, directly or indirectly, in particular by reference to an identifier such as a name, an identification number or factors specific to the physical, genetic or mental identity of that person, for example.

1 OUR ROLE IN THE NHS

We are here to improve the quality of healthcare for the people and patients of England through education, training and lifelong development of staff and appropriate planning of the workforce required to deliver healthcare services in England.

We aim to enable high quality, effective, compassionate care and to identify the right people with the right skills and the right values. All the information we collect is to support these objectives.

2 IMPORTANT INFORMATION

NHSE is the data controller in respect of any personal data it holds concerning trainees in Training, individuals employed by NHSE and individuals accessing NHSE’s website.

We have appointed a data protection officer (DPO) who is responsible for overseeing questions in relation to this privacy policy. If you have any questions about this privacy policy or our privacy practices, want to know more about how your information will be used, or make a request to exercise your legal rights, please contact our DPO in the following ways:

Name: Andrew Todd

Email address: gdpr@hee.nhs.uk

Postal address: NHS England of Skipton House, 80 London Road, London SE1 6LH

3 WHAT THIS PRIVACY STATEMENT COVERS

This privacy statement only covers the processing of personal data by NHSE that NHSE has obtained from data subjects accessing any of NHSE’s websites and from its provision of services and exercise of functions. It does not cover the processing of data by any sites that can be linked to or from NHSE’s websites, so you should always be aware when you are moving to another site and read the privacy statement on that website.

When providing NHSE with any of your personal data for the first time, for example, when you take up an appointment with NHSE or when you register for any Training, you will be asked to confirm that you have read and accepted the terms of this privacy statement. A copy of your acknowledgement will be retained for reference.

If our privacy policy changes in any way, we will place an updated version on this page. Regularly reviewing the page ensures you are always aware of what information we collect, how we use it and under what circumstances, if any, we will share it with other parties.

4 WHY NHSE COLLECTS YOUR PERSONAL DATA

Personal data may be collected from you via the recruitment process, when you register and/or create an account for any Training, during your Annual Review of Competence Progression or via NHSE’s appraisal process. Personal data may also be obtained from Local Education Providers or employing organisations in connection with the functions of NHSE.

Your personal data is collected and processed for the purposes of and in connection with the functions that NHSE performs with regard to Training and planning. The collection and processing of such data is necessary for the purposes of those functions.

A full copy of our notification to the Information Commissioner’s Office (ICO) (the UK regulator for data protection issues), can be found on the ICO website here: www.ico.org.uk by searching NHSE’s ICO registration number, which is Z2950066.

In connection with Training, NHSE collects and uses your personal information for the following purposes:

  1. to manage your Training and programme, including allowing you to access your own learning history;
  2. to quality assure Training programmes and ensure that standards are maintained, including gathering feedback or input on the service, content, or layout of the Training and customising the content and/or layout of the Training;
  3. to identify workforce planning targets;
  4. to maintain patient safety through the management of performance concerns;
  5. to comply with legal and regulatory responsibilities including revalidation;
  6. to contact you about Training updates, opportunities, events, surveys and information that may be of interest to you;
  7. transferring your Training activity records for programmes to other organisations involved in medical training in the healthcare sector. These organisations include professional bodies that you may be a member of, such as a medical royal college or foundation school; or employing organisations, such as trusts;
  8. making your Training activity records visible to specific named individuals, such as tutors, to allow tutors to view their trainees’ activity. We would seek your explicit consent before authorising anyone else to view your records;
  9. providing anonymous, summarised data to partner organisations, such as professional bodies; or local organisations, such as strategic health authorities or trusts;
  10. for NHSE internal review;
  11. to provide HR related support services and Training to you, for clinical professional learner recruitment;
  12. to promote our services;
  13. to monitor our own accounts and records;
  14. to monitor our work, to report on progress made; and
  15. to let us fulfil our statutory obligations and statutory returns as set by the Department of Health and the law (for example complying with NHSE’s legal obligations and regulatory responsibilities under employment law).

Further information about our use of your personal data in connection with Training can be found in ’A Reference Guide for Postgraduate Foundation and Specialty Training in the UK’, published by the Conference of Postgraduate Medical Deans of the United Kingdom and known as the ‘Gold Guide’, which can be found here: https://www.copmed.org.uk/gold-guide.

5 TYPES OF PERSONAL DATA COLLECTED BY NHSE

The personal data that NHSE collects when you register for Training enables the creation of an accurate user profile/account, which is necessary for reporting purposes and to offer Training that is relevant to your needs.

The personal data that is stored by NHSE is limited to information relating to your work, such as your job role, place of work, and membership number for a professional body (e.g. your General Medical Council number). NHSE will never ask for your home address or any other domestic information.

When accessing Training, you will be asked to set up some security questions, which may contain personal information. These questions enable you to log in if you forget your password and will never be used for any other purpose. The answers that you submit when setting up these security questions are encrypted in the database so no one can view what has been entered, not even NHSE administrators.

NHSE also store a record of some Training activity, including upload and download of Training content, posts on forums or other communication media, and all enquires to the service desks that support the Training.

If you do not provide personal data that we need from you when requested, we may not be able to provide services (such as Training) to you. In this case, we may have to cancel such service, but we will notify you at the time if this happens.

6 COOKIES

When you access NHSE’s website and Training, we want to make them easy, useful and reliable. This sometimes involves placing small amounts of limited information on your device (such as your computer or mobile phone). These small files are known as cookies, and we ask you to agree to their usage in accordance with ICO guidance.

These cookies are used to improve the services (including the Training) we provide you through, for example:

  1. enabling a service to recognise your device, so you do not have to give the same information several times during one task (e.g. we use a cookie to remember your username if you check the ’Remember Me’ box on a log in page);
  2. recognising that you may already have given a username and password, so you do not need to do it for every web page requested;
  3. measuring how many people are using services, so they can be made easier to use and there is enough capacity to ensure they are fast; and
  4. analysing anonymised data to help us understand how people interact with services so we can make them better.

We use a series of cookies to monitor website speed and usage, as well as to ensure that any preferences you have selected previously are the same when you return to our website. Please visit our cookie policies page to understand the cookies that we use: https://www.dls.nhs.uk/v2/CookieConsent/CookiePolicy

Most cookies applied when accessing Training are used to keep track of your input when filling in online forms, known as session-ID cookies, which are exempt from needing consent as they are deemed essential for using the website or Training they apply to. Some cookies, like those used to measure how you use the Training, are not needed for our website to work. These cookies can help us improve the Training, but we’ll only use them if you say it’s OK. We’ll use a cookie to save your settings.

On a number of pages on our website or Training, we use ’plug-ins’ or embedded media. For example, we might embed YouTube videos. Where we have used this type of content, the suppliers of these services may also set cookies on your device when you visit their pages. These are known as ’third-party’ cookies. To opt-out of third-parties collecting any data regarding your interaction on our website, please refer to their websites for further information.

We will not use cookies to collect personal data about you. However, if you wish to restrict or block the cookies which are set by our websites or Training, or indeed any other website, you can do this through your browser settings. The ’Help’ function within your browser should tell you how. Alternatively, you may wish to visit www.aboutcookies.org which contains comprehensive information on how to do this on a wide variety of browsers. You will also find details on how to delete cookies from your machine as well as more general information about cookies. Please be aware that restricting cookies may impact on the functionality of our website.

7 LEGAL BASIS FOR PROCESSING

The retained EU law version of the General Data Protection Regulation ((EU) 2016/679) (UK GDPR) requires that data controllers and organisations that process personal data demonstrate compliance with its provisions. This involves publishing our basis for lawful processing of personal data.

As personal data is processed for the purposes of NHSE’s statutory functions, NHSE’s legal bases for the processing of personal data as listed in Article 6 of the UK GDPR are as follows:

Where NHSE processes special categories of personal data, its additional legal bases for processing such data as listed in Article 9 of the UK GDPR are as follows:

Special categories of personal data include data relating to racial or ethnic origin, political opinions, religious beliefs, sexual orientation and data concerning health.

Please note that not all of the above legal bases will apply for each type of processing activity that NHSE may undertake. However, when processing any personal data for any particular purpose, one or more of the above legal bases will apply.

We may seek your consent for some processing activities, for example for sending out invitations to you to Training events and sending out material from other government agencies. If you do not give consent for us to use your data for these purposes, we will not use your data for these purposes, but your data may still be retained by us and used by us for other processing activities based on the above lawful conditions for processing set out above.

8 INFORMATION THAT WE MAY NEED TO SEND YOU

We may occasionally have to send you information from NHSE, the Department of Health, other public authorities and government agencies about matters of policy where those policy issues impact on Training, workforce planning, or other matters related to NHSE. This is because NHSE is required by statute to exercise functions of the Secretary of State in respect of Training and workforce planning. If you prefer, you can opt out of receiving information about general matters of policy impacting on Training and workforce planning by contacting your Local Office recruitment lead or tel@hee.nhs.uk. The relevant Local Office or a representative from the relevant training platform website will provide you with further advice and guidance regarding any consequences of your request.

NHSE will not send you generic information from other public authorities and government agencies on issues of government policy.

9 TRANSFERS ABROAD

The UK GDPR imposes restrictions on the transfer of personal data outside the European Union, to third countries or international organisations, in order to ensure that the level of protection of individuals afforded by the UK GDPR is not undermined.

Your data may only be transferred abroad where NHSE is assured that a third country, a territory or one or more specific sectors in the third country, or an international organisation ensures an adequate level of protection.

10 HOW WE PROTECT YOUR PERSONAL DATA

Our processing of all personal data complies with the UK GDPR principles. We have put in place appropriate security measures to prevent your personal data from being accidentally lost, used or accessed in an unauthorised way, altered or disclosed. The security of the data is assured through the implementation of NHSE’s policies on information governance management.

The personal data we hold may be held as an electronic record on data systems managed by NHSE, or as a paper record. These records are only accessed, seen and used in the following circumstances:

  1. if required and/or permitted by law; or
  2. by NHSE staff who need access to them so they can do their jobs and who are subject to a duty of confidentiality; or
  3. by other partner organisations, including our suppliers, who have been asked to sign appropriate non-disclosure or data sharing agreements and will never be allowed to use the information for commercial purposes.

We make every effort to keep your personal information accurate and up to date, but in some cases we are reliant on you as the data subject to notify us of any necessary changes to your personal data. If you tell us of any changes in your circumstances, we can update the records with personal data you choose to share with us.

Information collected by NHSE will never be sold for financial gain or shared with other organisations for commercial purposes.

We have put in place procedures to deal with any suspected personal data breach and will notify you and any applicable regulator of a breach where we are legally required to do so.

11 SHARING PERSONAL DATA

So we can provide the right services at the right level, we may share your personal data within services across NHSE and with other third party organisations such as the Department of Health, higher education institutions, clinical placement providers, colleges, faculties, other NHSE Local Offices, the General Medical Council, NHS Trusts/Health Boards/Health and Social Care Trusts, approved academic researchers and other NHS and government agencies where necessary, to provide the best possible Training and to ensure that we discharge NHSEs responsibilities for employment and workforce planning for the NHS. This will be on a legitimate need to know basis only.

We may also share information, where necessary, to prevent, detect or assist in the investigation of fraud or criminal activity, to assist in the administration of justice, for the purposes of seeking legal advice or exercising or defending legal rights or as otherwise required by the law.

Where the data is used for analysis and publication by a recipient or third party, any publication will be on an anonymous basis, and will not make it possible to identify any individual. This will mean that the data ceases to become personal data.

12 HOW LONG WE RETAIN YOUR PERSONAL DATA

We will keep personal data for no longer than necessary to fulfil the purposes we collected it for, in accordance with our records management policy and the NHS records retention schedule within the NHS Records Management Code of Practice at: https://www.england.nhs.uk/contact-us/privacy-notice/nhs-england-as-a-data-controller (as may be amended from time to time).

In some circumstances you can ask us to delete your data. Please see the “Your rights” section below for further information.

In some circumstances we will anonymise your personal data (so that it can no longer be associated with you) for research or statistical purposes, in which case we may use this information indefinitely without further notice to you.

13 OPEN DATA

Open data is data that is published by central government, local authorities and public bodies to help you build products and services. NHSE policy is to observe the Cabinet Office transparency and accountability commitments towards more open use of public data in accordance with relevant and applicable UK legislation.

NHSE would never share personal data through the open data facility. To this end, NHSE will implement information governance protocols that reflect the ICO’s recommended best practice for record anonymisation, and Office of National Statistics guidance on publication of statistical information.

14 YOUR RIGHTS

14.1 Right to rectification and erasure

Under the UK GDPR you have the right to rectification of inaccurate personal data and the right to request the erasure of your personal data. However, the right to erasure is not an absolute right and it may be that it is necessary for NHSE to continue to process your personal data for a number of lawful and legitimate reasons.

14.2 Right to object and withdraw your consent

You have the right in certain circumstances to ask NHSE to stop processing your personal data in relation to any NHSE service. As set out above, you can decide that you do not wish to receive information from NHSE about matters of policy affecting Training and workforce. However, the right to object is not an absolute right and it may be that it is necessary in certain circumstances for NHSE to continue to process your personal data for a number of lawful and legitimate reasons.

If you object to the way in which NHSE is processing your personal information or if you wish to ask NHSE to stop processing your personal data, please contact your relevant Local Office.

Please note, if we do stop processing personal data about you, this may prevent NHSE from providing the best possible service to you. Withdrawing your consent will result in your Training account being anonymised and access to the Training removed.

14.3 Right to request access

You can access a copy of the information NHSE holds about you by writing to NHSE’s Public and Parliamentary Accountability Team. This information is generally available to you free of charge subject to the receipt of appropriate identification. More information about subject access requests can be found here: https://www.hee.nhs.uk/about/contact-us/subject-access-request.

14.4 Right to request a transfer

The UK GDPR sets out the right for a data subject to have their personal data ported from one controller to another on request in certain circumstances. You should discuss any request for this with your Local Office. This right only applies to automated information which you initially provided consent for us to use or where we used the information to perform a contract with you.

14.5 Right to restrict processing

You can ask us to suspend the processing of your personal data if you want us to establish the data’s accuracy, where our use of the data is unlawful but you do not want us to erase it, where you need us to hold the data even if we no longer require it as you need it to establish, exercise or defend legal claims or where you have objected to our use of your data but we need to verify whether we have overriding legitimate grounds to use it.

14.6 Complaints

You have the right to make a complaint at any time to the ICO. We would, however, appreciate the chance to deal with your concerns before you approach the ICO so please contact your Local Office or the DPO in the first instance, using the contact details above.

You can contact the ICO at the following address:

The Office of the Information Commissioner
Wycliffe House
Water Lane
Wilmslow
Cheshire
SK9 5AF

14.7 Your responsibilities

It is important that you work with us to ensure that the information we hold about you is accurate and up to date so please inform NHSE if any of your personal data needs to be updated or corrected.

All communications from NHSE will normally be by email. It is therefore essential for you to maintain an effective and secure email address, or you may not receive information or other important news and information about your employment or Training.

"; + + Execute.Sql(@"UPDATE Config SET UpdatedDate = GETDATE() ,ConfigText =N'" + PrivacyPolicyNew + "'" + + "where ConfigName='PrivacyPolicy' AND IsHtml = 1"); + + var AcceptableUseNew = @"

ACCEPTABLE USE POLICY

+
    +
  1. + General +
      +
    1. This Acceptable Use Policy sets out how we permit you to use any of our Platforms. Your compliance with this Acceptable Use Policy is a condition of your use of the Platform.
    2. +
    3. Capitalised terms have the meaning given to them in the terms of use for the Platform which are available at https://www.dls.nhs.uk/v2/LearningSolutions/Terms.
    4. +
    +
  2. +
  3. + Acceptable use +
      +
    1. You are permitted to use the Platform as set out in the Terms and for the purpose of personal study.
    2. +
    3. You must not use any part of the Content on the Platform for commercial purposes without obtaining a licence to do so from us or our licensors.
    4. +
    5. If you print off, copy, download, share or repost any part of the Platform in breach of this Acceptable Use Policy, your right to use the Platform will cease immediately and you must, at our option, return or destroy any copies of the materials you have made.
    6. +
    7. Our status (and that of any identified contributors) as the authors of Content on the Platform must always be acknowledged (except in respect of Third-Party Content).
    8. +
    +
  4. +
  5. + Prohibited uses +
      +
    1. + You may not use the Platform: +
        +
      1. in any way that breaches any applicable local, national or international law or regulation;
      2. +
      3. in any way that is unlawful or fraudulent or has any unlawful or fraudulent purpose or effect;
      4. +
      5. in any way that infringes the rights of, or restricts or inhibits the use and enjoyment of this site by any third party;
      6. +
      7. for the purpose of harming or attempting to harm minors in any way;
      8. +
      9. to bully, insult, intimidate or humiliate any person;
      10. +
      11. to send, knowingly receive, upload, download, use or re-use any material which does not comply with our Content Standards as set out in paragraph 4;
      12. +
      13. to transmit, or procure the sending of, any unsolicited or unauthorised advertising or promotional material or any other form of similar solicitation (spam), or any unwanted or repetitive content that may cause disruption to the Platform or diminish the user experience, of the Platform’s usefulness or relevant to others;
      14. +
      15. to do any act or thing with the intention of disrupting the Platform in any way, including uploading any malware or links to malware, or introduce any virus, trojan, worm, logic bomb or other material that is malicious or technologically harmful or other potentially damaging items into the Platform;
      16. +
      17. to knowingly transmit any data, send or upload any material that contains viruses, Trojan horses, worms, time-bombs, keystroke loggers, spyware, adware or any other harmful programs or similar computer code designed to adversely affect the operation of any computer software or hardware; or
      18. +
      19. to upload terrorist content.
      20. +
      +
    2. +
    3. + You also agree: +
        +
      1. to follow any reasonable instructions given to you by us in connection with your use of the Platform;
      2. +
      3. to respect the rights and dignity of others, in order to maintain the ethos and good reputation of the NHS, the public good generally and the spirit of cooperation between those studying and working within the health and care sector. In particular, you must act in a professional manner with regard to all other users of the Platform at all times;
      4. +
      5. + not to modify or attempt to modify any of the Content, save: +
          +
        1. in respect of Contributions;
        2. +
        3. where you are the editor of a catalogue within the Learning Hub, you may alter Content within that catalogue;
        4. +
        +
      6. +
      7. not to download or copy any of the Content to electronic or photographic media;
      8. +
      9. not to reproduce any part of the Content by any means or under any format other than as a reasonable aid to your personal study;
      10. +
      11. not to reproduce, duplicate, copy or re-sell any Content in contravention of the provisions of this Acceptable Use Policy; and
      12. +
      13. not to use tools that automatically perform actions on your behalf;
      14. +
      15. not to upload any content that infringes the intellectual property rights, privacy rights or any other rights of any person or organisation; and
      16. +
      17. not to attempt to disguise your identity or that of your organisation;
      18. +
      19. + not to access without authority, interfere with, damage or disrupt: +
          +
        1. any part of the Platform;
        2. +
        3. any equipment or network on which the Platform is stored;
        4. +
        5. any software used in the provision of the Platform;
        6. +
        7. the server on which the Platform is stored;
        8. +
        9. any computer or database connected to the Platform; or
        10. +
        11. any equipment or network or software owned or used by any third party.
        12. +
        +
      20. +
      21. not to attack the Platform via a denial-of-service attack or a distributed denial-of-service attack.
      22. +
      +
    4. +
    +
  6. +
  7. + Content standards +
      +
    1. The content standards set out in this paragraph 4 (Content Standards) apply to any and all Contributions.
    2. +
    3. The Content Standards must be complied with in spirit as well as to the letter. The Content Standards apply to each part of any Contribution as well as to its whole.
    4. +
    5. We will determine, in our discretion, whether a Contribution breaches the Content Standards.
    6. +
    7. + A Contribution must: +
        +
      1. be accurate (where it states facts);
      2. +
      3. be genuinely held (where it states opinions); and
      4. +
      5. comply with the law applicable in England and Wales and in any country from which it is posted.
      6. +
      +
    8. +
    9. + A Contribution must not: +
        +
      1. + contain misinformation that is likely to harm users, patients/service users, health and care workers, or the general public’s wellbeing, safety, trust and reputation, including the reputation of the NHS or any part of it. This could include false and misleading information relating to disease prevention and treatment, conspiracy theories, content that encourages discrimination, harassment or physical violence, content originating from misinformation campaigns, and content edited or manipulated in such a way as to constitute misinformation; +
      2. +
      3. + contain any content or link to any content: +
          +
        1. which is created for advertising, promotional or other commercial purposes, including links, logos and business names;
        2. +
        3. which requires a subscription or payment to gain access to such content;
        4. +
        5. in which the user has a commercial interest;
        6. +
        7. which promotes a business name and/or logo;
        8. +
        9. which contains a link to an app via iOS or Google Play; or
        10. +
        11. which has as its purpose or effect the collection and sharing of personal data;
        12. +
        +
      4. +
      5. + be irrelevant to the purpose or aims of the Platform or while addressing relevant subject matter, contain an irrelevant, unsuitable or inappropriate slant (for example relating to potentially controversial opinions or beliefs of any kind intended to influence others); +
      6. +
      7. be defamatory of any person;
      8. +
      9. be obscene, offensive, hateful or inflammatory, or contain any profanity;
      10. +
      11. bully, insult, intimidate or humiliate;
      12. +
      13. + encourage suicide, substance abuse, eating disorders or other acts of self-harm.Content related to self - harm for the purposes of therapy, education and the promotion of general wellbeing may be uploaded, but we reserve the right to make changes to the way in which it is accessed in order that users do not view it accidentally; +
      14. +
      15. + feature sexual imagery purely intended to stimulate sexual arousal. Non - pornographic content relating to sexual health and related issues, surgical procedures and the results of surgical procedures, breastfeeding, therapy, education and the promotion of general wellbeing may be uploaded, but we reserve the right to make changes to the way in which it is accessed in order that users do not view it accidentally; +
      16. +
      17. + include child sexual abuse material. Content relating to safeguarding which addresses the subject of child sexual abuse may be uploaded, but we reserve the right to make changes to the way in which it is accessed in order that users do not view it accidentally; +
      18. +
      19. + incite or glorify violence including content designed principally for the purposes of causing reactions of shock or disgust; +
      20. +
      21. + promote discrimination or discriminate in respect of the protected characteristics set out in the Equality Act 2010, being age, disability, gender reassignment, marriage and civil partnership, pregnancy and maternity, race, nationality, religion or belief, sex, and sexual orientation; +
      22. +
      23. infringe any copyright, database right or trade mark of any other person;
      24. +
      25. be likely to deceive any person;
      26. +
      27. breach any legal duty owed to a third party, such as a contractual duty or a duty of confidence;
      28. +
      29. + promote any illegal content or activity, including but not limited to the encouragement, promotion, justification, praise or provision of aid to dangerous persons or organisations, including extremists, terrorists and terrorist organisations and those engaged in any form of criminal activity; +
      30. +
      31. be in contempt of court;
      32. +
      33. + be threatening, abuse or invade another''s privacy, or cause annoyance, inconvenience or needless anxiety; +
      34. +
      35. be likely to harass, bully, shame, degrade, upset, embarrass, alarm or annoy any other person;
      36. +
      37. impersonate any person or misrepresent your identity or affiliation with any person;
      38. +
      39. + advocate, promote, incite any party to commit, or assist any unlawful or criminal act such as (by way of example only) copyright infringement or computer misuse; +
      40. +
      41. + contain a statement which you know or believe, or have reasonable grounds for believing, that members of the public to whom the statement is, or is to be, published are likely to understand as a direct or indirect encouragement or other inducement to the commission, preparation or instigation of acts of terrorism; +
      42. +
      43. contain harmful material;
      44. +
      45. give the impression that the Contribution emanates from us, if this is not the case; or
      46. +
      47. disclose any third party’s confidential information, identity, personally identifiable information or personal data (including data concerning health).
      48. +
      +
    10. +
    11. You acknowledge and accept that, when using the Platform and accessing the Content, some Content that has been uploaded by third parties may be factually inaccurate, or the topics addressed by the Content may be offensive, indecent, or objectionable in nature. We are not responsible (legally or otherwise) for any claim you may have in relation to the Content.
    12. +
    13. When producing Content to upload to the Platform, we encourage you to implement NICE guideline recommendations and ensure that sources of evidence are valid (for example, by peer review).
    14. +
    +
  8. +
  9. + Metadata +

    When making any Contribution, you must where prompted include a sufficient description of the Content so that other users can understand the description, source, and age of the Content. For example, if Content has been quality assured, then the relevant information should be posted in the appropriate field. All metadata fields on the Platform must be completed appropriately before initiating upload. Including the correct information is important in order to help other users locate the Content (otherwise the Content may not appear in search results for others to select).

    +
  10. +
  11. + Updates +

    You must update each Contribution at least once every 3 (three) years, or update or remove it should it cease to be relevant or become outdated or revealed or generally perceived to be unsafe or otherwise unsuitable for inclusion on the Platform.

    +
  12. +
  13. + Accessibility +

    Where practicable, all Contributions should aim to meet the accessibility standards as described in our Accessibility Statement + [https://www.dls.nhs.uk/v2/LearningSolutions/AccessibilityHelp] and as set out in the AA Standard Web Content Accessibility Guidelines v2.1 found here: + https://www.w3.org/TR/WCAG21/.

    +
  14. +
  15. + Rules about linking to the Platform +
      +
    1. The Platform must not be framed on any other site.
    2. +
    3. You may directly link to any Content that is hosted on the Platform, however, please be aware that not all links will continue to be available indefinitely. We will use our best efforts to ensure that all links are valid at the time of creating the related Content but cannot be held responsible for any subsequent changes to the link address or related Content.
    4. +
    +
  16. +
  17. + No text or data mining, or web scraping +
      +
    1. + You shall not conduct, facilitate, authorize or permit any text or data mining or web scraping in relation to the Platform or any services provided via, or in relation to, the Platform. This includes using (or permitting, authorizing or attempting the use of): +
        +
      1. any ""robot"", ""bot"", ""spider"", ""scraper"" or other automated device, program, tool, algorithm, code, process or methodology to access, obtain, copy, monitor or republish any portion of the Platform or any data, Content, information or services accessed via the same; and/or
      2. +
      3. any automated analytical technique aimed at analyzing text and data in digital form to generate information which includes but is not limited to patterns, trends, and correlations.
      4. +
      +
    2. +
    3. The provisions in this paragraph should be treated as an express reservation of our rights in this regard, including for the purposes of Article 4(3) of Digital Copyright Directive ((EU) 2019/790).
    4. +
    5. This paragraph shall not apply insofar as (but only to the extent that) we are unable to exclude or limit text or data mining or web scraping activity by contract under the laws which are applicable to us.
    6. +
    +
  18. +
  19. + Breach of this Acceptable Use Policy +

    Failure to comply with this Acceptable Use Policy constitutes a material breach of this Acceptable Use Policy upon which you are permitted to use the Platform and may result in our taking all or any of the following actions:

    +
      +
    1. Immediate, temporary, or permanent withdrawal of your right to use the Platform;
    2. +
    3. Immediate, temporary, or permanent removal of any Contribution uploaded by you to the Platform;
    4. +
    5. Issue of a warning to you;
    6. +
    7. Legal proceedings against you for reimbursement of all costs...
    8. +
    9. Disclosure of such information to law enforcement authorities...
    10. +
    11. Any other action we reasonably deem appropriate.
    12. +
    +
  20. +
"; + + Execute.Sql(@"UPDATE Config SET UpdatedDate = GETDATE() ,ConfigText =N'" + AcceptableUseNew + "'" + + "where ConfigName='AcceptableUse' AND IsHtml = 1"); + } + public override void Down() + { + var PrivacyPolicyOld = @"

PRIVACY NOTICE

    This page explains our privacy policy and how we will use and protect any information about you that you give to us or that we collate when you visit this website, or undertake employment with NHS England (NHSE or we/us/our), or participate in any NHSE sponsored training, education and development including via any of our training platform websites (Training).
    This privacy notice is intended to provide transparency regarding what personal data NHSE may hold about you, how it will be processed and stored, how long it will be retained and who may have access to your data.
    Personal data is any information relating to an identified or identifiable living person (known as the data subject). An identifiable person is one who can be identified, directly or indirectly, in particular by reference to an identifier such as a name, an identification number or factors specific to the physical, genetic or mental identity of that person, for example.

1 OUR ROLE IN THE NHS

    We are here to improve the quality of healthcare for the people and patients of England through education, training and lifelong development of staff and appropriate planning of the workforce required to deliver healthcare services in England.
    We aim to enable high quality, effective, compassionate care and to identify the right people with the right skills and the right values. All the information we collect is to support these objectives.

2 IMPORTANT INFORMATION

    NHSE is the data controller in respect of any personal data it holds concerning trainees in Training, individuals employed by NHSE and individuals accessing NHSE’s website.
    We have appointed a data protection officer (DPO) who is responsible for overseeing questions in relation to this privacy policy. If you have any questions about this privacy policy or our privacy practices, want to know more about how your information will be used, or make a request to exercise your legal rights, please contact our DPO in the following ways:
    Name: Andrew Todd
    Email address: gdpr@hee.nhs.uk
    Postal address: NHS England of Skipton House, 80 London Road, London SE1 6LH

3 WHAT THIS PRIVACY STATEMENT COVERS

    This privacy statement only covers the processing of personal data by NHSE that NHSE has obtained from data subjects accessing any of NHSE’s websites and from its provision of services and exercise of functions. It does not cover the processing of data by any sites that can be linked to or from NHSE’s websites, so you should always be aware when you are moving to another site and read the privacy statement on that website.
    When providing NHSE with any of your personal data for the first time, for example, when you take up an appointment with NHSE or when you register for any Training, you will be asked to confirm that you have read and accepted the terms of this privacy statement. A copy of your acknowledgement will be retained for reference.
    If our privacy policy changes in any way, we will place an updated version on this page. Regularly reviewing the page ensures you are always aware of what information we collect, how we use it and under what circumstances, if any, we will share it with other parties.

4 WHY NHSE COLLECTS YOUR PERSONAL DATA

    Personal data may be collected from you via the recruitment process, when you register and/or create an account for any Training, during your Annual Review of Competence Progression or via NHSE’s appraisal process. Personal data may also be obtained from Local Education Providers or employing organisations in connection with the functions of NHSE.
    Your personal data is collected and processed for the purposes of and in connection with the functions that NHSE performs with regard to Training and planning. The collection and processing of such data is necessary for the purposes of those functions.
    A full copy of our notification to the Information Commissioner’s Office (ICO) (the UK regulator for data protection issues), can be found on the ICO website here: www.ico.org.uk by searching NHSE’s ICO registration number, which is Z2950066.
    In connection with Training, NHSE collects and uses your personal information for the following purposes:
  1. to manage your Training and programme, including allowing you to access your own learning history;
  2. to quality assure Training programmes and ensure that standards are maintained, including gathering feedback or input on the service, content, or layout of the Training and customising the content and/or layout of the Training;
  3. to identify workforce planning targets;
  4. to maintain patient safety through the management of performance concerns;
  5. to comply with legal and regulatory responsibilities including revalidation;
  6. to contact you about Training updates, opportunities, events, surveys and information that may be of interest to you;
  7. transferring your Training activity records for programmes to other organisations involved in medical training in the healthcare sector. These organisations include professional bodies that you may be a member of, such as a medical royal college or foundation school; or employing organisations, such as trusts;
  8. making your Training activity records visible to specific named individuals, such as tutors, to allow tutors to view their trainees’ activity. We would seek your explicit consent before authorising anyone else to view your records;
  9. providing anonymous, summarised data to partner organisations, such as professional bodies; or local organisations, such as strategic health authorities or trusts;
  10. for NHSE internal review;
  11. to provide HR related support services and Training to you, for clinical professional learner recruitment;
  12. to promote our services;
  13. to monitor our own accounts and records;
  14. to monitor our work, to report on progress made; and
  15. to let us fulfil our statutory obligations and statutory returns as set by the Department of Health and the law (for example complying with NHSE’s legal obligations and regulatory responsibilities under employment law).
    Further information about our use of your personal data in connection with Training can be found in ’A Reference Guide for Postgraduate Foundation and Specialty Training in the UK’, published by the Conference of Postgraduate Medical Deans of the United Kingdom and known as the ‘Gold Guide’, which can be found here: https://www.copmed.org.uk/gold-guide.

5 TYPES OF PERSONAL DATA COLLECTED BY NHSE

    The personal data that NHSE collects when you register for Training enables the creation of an accurate user profile/account, which is necessary for reporting purposes and to offer Training that is relevant to your needs.
    The personal data that is stored by NHSE is limited to information relating to your work, such as your job role, place of work, and membership number for a professional body (e.g. your General Medical Council number). NHSE will never ask for your home address or any other domestic information.
    When accessing Training, you will be asked to set up some security questions, which may contain personal information. These questions enable you to log in if you forget your password and will never be used for any other purpose. The answers that you submit when setting up these security questions are encrypted in the database so no one can view what has been entered, not even NHSE administrators.
    NHSE also store a record of some Training activity, including upload and download of Training content, posts on forums or other communication media, and all enquires to the service desks that support the Training.
    If you do not provide personal data that we need from you when requested, we may not be able to provide services (such as Training) to you. In this case, we may have to cancel such service, but we will notify you at the time if this happens.

6 COOKIES

    When you access NHSE’s website and Training, we want to make them easy, useful and reliable. This sometimes involves placing small amounts of limited information on your device (such as your computer or mobile phone). These small files are known as cookies, and we ask you to agree to their usage in accordance with ICO guidance.
    These cookies are used to improve the services (including the Training) we provide you through, for example:
  1. enabling a service to recognise your device, so you do not have to give the same information several times during one task (e.g. we use a cookie to remember your username if you check the ’Remember Me’ box on a log in page);
  2. recognising that you may already have given a username and password, so you do not need to do it for every web page requested;
  3. measuring how many people are using services, so they can be made easier to use and there is enough capacity to ensure they are fast; and
  4. analysing anonymised data to help us understand how people interact with services so we can make them better.
    We use a series of cookies to monitor website speed and usage, as well as to ensure that any preferences you have selected previously are the same when you return to our website. Please visit our cookie policies page to understand the cookies that we use: https://www.dls.nhs.uk/v2/CookieConsent/CookiePolicy
    Most cookies applied when accessing Training are used to keep track of your input when filling in online forms, known as session-ID cookies, which are exempt from needing consent as they are deemed essential for using the website or Training they apply to. Some cookies, like those used to measure how you use the Training, are not needed for our website to work. These cookies can help us improve the Training, but we’ll only use them if you say it’s OK. We’ll use a cookie to save your settings.
    On a number of pages on our website or Training, we use ’plug-ins’ or embedded media. For example, we might embed YouTube videos. Where we have used this type of content, the suppliers of these services may also set cookies on your device when you visit their pages. These are known as ’third-party’ cookies. To opt-out of third-parties collecting any data regarding your interaction on our website, please refer to their websites for further information.
    We will not use cookies to collect personal data about you. However, if you wish to restrict or block the cookies which are set by our websites or Training, or indeed any other website, you can do this through your browser settings. The ’Help’ function within your browser should tell you how. Alternatively, you may wish to visit www.aboutcookies.org which contains comprehensive information on how to do this on a wide variety of browsers. You will also find details on how to delete cookies from your machine as well as more general information about cookies. Please be aware that restricting cookies may impact on the functionality of our website.

7 LEGAL BASIS FOR PROCESSING

    The retained EU law version of the General Data Protection Regulation ((EU) 2016/679) (UK GDPR) requires that data controllers and organisations that process personal data demonstrate compliance with its provisions. This involves publishing our basis for lawful processing of personal data.
    As personal data is processed for the purposes of NHSE’s statutory functions, NHSE’s legal bases for the processing of personal data as listed in Article 6 of the UK GDPR are as follows:
    Where NHSE processes special categories of personal data, its additional legal bases for processing such data as listed in Article 9 of the UK GDPR are as follows:
    Special categories of personal data include data relating to racial or ethnic origin, political opinions, religious beliefs, sexual orientation and data concerning health.
    Please note that not all of the above legal bases will apply for each type of processing activity that NHSE may undertake. However, when processing any personal data for any particular purpose, one or more of the above legal bases will apply.
    We may seek your consent for some processing activities, for example for sending out invitations to you to Training events and sending out material from other government agencies. If you do not give consent for us to use your data for these purposes, we will not use your data for these purposes, but your data may still be retained by us and used by us for other processing activities based on the above lawful conditions for processing set out above.

8 INFORMATION THAT WE MAY NEED TO SEND YOU

    We may occasionally have to send you information from NHSE, the Department of Health, other public authorities and government agencies about matters of policy where those policy issues impact on Training, workforce planning, or other matters related to NHSE. This is because NHSE is required by statute to exercise functions of the Secretary of State in respect of Training and workforce planning. If you prefer, you can opt out of receiving information about general matters of policy impacting on Training and workforce planning by contacting your Local Office recruitment lead or tel@hee.nhs.uk. The relevant Local Office or a representative from the relevant training platform website will provide you with further advice and guidance regarding any consequences of your request.
    NHSE will not send you generic information from other public authorities and government agencies on issues of government policy.

9 TRANSFERS ABROAD

    The UK GDPR imposes restrictions on the transfer of personal data outside the European Union, to third countries or international organisations, in order to ensure that the level of protection of individuals afforded by the UK GDPR is not undermined.
    Your data may only be transferred abroad where NHSE is assured that a third country, a territory or one or more specific sectors in the third country, or an international organisation ensures an adequate level of protection.

10 HOW WE PROTECT YOUR PERSONAL DATA

    Our processing of all personal data complies with the UK GDPR principles. We have put in place appropriate security measures to prevent your personal data from being accidentally lost, used or accessed in an unauthorised way, altered or disclosed. The security of the data is assured through the implementation of NHSE’s policies on information governance management.
    The personal data we hold may be held as an electronic record on data systems managed by NHSE, or as a paper record. These records are only accessed, seen and used in the following circumstances:
  1. if required and/or permitted by law; or
  2. by NHSE staff who need access to them so they can do their jobs and who are subject to a duty of confidentiality; or
  3. by other partner organisations, including our suppliers, who have been asked to sign appropriate non-disclosure or data sharing agreements and will never be allowed to use the information for commercial purposes.
    We make every effort to keep your personal information accurate and up to date, but in some cases we are reliant on you as the data subject to notify us of any necessary changes to your personal data. If you tell us of any changes in your circumstances, we can update the records with personal data you choose to share with us.
    Information collected by NHSE will never be sold for financial gain or shared with other organisations for commercial purposes.
    We have put in place procedures to deal with any suspected personal data breach and will notify you and any applicable regulator of a breach where we are legally required to do so.

11 SHARING PERSONAL DATA

    So we can provide the right services at the right level, we may share your personal data within services across NHSE and with other third party organisations such as the Department of Health, higher education institutions, clinical placement providers, colleges, faculties, other NHSE Local Offices, the General Medical Council, NHS Trusts/Health Boards/Health and Social Care Trusts, approved academic researchers and other NHS and government agencies where necessary, to provide the best possible Training and to ensure that we discharge NHSEs responsibilities for employment and workforce planning for the NHS. This will be on a legitimate need to know basis only.
    We may also share information, where necessary, to prevent, detect or assist in the investigation of fraud or criminal activity, to assist in the administration of justice, for the purposes of seeking legal advice or exercising or defending legal rights or as otherwise required by the law.
    Where the data is used for analysis and publication by a recipient or third party, any publication will be on an anonymous basis, and will not make it possible to identify any individual. This will mean that the data ceases to become personal data.

12 HOW LONG WE RETAIN YOUR PERSONAL DATA

    We will keep personal data for no longer than necessary to fulfil the purposes we collected it for, in accordance with our records management policy and the NHS records retention schedule within the NHS Records Management Code of Practice at: https://www.england.nhs.uk/contact-us/privacy-notice/nhs-england-as-a-data-controller (as may be amended from time to time).
    In some circumstances you can ask us to delete your data. Please see the “Your rights” section below for further information.
    In some circumstances we will anonymise your personal data (so that it can no longer be associated with you) for research or statistical purposes, in which case we may use this information indefinitely without further notice to you.

13 OPEN DATA

    Open data is data that is published by central government, local authorities and public bodies to help you build products and services. NHSE policy is to observe the Cabinet Office transparency and accountability commitments towards more open use of public data in accordance with relevant and applicable UK legislation.
    NHSE would never share personal data through the open data facility. To this end, NHSE will implement information governance protocols that reflect the ICO’s recommended best practice for record anonymisation, and Office of National Statistics guidance on publication of statistical information.

14 YOUR RIGHTS

14.1 Right to rectification and erasure

    Under the UK GDPR you have the right to rectification of inaccurate personal data and the right to request the erasure of your personal data. However, the right to erasure is not an absolute right and it may be that it is necessary for NHSE to continue to process your personal data for a number of lawful and legitimate reasons.

14.2 Right to object and withdraw your consent

    You have the right in certain circumstances to ask NHSE to stop processing your personal data in relation to any NHSE service. As set out above, you can decide that you do not wish to receive information from NHSE about matters of policy affecting Training and workforce. However, the right to object is not an absolute right and it may be that it is necessary in certain circumstances for NHSE to continue to process your personal data for a number of lawful and legitimate reasons.
    If you object to the way in which NHSE is processing your personal information or if you wish to ask NHSE to stop processing your personal data, please contact your relevant Local Office.
    Please note, if we do stop processing personal data about you, this may prevent NHSE from providing the best possible service to you. Withdrawing your consent will result in your Training account being anonymised and access to the Training removed.

14.3 Right to request access

    You can access a copy of the information NHSE holds about you by writing to NHSE’s Public and Parliamentary Accountability Team. This information is generally available to you free of charge subject to the receipt of appropriate identification. More information about subject access requests can be found here: https://www.hee.nhs.uk/about/contact-us/subject-access-request.

14.4 Right to request a transfer

    The UK GDPR sets out the right for a data subject to have their personal data ported from one controller to another on request in certain circumstances. You should discuss any request for this with your Local Office. This right only applies to automated information which you initially provided consent for us to use or where we used the information to perform a contract with you.

14.5 Right to restrict processing

    You can ask us to suspend the processing of your personal data if you want us to establish the data’s accuracy, where our use of the data is unlawful but you do not want us to erase it, where you need us to hold the data even if we no longer require it as you need it to establish, exercise or defend legal claims or where you have objected to our use of your data but we need to verify whether we have overriding legitimate grounds to use it.

14.6 Complaints

    You have the right to make a complaint at any time to the ICO. We would, however, appreciate the chance to deal with your concerns before you approach the ICO so please contact your Local Office or the DPO in the first instance, using the contact details above.
    You can contact the ICO at the following address:

    The Office of the Information Commissioner
    Wycliffe House
    Water Lane
    Wilmslow
    Cheshire
    SK9 5AF

14.7 Your responsibilities

    It is important that you work with us to ensure that the information we hold about you is accurate and up to date so please inform NHSE if any of your personal data needs to be updated or corrected.
    All communications from NHSE will normally be by email. It is therefore essential for you to maintain an effective and secure email address, or you may not receive information or other important news and information about your employment or Training.
"; + + Execute.Sql(@"UPDATE Config SET UpdatedDate = GETDATE() ,ConfigText =N'" + PrivacyPolicyOld + "'" + + "where ConfigName='PrivacyPolicy' AND IsHtml = 1"); + + var AcceptableUseOld = @"

ACCEPTABLE USE POLICY

+
    +
  1. + General +
      +
    1. This Acceptable Use Policy sets out how we permit you to use any of our Platforms. Your compliance with this Acceptable Use Policy is a condition of your use of the Platform.
    2. +
    3. Capitalised terms have the meaning given to them in the terms of use for the Platform which are available at https://www.dls.nhs.uk/v2/LearningSolutions/Terms.
    4. +
    +
  2. +
  3. + Acceptable use +
      +
    1. You are permitted to use the Platform as set out in the Terms and for the purpose of personal study.
    2. +
    3. You must not use any part of the Content on the Platform for commercial purposes without obtaining a licence to do so from us or our licensors.
    4. +
    5. If you print off, copy, download, share or repost any part of the Platform in breach of this Acceptable Use Policy, your right to use the Platform will cease immediately and you must, at our option, return or destroy any copies of the materials you have made.
    6. +
    7. Our status (and that of any identified contributors) as the authors of Content on the Platform must always be acknowledged (except in respect of Third-Party Content).
    8. +
    +
  4. +
  5. + Prohibited uses +
      +
    1. + You may not use the Platform: +
        +
      1. in any way that breaches any applicable local, national or international law or regulation;
      2. +
      3. in any way that is unlawful or fraudulent or has any unlawful or fraudulent purpose or effect;
      4. +
      5. in any way that infringes the rights of, or restricts or inhibits the use and enjoyment of this site by any third party;
      6. +
      7. for the purpose of harming or attempting to harm minors in any way;
      8. +
      9. to bully, insult, intimidate or humiliate any person;
      10. +
      11. to send, knowingly receive, upload, download, use or re-use any material which does not comply with our Content Standards as set out in paragraph 4;
      12. +
      13. to transmit, or procure the sending of, any unsolicited or unauthorised advertising or promotional material or any other form of similar solicitation (spam), or any unwanted or repetitive content that may cause disruption to the Platform or diminish the user experience, of the Platform’s usefulness or relevant to others;
      14. +
      15. to do any act or thing with the intention of disrupting the Platform in any way, including uploading any malware or links to malware, or introduce any virus, trojan, worm, logic bomb or other material that is malicious or technologically harmful or other potentially damaging items into the Platform;
      16. +
      17. to knowingly transmit any data, send or upload any material that contains viruses, Trojan horses, worms, time-bombs, keystroke loggers, spyware, adware or any other harmful programs or similar computer code designed to adversely affect the operation of any computer software or hardware; or
      18. +
      19. to upload terrorist content.
      20. +
      +
    2. +
    3. + You also agree: +
        +
      1. to follow any reasonable instructions given to you by us in connection with your use of the Platform;
      2. +
      3. to respect the rights and dignity of others, in order to maintain the ethos and good reputation of the NHS, the public good generally and the spirit of cooperation between those studying and working within the health and care sector. In particular, you must act in a professional manner with regard to all other users of the Platform at all times;
      4. +
      5. + not to modify or attempt to modify any of the Content, save: +
          +
        1. in respect of Contributions;
        2. +
        3. where you are the editor of a catalogue within the Learning Hub, you may alter Content within that catalogue;
        4. +
        +
      6. +
      7. not to download or copy any of the Content to electronic or photographic media;
      8. +
      9. not to reproduce any part of the Content by any means or under any format other than as a reasonable aid to your personal study;
      10. +
      11. not to reproduce, duplicate, copy or re-sell any Content in contravention of the provisions of this Acceptable Use Policy; and
      12. +
      13. not to use tools that automatically perform actions on your behalf;
      14. +
      15. not to upload any content that infringes the intellectual property rights, privacy rights or any other rights of any person or organisation; and
      16. +
      17. not to attempt to disguise your identity or that of your organisation;
      18. +
      19. + not to access without authority, interfere with, damage or disrupt: +
          +
        1. any part of the Platform;
        2. +
        3. any equipment or network on which the Platform is stored;
        4. +
        5. any software used in the provision of the Platform;
        6. +
        7. the server on which the Platform is stored;
        8. +
        9. any computer or database connected to the Platform; or
        10. +
        11. any equipment or network or software owned or used by any third party.
        12. +
        +
      20. +
      21. not to attack the Platform via a denial-of-service attack or a distributed denial-of-service attack.
      22. +
      +
    4. +
    +
  6. +
  7. + Content standards +
      +
    1. The content standards set out in this paragraph 4 (Content Standards) apply to any and all Contributions.
    2. +
    3. The Content Standards must be complied with in spirit as well as to the letter. The Content Standards apply to each part of any Contribution as well as to its whole.
    4. +
    5. We will determine, in our discretion, whether a Contribution breaches the Content Standards.
    6. +
    7. + A Contribution must: +
        +
      1. be accurate (where it states facts);
      2. +
      3. be genuinely held (where it states opinions); and
      4. +
      5. comply with the law applicable in England and Wales and in any country from which it is posted.
      6. +
      +
    8. +
    9. + A Contribution must not: +
        +
      1. + contain misinformation that is likely to harm users, patients/service users, health and care workers, or the general public’s wellbeing, safety, trust and reputation, including the reputation of the NHS or any part of it. This could include false and misleading information relating to disease prevention and treatment, conspiracy theories, content that encourages discrimination, harassment or physical violence, content originating from misinformation campaigns, and content edited or manipulated in such a way as to constitute misinformation; +
      2. +
      3. + contain any content or link to any content: +
          +
        1. which is created for advertising, promotional or other commercial purposes, including links, logos and business names;
        2. +
        3. which requires a subscription or payment to gain access to such content;
        4. +
        5. in which the user has a commercial interest;
        6. +
        7. which promotes a business name and/or logo;
        8. +
        9. which contains a link to an app via iOS or Google Play; or
        10. +
        11. which has as its purpose or effect the collection and sharing of personal data;
        12. +
        +
      4. +
      5. + be irrelevant to the purpose or aims of the Platform or while addressing relevant subject matter, contain an irrelevant, unsuitable or inappropriate slant (for example relating to potentially controversial opinions or beliefs of any kind intended to influence others); +
      6. +
      7. be defamatory of any person;
      8. +
      9. be obscene, offensive, hateful or inflammatory, or contain any profanity;
      10. +
      11. bully, insult, intimidate or humiliate;
      12. +
      13. + encourage suicide, substance abuse, eating disorders or other acts of self-harm.Content related to self - harm for the purposes of therapy, education and the promotion of general wellbeing may be uploaded, but we reserve the right to make changes to the way in which it is accessed in order that users do not view it accidentally; +
      14. +
      15. + feature sexual imagery purely intended to stimulate sexual arousal. Non - pornographic content relating to sexual health and related issues, surgical procedures and the results of surgical procedures, breastfeeding, therapy, education and the promotion of general wellbeing may be uploaded, but we reserve the right to make changes to the way in which it is accessed in order that users do not view it accidentally; +
      16. +
      17. + include child sexual abuse material. Content relating to safeguarding which addresses the subject of child sexual abuse may be uploaded, but we reserve the right to make changes to the way in which it is accessed in order that users do not view it accidentally; +
      18. +
      19. + incite or glorify violence including content designed principally for the purposes of causing reactions of shock or disgust; +
      20. +
      21. + promote discrimination or discriminate in respect of the protected characteristics set out in the Equality Act 2010, being age, disability, gender reassignment, marriage and civil partnership, pregnancy and maternity, race, nationality, religion or belief, sex, and sexual orientation; +
      22. +
      23. infringe any copyright, database right or trade mark of any other person;
      24. +
      25. be likely to deceive any person;
      26. +
      27. breach any legal duty owed to a third party, such as a contractual duty or a duty of confidence;
      28. +
      29. + promote any illegal content or activity, including but not limited to the encouragement, promotion, justification, praise or provision of aid to dangerous persons or organisations, including extremists, terrorists and terrorist organisations and those engaged in any form of criminal activity; +
      30. +
      31. be in contempt of court;
      32. +
      33. + be threatening, abuse or invade another''s privacy, or cause annoyance, inconvenience or needless anxiety; +
      34. +
      35. be likely to harass, bully, shame, degrade, upset, embarrass, alarm or annoy any other person;
      36. +
      37. impersonate any person or misrepresent your identity or affiliation with any person;
      38. +
      39. + advocate, promote, incite any party to commit, or assist any unlawful or criminal act such as (by way of example only) copyright infringement or computer misuse; +
      40. +
      41. + contain a statement which you know or believe, or have reasonable grounds for believing, that members of the public to whom the statement is, or is to be, published are likely to understand as a direct or indirect encouragement or other inducement to the commission, preparation or instigation of acts of terrorism; +
      42. +
      43. contain harmful material;
      44. +
      45. give the impression that the Contribution emanates from us, if this is not the case; or
      46. +
      47. disclose any third party’s confidential information, identity, personally identifiable information or personal data (including data concerning health).
      48. +
      +
    10. +
    11. You acknowledge and accept that, when using the Platform and accessing the Content, some Content that has been uploaded by third parties may be factually inaccurate, or the topics addressed by the Content may be offensive, indecent, or objectionable in nature. We are not responsible (legally or otherwise) for any claim you may have in relation to the Content.
    12. +
    13. When producing Content to upload to the Platform, we encourage you to implement NICE guideline recommendations and ensure that sources of evidence are valid (for example, by peer review).
    14. +
    +
  8. +
  9. + Metadata +
      + When making any Contribution, you must where prompted include a sufficient description of the Content so that other users can understand the description, source, and age of the Content. For example, if Content has been quality assured, then the relevant information should be posted in the appropriate field. All metadata fields on the Platform must be completed appropriately before initiating upload. Including the correct information is important in order to help other users locate the Content (otherwise the Content may not appear in search results for others to select). +
    +
  10. +
  11. + Updates +
      + You must update each Contribution at least once every 3 (three) years, or update or remove it should it cease to be relevant or become outdated or revealed or generally perceived to be unsafe or otherwise unsuitable for inclusion on the Platform. +
    +
  12. +
  13. + Accessibility +
      + Where practicable, all Contributions should aim to meet the accessibility standards as described in our Accessibility Statement + [https://www.dls.nhs.uk/v2/LearningSolutions/AccessibilityHelp] and as set out in the AA Standard Web Content Accessibility Guidelines v2.1 found here: + https://www.w3.org/TR/WCAG21/. +
    +
  14. +
  15. + Rules about linking to the Platform +
      +
    1. The Platform must not be framed on any other site.
    2. +
    3. You may directly link to any Content that is hosted on the Platform, however, please be aware that not all links will continue to be available indefinitely. We will use our best efforts to ensure that all links are valid at the time of creating the related Content but cannot be held responsible for any subsequent changes to the link address or related Content.
    4. +
    +
  16. +
  17. + No text or data mining, or web scraping +
      +
    1. + You shall not conduct, facilitate, authorize or permit any text or data mining or web scraping in relation to the Platform or any services provided via, or in relation to, the Platform. This includes using (or permitting, authorizing or attempting the use of): +
        +
      1. any ""robot"", ""bot"", ""spider"", ""scraper"" or other automated device, program, tool, algorithm, code, process or methodology to access, obtain, copy, monitor or republish any portion of the Platform or any data, Content, information or services accessed via the same; and/or
      2. +
      3. any automated analytical technique aimed at analyzing text and data in digital form to generate information which includes but is not limited to patterns, trends, and correlations.
      4. +
      +
    2. +
    3. The provisions in this paragraph should be treated as an express reservation of our rights in this regard, including for the purposes of Article 4(3) of Digital Copyright Directive ((EU) 2019/790).
    4. +
    5. This paragraph shall not apply insofar as (but only to the extent that) we are unable to exclude or limit text or data mining or web scraping activity by contract under the laws which are applicable to us.
    6. +
    +
  18. +
  19. + Breach of this Acceptable Use Policy +
      + Failure to comply with this Acceptable Use Policy constitutes a material breach of this Acceptable Use Policy upon which you are permitted to use the Platform and may result in our taking all or any of the following actions: +
    1. immediate, temporary, or permanent withdrawal of your right to use the Platform;
    2. +
    3. immediate, temporary, or permanent removal of any Contribution uploaded by you to the Platform;
    4. +
    5. issue of a warning to you;
    6. +
    7. legal proceedings against you for reimbursement of all costs on an indemnity basis (including, but not limited to, reasonable administrative and legal costs) resulting from the breach, and/or further legal action against you;
    8. +
    9. disclosure of such information to law enforcement authorities as we reasonably feel is necessary or as required by law; and/or
    10. +
    11. any other action we reasonably deem appropriate.
    12. +
    +
  20. +
"; + + Execute.Sql(@"UPDATE Config SET UpdatedDate = GETDATE() ,ConfigText =N'" + AcceptableUseOld + "'" + + "where ConfigName='AcceptableUse' AND IsHtml = 1"); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202507221424_AddNewColumnsInSelfAssessments.cs b/DigitalLearningSolutions.Data.Migrations/202507221424_AddNewColumnsInSelfAssessments.cs new file mode 100644 index 0000000000..7b6c381573 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202507221424_AddNewColumnsInSelfAssessments.cs @@ -0,0 +1,22 @@ +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + [Migration(202507221424)] + public class AddNewColumnsInSelfAssessments : Migration + { + public override void Up() + { + Alter.Table("SelfAssessments") + .AddColumn("RetirementDate").AsDateTime().Nullable() + .AddColumn("EnrolmentCutoffDate").AsDateTime().Nullable() + .AddColumn("RetirementReason").AsString(2000).Nullable(); + } + + public override void Down() + { + Delete.Column("RetirementDate").FromTable("SelfAssessments"); + Delete.Column("EnrolmentCutoffDate").FromTable("SelfAssessments"); + Delete.Column("RetirementReason").FromTable("SelfAssessments"); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202507240953_Alter_GetActivitiesForDelegateEnrolment_Retired_SA.cs b/DigitalLearningSolutions.Data.Migrations/202507240953_Alter_GetActivitiesForDelegateEnrolment_Retired_SA.cs new file mode 100644 index 0000000000..abf874dd9b --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202507240953_Alter_GetActivitiesForDelegateEnrolment_Retired_SA.cs @@ -0,0 +1,18 @@ +using FluentMigrator; + +namespace DigitalLearningSolutions.Data.Migrations +{ + [Migration(202507240953)] + public class Alter_GetActivitiesForDelegateEnrolment_Retired_SA : Migration + { + public override void Up() + { + Execute.Sql(Properties.Resources.TD_5535_Alter_GetActivitiesForDelegateEnrolment_Up); + } + public override void Down() + { + Execute.Sql(Properties.Resources.TD_5535_Alter_GetActivitiesForDelegateEnrolment_Down); + } + } +} + diff --git a/DigitalLearningSolutions.Data.Migrations/202508181154_CreateOrAlterMoveCompetenciesAndGroups.cs b/DigitalLearningSolutions.Data.Migrations/202508181154_CreateOrAlterMoveCompetenciesAndGroups.cs new file mode 100644 index 0000000000..60ac1b3cfb --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202508181154_CreateOrAlterMoveCompetenciesAndGroups.cs @@ -0,0 +1,19 @@ +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + [Migration(202508181154)] + public class CreateOrAlterMoveCompetenciesAndGroups : Migration + { + public override void Up() + { + Execute.Sql(Properties.Resources.TD_483_uspMoveCompetencyInSelfAssessmentCreateOrAlter_UP); + Execute.Sql(Properties.Resources.TD_483_uspMoveCompetencyGroupInSelfAssessmentCreateOrAlter_UP); + } + public override void Down() + { + Execute.Sql("DROP PROCEDURE IF EXISTS [dbo].[usp_MoveCompetencyGroupInSelfAssessment]"); + Execute.Sql("DROP PROCEDURE IF EXISTS [dbo].[usp_MoveCompetencyInSelfAssessment]"); + Execute.Sql("DROP PROCEDURE IF EXISTS [dbo].[usp_RenumberSelfAssessmentStructure]"); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202508190845_AddSelfAssessmentProcessAgreed .cs b/DigitalLearningSolutions.Data.Migrations/202508190845_AddSelfAssessmentProcessAgreed .cs new file mode 100644 index 0000000000..97ea3eeae0 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202508190845_AddSelfAssessmentProcessAgreed .cs @@ -0,0 +1,18 @@ +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + + [Migration(202508190845)] + public class AddSelfAssessmentProcessAgreed : Migration + { + public override void Up() + { + Alter.Table("CandidateAssessments").AddColumn("SelfAssessmentProcessAgreed").AsDateTime().Nullable(); + } + + public override void Down() + { + Delete.Column("SelfAssessmentProcessAgreed").FromTable("CandidateAssessments"); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202508191440_CreateSendRetiringSelfAssessmentNotificationSP.cs b/DigitalLearningSolutions.Data.Migrations/202508191440_CreateSendRetiringSelfAssessmentNotificationSP.cs new file mode 100644 index 0000000000..e657f3d384 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202508191440_CreateSendRetiringSelfAssessmentNotificationSP.cs @@ -0,0 +1,31 @@ +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + [Migration(202508191440)] + public class CreateSendRetiringSelfAssessmentNotificationSP : Migration + { + public override void Up() + { + string generalTELTeam = + @"[ + { ""FirstName"":"""", ""LastName"":"""", ""Email"":""Support@dls.nhs.uk"" }, + { ""FirstName"":""Anna"", ""LastName"":""Athanasopoulou"", ""Email"":""anna.athanasopoulou@nhs.net"" }, + { ""FirstName"":""Benjamin"", ""LastName"":""Witton"", ""Email"":""Benjamin.witton1@nhs.net"" } + ]"; + + Execute.Sql(@$"IF NOT EXISTS (SELECT ConfigID FROM Config WHERE ConfigName = 'GeneralTELTeam') + BEGIN + INSERT INTO Config VALUES ('GeneralTELTeam', '{generalTELTeam}', 0,GETDATE(), GETDATE()) + END" + ); + + Execute.Sql(Properties.Resources.TD_5552_SendRetiringNotification); + } + public override void Down() + { + Execute.Sql(@"DELETE FROM Config WHERE ConfigName = N'GeneralTELTeam'"); + + Execute.Sql("DROP PROCEDURE [dbo].[SendRetiringSelfAssessmentNotification]"); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202509180915_Alter_SendRetiringSelfAssessmentNotification.cs b/DigitalLearningSolutions.Data.Migrations/202509180915_Alter_SendRetiringSelfAssessmentNotification.cs new file mode 100644 index 0000000000..8e870cd933 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202509180915_Alter_SendRetiringSelfAssessmentNotification.cs @@ -0,0 +1,19 @@ + + +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + + [Migration(202509180915)] + public class Alter_SendRetiringSelfAssessmentNotification : Migration + { + public override void Up() + { + Execute.Sql(Properties.Resources.TD_5552_Alter_SendRetiringSelfAssessmentNotification_Up); + } + public override void Down() + { + Execute.Sql(Properties.Resources.TD_5552_Alter_SendRetiringSelfAssessmentNotification_Down); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/DigitalLearningSolutions.Data.Migrations.csproj b/DigitalLearningSolutions.Data.Migrations/DigitalLearningSolutions.Data.Migrations.csproj index aad3eb5051..ef4e4a02b1 100644 --- a/DigitalLearningSolutions.Data.Migrations/DigitalLearningSolutions.Data.Migrations.csproj +++ b/DigitalLearningSolutions.Data.Migrations/DigitalLearningSolutions.Data.Migrations.csproj @@ -19,7 +19,7 @@ - + diff --git a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs index 5b5d5f74dd..79fe3eb560 100644 --- a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs +++ b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs @@ -2237,6 +2237,58 @@ internal static string TD_4634_Alter_GetCompletedCoursesForCandidate_UP { } } + /// + /// Looks up a localized string similar to + ///CREATE OR ALTER PROCEDURE usp_RenumberSelfAssessmentStructure + /// @SelfAssessmentID INT + ///AS + ///BEGIN + /// SET NOCOUNT ON; + /// + /// /* + /// Step 1: Build an ordered list of groups + /// - Each group is ranked by its current Min(Ordering) + /// - Ungrouped competencies (NULL CompetencyGroupID) are treated as their own "pseudo group" + /// */ + /// ;WITH GroupRanks AS ( + /// SELECT + /// CompetencyGroupID, + /// ROW_NUMBER() OVER (ORDER BY MIN(Ordering)) AS GroupRank + /// FROM Sel [rest of string was truncated]";. + /// + internal static string TD_483_uspMoveCompetencyGroupInSelfAssessmentCreateOrAlter_UP { + get { + return ResourceManager.GetString("TD-483-uspMoveCompetencyGroupInSelfAssessmentCreateOrAlter_UP", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CREATE OR ALTER PROCEDURE usp_MoveCompetencyInSelfAssessment + /// @SelfAssessmentID INT, + /// @CompetencyID INT, + /// @Direction NVARCHAR(10) + ///AS + ///BEGIN + /// SET NOCOUNT ON; + /// + /// DECLARE @GroupID INT, @CurrentOrder INT; + /// + /// SELECT + /// @GroupID = CompetencyGroupID, + /// @CurrentOrder = Ordering + /// FROM SelfAssessmentStructure + /// WHERE SelfAssessmentID = @SelfAssessmentID AND CompetencyID = @CompetencyID; + /// + /// IF @GroupID IS NULL + /// BEGIN + /// -- Can't reorder ungrouped competencies [rest of string was truncated]";. + /// + internal static string TD_483_uspMoveCompetencyInSelfAssessmentCreateOrAlter_UP { + get { + return ResourceManager.GetString("TD-483-uspMoveCompetencyInSelfAssessmentCreateOrAlter_UP", resourceCulture); + } + } + /// /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[GetActivitiesForDelegateEnrolment] Script Date: 22/10/2024 16:55:08 ******/ ///SET ANSI_NULLS ON @@ -2392,6 +2444,331 @@ internal static string TD_4950_dboGetOtherCentresForSelfAssessmentCreateOrAlter } } + /// + /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[SendExpiredTBCReminders] Script Date: 11/03/2025 13:13:15 ******/ + ///SET ANSI_NULLS ON + ///GO + /// + ///SET QUOTED_IDENTIFIER ON + ///GO + /// + ///-- ============================================= + ///-- Author: Kevin Whittaker + ///-- Create date: 17/08/2018 + ///-- Description: Uses DB mail to send reminders to delegates on courses with a TBC date within 1 month. + ///-- ============================================= + ///ALTER PROCEDURE [dbo].[SendExpiredTBCReminders] + /// -- Add the parameters for the stor [rest of string was truncated]";. + /// + internal static string TD_5412_Alter_SendExpiredTBCReminders_Down { + get { + return ResourceManager.GetString("TD_5412_Alter_SendExpiredTBCReminders_Down", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[SendExpiredTBCReminders] Script Date: 11/03/2025 13:13:15 ******/ + ///SET ANSI_NULLS ON + ///GO + /// + ///SET QUOTED_IDENTIFIER ON + ///GO + /// + ///-- ============================================= + ///-- Author: Kevin Whittaker + ///-- Create date: 17/08/2018 + ///-- Description: Uses DB mail to send reminders to delegates on courses with a TBC date within 1 month. + ///-- ============================================= + ///ALTER PROCEDURE [dbo].[SendExpiredTBCReminders] + /// -- Add the parameters for the stor [rest of string was truncated]";. + /// + internal static string TD_5412_Alter_SendExpiredTBCReminders_Up { + get { + return ResourceManager.GetString("TD_5412_Alter_SendExpiredTBCReminders_Up", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[ReorderFrameworkCompetency] Script Date: 24/04/2025 09:23:17 ******/ + ///SET ANSI_NULLS ON + ///GO + /// + ///SET QUOTED_IDENTIFIER ON + ///GO + /// + ///-- ============================================= + ///-- Author: Kevin Whittaker + ///-- Create date: 04/01/2021 + ///-- Description: Reorders the FrameworkCompetencies in a given FrameworkCompetencyGroup - moving the given competency up or down. + ///-- ============================================= + ///ALTER PROCEDURE [dbo].[ReorderFrameworkCompetency] + /// [rest of string was truncated]";. + /// + internal static string TD_5447_Alter_ReorderFrameworkCompetency_Down { + get { + return ResourceManager.GetString("TD_5447_Alter_ReorderFrameworkCompetency_Down", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[ReorderFrameworkCompetency] Script Date: 24/04/2025 09:23:17 ******/ + ///SET ANSI_NULLS ON + ///GO + /// + ///SET QUOTED_IDENTIFIER ON + ///GO + /// + ///-- ============================================= + ///-- Author: Kevin Whittaker + ///-- Create date: 04/01/2021 + ///-- Description: Reorders the FrameworkCompetencies in a given FrameworkCompetencyGroup - moving the given competency up or down. + ///-- ============================================= + ///ALTER PROCEDURE [dbo].[ReorderFrameworkCompetency] + /// [rest of string was truncated]";. + /// + internal static string TD_5447_Alter_ReorderFrameworkCompetency_Up { + get { + return ResourceManager.GetString("TD_5447_Alter_ReorderFrameworkCompetency_Up", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[SendExpiredTBCReminders] Script Date: 16/04/2025 10:50:12 ******/ + ///SET ANSI_NULLS ON + ///GO + /// + ///SET QUOTED_IDENTIFIER ON + ///GO + /// + ///-- ============================================= + ///-- Author: Kevin Whittaker + ///-- Create date: 17/08/2018 + ///-- Description: Uses DB mail to send reminders to delegates on courses with a TBC date within 1 month. + ///-- ============================================= + ///ALTER PROCEDURE [dbo].[SendExpiredTBCReminders] + /// -- Add the parameters for the stor [rest of string was truncated]";. + /// + internal static string TD_5514_Alter_SendExpiredTBCReminders_Down { + get { + return ResourceManager.GetString("TD_5514_Alter_SendExpiredTBCReminders_Down", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[SendExpiredTBCReminders] Script Date: 16/04/2025 10:50:12 ******/ + ///SET ANSI_NULLS ON + ///GO + /// + ///SET QUOTED_IDENTIFIER ON + ///GO + /// + ///-- ============================================= + ///-- Author: Kevin Whittaker + ///-- Create date: 17/08/2018 + ///-- Description: Uses DB mail to send reminders to delegates on courses with a TBC date within 1 month. + ///-- ============================================= + ///ALTER PROCEDURE [dbo].[SendExpiredTBCReminders] + /// -- Add the parameters for the stor [rest of string was truncated]";. + /// + internal static string TD_5514_Alter_SendExpiredTBCReminders_Up { + get { + return ResourceManager.GetString("TD_5514_Alter_SendExpiredTBCReminders_Up", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[GetActivitiesForDelegateEnrolment] Script Date: 24/07/2025 02:06:43 ******/ + ///SET ANSI_NULLS ON + ///GO + /// + ///SET QUOTED_IDENTIFIER ON + ///GO + /// + ///-- ============================================= + ///-- Author: Kevin Whittaker + ///-- Create date: 24/01/2023 + ///-- Description: Returns active available for delegate enrolment based on original GetActiveAvailableCustomisationsForCentreFiltered_V6 sproc but adjusted for user account refactor and filters properly for category. + ///-- ========= [rest of string was truncated]";. + /// + internal static string TD_5535_Alter_GetActivitiesForDelegateEnrolment_Down { + get { + return ResourceManager.GetString("TD_5535_Alter_GetActivitiesForDelegateEnrolment_Down", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[GetActivitiesForDelegateEnrolment] Script Date: 24/07/2025 02:06:43 ******/ + ///SET ANSI_NULLS ON + ///GO + /// + ///SET QUOTED_IDENTIFIER ON + ///GO + /// + ///-- ============================================= + ///-- Author: Kevin Whittaker + ///-- Create date: 24/01/2023 + ///-- Description: Returns active available for delegate enrolment based on original GetActiveAvailableCustomisationsForCentreFiltered_V6 sproc but adjusted for user account refactor and filters properly for category. + ///-- ========= [rest of string was truncated]";. + /// + internal static string TD_5535_Alter_GetActivitiesForDelegateEnrolment_Up { + get { + return ResourceManager.GetString("TD_5535_Alter_GetActivitiesForDelegateEnrolment_Up", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[SendRetiringSelfAssessmentNotification] Script Date: 18/09/2025 09:03:21 ******/ + ///SET ANSI_NULLS ON + ///GO + /// + ///SET QUOTED_IDENTIFIER ON + ///GO + /// + ///-- ============================================= + ///-- Author: Auldrin Possa + ///-- Create date: 04/08/2015 + ///-- Description: Uses DB mail to send notification to delegates on retiring self assessment. + ///-- ============================================= + ///ALTER PROCEDURE [dbo].[SendRetiringSelfAssessmentNotification] + /// @SelfAssessmentId [rest of string was truncated]";. + /// + internal static string TD_5552_Alter_SendRetiringSelfAssessmentNotification_Down { + get { + return ResourceManager.GetString("TD_5552_Alter_SendRetiringSelfAssessmentNotification_Down", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[SendRetiringSelfAssessmentNotification] Script Date: 18/09/2025 09:03:21 ******/ + ///SET ANSI_NULLS ON + ///GO + /// + ///SET QUOTED_IDENTIFIER ON + ///GO + /// + ///-- ============================================= + ///-- Author: Auldrin Possa + ///-- Create date: 04/08/2015 + ///-- Description: Uses DB mail to send notification to delegates on retiring self assessment. + ///-- ============================================= + ///ALTER PROCEDURE [dbo].[SendRetiringSelfAssessmentNotification] + /// @SelfAssessmentId [rest of string was truncated]";. + /// + internal static string TD_5552_Alter_SendRetiringSelfAssessmentNotification_Up { + get { + return ResourceManager.GetString("TD_5552_Alter_SendRetiringSelfAssessmentNotification_Up", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SET ANSI_NULLS ON + ///GO + /// + ///SET QUOTED_IDENTIFIER ON + ///GO + /// + /// + /// + ///-- ============================================= + ///-- Author: Auldrin Possa + ///-- Create date: 04/08/2015 + ///-- Description: Uses DB mail to send notification to delegates on retiring self assessment. + ///-- ============================================= + ///CREATE PROCEDURE [dbo].[SendRetiringSelfAssessmentNotification] + /// @SelfAssessmentId int, + /// @TestOnly bit + ///AS + ///BEGIN + /// -- SET NOCOUNT ON added to prevent extra result sets from + /// -- interfering with SELEC [rest of string was truncated]";. + /// + internal static string TD_5552_SendRetiringNotification { + get { + return ResourceManager.GetString("TD_5552_SendRetiringNotification", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IF OBJECT_ID('dbo.IndexOptimize', 'P') IS NOT NULL DROP PROCEDURE dbo.IndexOptimize; + ///IF OBJECT_ID('dbo.CommandExecute', 'P') IS NOT NULL DROP PROCEDURE dbo.CommandExecute; + ///IF OBJECT_ID('dbo.sp_purge_commandlog', 'P') IS NOT NULL DROP PROCEDURE dbo.sp_purge_commandlog; + ///IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog; + ///. + /// + internal static string TD_5670_MaintenanceScripts_DOWN { + get { + return ResourceManager.GetString("TD-5670-MaintenanceScripts_DOWN", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to -- ============================================ + ///-- Drop if exists (for clean redeploy) + ///-- ============================================ + ///IF OBJECT_ID('dbo.IndexOptimize', 'P') IS NOT NULL DROP PROCEDURE dbo.IndexOptimize; + ///IF OBJECT_ID('dbo.DatabaseIntegrityCheck', 'P') IS NOT NULL DROP PROCEDURE dbo.DatabaseIntegrityCheck; + ///IF OBJECT_ID('dbo.CommandExecute', 'P') IS NOT NULL DROP PROCEDURE dbo.CommandExecute; + ///IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog; + ///GO + /// + ///-- =========== [rest of string was truncated]";. + /// + internal static string TD_5670_MaintenanceScripts_UP { + get { + return ResourceManager.GetString("TD_5670_MaintenanceScripts_UP", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CREATE OR ALTER PROCEDURE [dbo].[usp_GetSelfAssessmentReport] + /// @SelfAssessmentID INT, + /// @CentreID INT + ///AS + ///BEGIN + /// SET NOCOUNT ON; + /// + /// -- Step 1: Materialize the LatestAssessmentResults into a temp table + /// IF OBJECT_ID('tempdb..#LatestAssessmentResults') IS NOT NULL + /// DROP TABLE #LatestAssessmentResults; + /// + /// SELECT + /// s.DelegateUserID, + /// CASE WHEN COALESCE(rr.LevelRAG, 0) = 3 THEN s.ID ELSE NULL END AS SelfAssessed, + /// CASE + /// WHEN sv.Verified IS NOT N [rest of string was truncated]";. + /// + internal static string TD_5759_CreateOrAlterSelfAssessmentReportSPandTVF_Fix_UP { + get { + return ResourceManager.GetString("TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF-Fix_UP", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CREATE OR ALTER FUNCTION dbo.GetOtherCentresForSelfAssessmentTVF + ///( + /// @UserID INT, + /// @SelfAssessmentID INT, + /// @ExcludeCentreID INT + ///) + ///RETURNS TABLE + ///AS + ///RETURN + ///( + /// SELECT + /// STUFF(( + /// SELECT DISTINCT + /// ', ' + c.CentreName + /// FROM Users AS u + /// INNER JOIN DelegateAccounts AS da ON u.ID = da.UserID + /// INNER JOIN Centres AS c ON da.CentreID = c.CentreID + /// INNER JOIN CentreSelfAssessments AS csa ON c.CentreID = csa.CentreID [rest of string was truncated]";. + /// + internal static string TD_5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP { + get { + return ResourceManager.GetString("TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP", resourceCulture); + } + } + /// /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[GetActiveAvailableCustomisationsForCentreFiltered_V6] Script Date: 29/09/2022 19:11:04 ******/ ///SET ANSI_NULLS ON diff --git a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx index c2c733076c..c1c2673d2f 100644 --- a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx +++ b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx @@ -469,13 +469,55 @@ ..\Scripts\TD_4950_Alter_GetAssessmentResultsByDelegate_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 - - ..\Resources\TD-4950-dboGetOtherCentresForSelfAssessmentCreateOrAlter.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + ..\Scripts\TD-5412-Alter_SendExpiredTBCReminders_Down.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 - - ..\Scripts\TD-4878-Alter_GetActivitiesForDelegateEnrolment_Down.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + ..\Scripts\TD-5412-Alter_SendExpiredTBCReminders_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 - - ..\Scripts\TD-4878-Alter_GetActivitiesForDelegateEnrolment_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + ..\Scripts\TD-5514-Alter_SendExpiredTBCReminders_Down.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + ..\Scripts\TD-5514-Alter_SendExpiredTBCReminders_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + ..\Scripts\TD-5447-Alter_ReorderFrameworkCompetency_Down.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + ..\Scripts\TD-5447-Alter_ReorderFrameworkCompetency_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + ..\Scripts\TD-5670-MaintenanceScripts_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Scripts\TD-5670-MaintenanceScripts_DOWN.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Scripts\TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Scripts\TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF-Fix_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Scripts\TD-483-uspMoveCompetencyInSelfAssessmentCreateOrAlter_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Scripts\TD-483-uspMoveCompetencyGroupInSelfAssessmentCreateOrAlter_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Scripts\TD-5535-Alter_GetActivitiesForDelegateEnrolment_Down.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + ..\Scripts\TD-5535-Alter_GetActivitiesForDelegateEnrolment_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + ..\Scripts\TD-5552-SendRetiringNotification.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + ..\Scripts\TD-5552-Alter_SendRetiringSelfAssessmentNotification_Down.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + + ..\Scripts\TD-5552-Alter_SendRetiringSelfAssessmentNotification_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 \ No newline at end of file diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-483-uspMoveCompetencyGroupInSelfAssessmentCreateOrAlter_UP.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-483-uspMoveCompetencyGroupInSelfAssessmentCreateOrAlter_UP.sql new file mode 100644 index 0000000000..f42d8894c7 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-483-uspMoveCompetencyGroupInSelfAssessmentCreateOrAlter_UP.sql @@ -0,0 +1,164 @@ + +CREATE OR ALTER PROCEDURE usp_RenumberSelfAssessmentStructure + @SelfAssessmentID INT +AS +BEGIN + SET NOCOUNT ON; + + /* + Step 1: Build an ordered list of groups + - Each group is ranked by its current Min(Ordering) + - Ungrouped competencies (NULL CompetencyGroupID) are treated as their own "pseudo group" + */ + ;WITH GroupRanks AS ( + SELECT + CompetencyGroupID, + ROW_NUMBER() OVER (ORDER BY MIN(Ordering)) AS GroupRank + FROM SelfAssessmentStructure + WHERE SelfAssessmentID = @SelfAssessmentID + GROUP BY CompetencyGroupID + ) + /* + Step 2: Renumber groups and competencies + - Groups get ranked (1,2,3) + - Within each group, competencies are ordered by their current Ordering and renumbered (1,2,3) + */ + UPDATE sas + SET sas.Ordering = rn.NewOrdering + FROM SelfAssessmentStructure sas + INNER JOIN ( + SELECT + s.ID, + -- new group position * 1000 + row within group + -- gives room between groups and avoids clashes + (g.GroupRank * 1000) + + ROW_NUMBER() OVER (PARTITION BY s.CompetencyGroupID, g.GroupRank ORDER BY s.Ordering, s.ID) AS NewOrdering + FROM SelfAssessmentStructure s + INNER JOIN GroupRanks g + ON g.CompetencyGroupID = s.CompetencyGroupID + WHERE s.SelfAssessmentID = @SelfAssessmentID + ) rn ON sas.ID = rn.ID; + +END + +GO + +CREATE OR ALTER PROCEDURE usp_MoveCompetencyGroupInSelfAssessment + @SelfAssessmentID INT, + @GroupID INT, + @Direction NVARCHAR(10) -- 'up' or 'down' +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + BEGIN TRY + BEGIN TRAN; + + /* 1) Rank groups by current Min(Ordering) (NULL groups excluded here; include if desired). */ + ;WITH GroupRanks AS ( + SELECT + CompetencyGroupID, + MIN(Ordering) AS MinOrder, + ROW_NUMBER() OVER ( + ORDER BY MIN(Ordering), MIN(CompetencyGroupID) + ) AS RankPos + FROM SelfAssessmentStructure + WHERE SelfAssessmentID = @SelfAssessmentID + AND CompetencyGroupID IS NOT NULL + GROUP BY CompetencyGroupID + ) + SELECT * + INTO #Groups + FROM GroupRanks; + + DECLARE @CurRank INT, @SwapRank INT, @SwapGroupID INT; + + SELECT @CurRank = RankPos + FROM #Groups + WHERE CompetencyGroupID = @GroupID; + + IF @CurRank IS NULL + BEGIN + DROP TABLE #Groups; + COMMIT TRAN; RETURN; -- nothing to do + END + + IF LOWER(@Direction) = 'up' + SET @SwapRank = @CurRank - 1; + ELSE IF LOWER(@Direction) = 'down' + SET @SwapRank = @CurRank + 1; + ELSE + BEGIN + DROP TABLE #Groups; + ROLLBACK TRAN; THROW 50000, 'Direction must be ''up'' or ''down''.', 1; + END + + SELECT @SwapGroupID = CompetencyGroupID + FROM #Groups + WHERE RankPos = @SwapRank; + + IF @SwapGroupID IS NULL + BEGIN + DROP TABLE #Groups; + COMMIT TRAN; RETURN; -- already at top/bottom + END + + /* 2) Build a mapping where ONLY the two groups swap ranks; others keep theirs. */ + SELECT + g.CompetencyGroupID, + CASE + WHEN g.CompetencyGroupID = @GroupID THEN @SwapRank + WHEN g.CompetencyGroupID = @SwapGroupID THEN @CurRank + ELSE g.RankPos + END AS NewRank + INTO #RankMap + FROM #Groups g; + + /* 3) Choose a block size big enough to keep groups separated when we recompute. + Using count(rows in this self assessment) + 10 is a safe dynamic choice. */ + DECLARE @Block INT = + (SELECT COUNT(*) FROM SelfAssessmentStructure WHERE SelfAssessmentID = @SelfAssessmentID) + 10; + + /* 4) Recompute EVERY rows Ordering from the rank map (this sets the new global order). + We preserve within-group relative order using the existing Ordering (and ID as tiebreak). */ + ;WITH NewOrders AS ( + SELECT + s.ID, + (m.NewRank * @Block) + + ROW_NUMBER() OVER ( + PARTITION BY s.CompetencyGroupID + ORDER BY s.Ordering, s.ID + ) AS NewOrdering + FROM SelfAssessmentStructure s + JOIN #RankMap m + ON m.CompetencyGroupID = s.CompetencyGroupID + WHERE s.SelfAssessmentID = @SelfAssessmentID + ) + UPDATE s + SET s.Ordering = n.NewOrdering + FROM SelfAssessmentStructure s + JOIN NewOrders n ON n.ID = s.ID; + + /* 5) (Optional) Compress to 1..N while keeping the just-established relative order. */ + ;WITH Ordered AS ( + SELECT ID, + ROW_NUMBER() OVER (ORDER BY Ordering, ID) AS Seq + FROM SelfAssessmentStructure + WHERE SelfAssessmentID = @SelfAssessmentID + ) + UPDATE s + SET s.Ordering = o.Seq + FROM SelfAssessmentStructure s + JOIN Ordered o ON o.ID = s.ID; + + DROP TABLE #Groups; + DROP TABLE #RankMap; + + COMMIT TRAN; + END TRY + BEGIN CATCH + IF @@TRANCOUNT > 0 ROLLBACK TRAN; + THROW; + END CATCH +END; diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-483-uspMoveCompetencyInSelfAssessmentCreateOrAlter_UP.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-483-uspMoveCompetencyInSelfAssessmentCreateOrAlter_UP.sql new file mode 100644 index 0000000000..176422b549 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-483-uspMoveCompetencyInSelfAssessmentCreateOrAlter_UP.sql @@ -0,0 +1,59 @@ +CREATE OR ALTER PROCEDURE usp_MoveCompetencyInSelfAssessment + @SelfAssessmentID INT, + @CompetencyID INT, + @Direction NVARCHAR(10) +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @GroupID INT, @CurrentOrder INT; + + SELECT + @GroupID = CompetencyGroupID, + @CurrentOrder = Ordering + FROM SelfAssessmentStructure + WHERE SelfAssessmentID = @SelfAssessmentID AND CompetencyID = @CompetencyID; + + IF @GroupID IS NULL + BEGIN + -- Can't reorder ungrouped competencies via group-based logic + RETURN; + END + + DECLARE @TargetCompetencyID INT, @TargetOrder INT; + + IF @Direction = 'up' + BEGIN + SELECT TOP 1 + @TargetCompetencyID = CompetencyID, + @TargetOrder = Ordering + FROM SelfAssessmentStructure + WHERE SelfAssessmentID = @SelfAssessmentID + AND CompetencyGroupID = @GroupID + AND Ordering < @CurrentOrder + ORDER BY Ordering DESC; + END + ELSE IF @Direction = 'down' + BEGIN + SELECT TOP 1 + @TargetCompetencyID = CompetencyID, + @TargetOrder = Ordering + FROM SelfAssessmentStructure + WHERE SelfAssessmentID = @SelfAssessmentID + AND CompetencyGroupID = @GroupID + AND Ordering > @CurrentOrder + ORDER BY Ordering ASC; + END + + IF @TargetCompetencyID IS NOT NULL + BEGIN + -- Swap the orderings + UPDATE SelfAssessmentStructure + SET Ordering = @TargetOrder + WHERE SelfAssessmentID = @SelfAssessmentID AND CompetencyID = @CompetencyID; + + UPDATE SelfAssessmentStructure + SET Ordering = @CurrentOrder + WHERE SelfAssessmentID = @SelfAssessmentID AND CompetencyID = @TargetCompetencyID; + END +END diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5412-Alter_SendExpiredTBCReminders_Down.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5412-Alter_SendExpiredTBCReminders_Down.sql new file mode 100644 index 0000000000..7ca4a74075 Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5412-Alter_SendExpiredTBCReminders_Down.sql differ diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5412-Alter_SendExpiredTBCReminders_Up.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5412-Alter_SendExpiredTBCReminders_Up.sql new file mode 100644 index 0000000000..3ff6d79f99 Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5412-Alter_SendExpiredTBCReminders_Up.sql differ diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5447-Alter_ReorderFrameworkCompetency_Down.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5447-Alter_ReorderFrameworkCompetency_Down.sql new file mode 100644 index 0000000000..56bac72b67 Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5447-Alter_ReorderFrameworkCompetency_Down.sql differ diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5447-Alter_ReorderFrameworkCompetency_Up.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5447-Alter_ReorderFrameworkCompetency_Up.sql new file mode 100644 index 0000000000..bd170f6b9e Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5447-Alter_ReorderFrameworkCompetency_Up.sql differ diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5514-Alter_SendExpiredTBCReminders_Down.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5514-Alter_SendExpiredTBCReminders_Down.sql new file mode 100644 index 0000000000..f80def01d5 Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5514-Alter_SendExpiredTBCReminders_Down.sql differ diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5514-Alter_SendExpiredTBCReminders_Up.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5514-Alter_SendExpiredTBCReminders_Up.sql new file mode 100644 index 0000000000..448ba42946 Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5514-Alter_SendExpiredTBCReminders_Up.sql differ diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5535-Alter_GetActivitiesForDelegateEnrolment_Down.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5535-Alter_GetActivitiesForDelegateEnrolment_Down.sql new file mode 100644 index 0000000000..d2f8374a09 Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5535-Alter_GetActivitiesForDelegateEnrolment_Down.sql differ diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5535-Alter_GetActivitiesForDelegateEnrolment_Up.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5535-Alter_GetActivitiesForDelegateEnrolment_Up.sql new file mode 100644 index 0000000000..0e8c76138f Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5535-Alter_GetActivitiesForDelegateEnrolment_Up.sql differ diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-Alter_SendRetiringSelfAssessmentNotification_Down.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-Alter_SendRetiringSelfAssessmentNotification_Down.sql new file mode 100644 index 0000000000..c416e54e50 Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-Alter_SendRetiringSelfAssessmentNotification_Down.sql differ diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-Alter_SendRetiringSelfAssessmentNotification_Up.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-Alter_SendRetiringSelfAssessmentNotification_Up.sql new file mode 100644 index 0000000000..6ee6c7c21c Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-Alter_SendRetiringSelfAssessmentNotification_Up.sql differ diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-SendRetiringNotification.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-SendRetiringNotification.sql new file mode 100644 index 0000000000..a5c5437375 Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-SendRetiringNotification.sql differ diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_DOWN.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_DOWN.sql new file mode 100644 index 0000000000..14a45533ad --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_DOWN.sql @@ -0,0 +1,4 @@ +IF OBJECT_ID('dbo.IndexOptimize', 'P') IS NOT NULL DROP PROCEDURE dbo.IndexOptimize; +IF OBJECT_ID('dbo.CommandExecute', 'P') IS NOT NULL DROP PROCEDURE dbo.CommandExecute; +IF OBJECT_ID('dbo.sp_purge_commandlog', 'P') IS NOT NULL DROP PROCEDURE dbo.sp_purge_commandlog; +IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog; diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_UP.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_UP.sql new file mode 100644 index 0000000000..ac8258b6fa --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_UP.sql @@ -0,0 +1,204 @@ +-- ============================================ +-- Drop if exists (for clean redeploy) +-- ============================================ +IF OBJECT_ID('dbo.IndexOptimize', 'P') IS NOT NULL DROP PROCEDURE dbo.IndexOptimize; +IF OBJECT_ID('dbo.DatabaseIntegrityCheck', 'P') IS NOT NULL DROP PROCEDURE dbo.DatabaseIntegrityCheck; +IF OBJECT_ID('dbo.CommandExecute', 'P') IS NOT NULL DROP PROCEDURE dbo.CommandExecute; +IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog; +GO + +-- ============================================ +-- CommandLog table +-- ============================================ +CREATE TABLE dbo.CommandLog ( + ID INT IDENTITY PRIMARY KEY, + DatabaseName SYSNAME NULL, + SchemaName SYSNAME NULL, + ObjectName SYSNAME NULL, + ObjectType CHAR(2) NULL, + IndexName SYSNAME NULL, + IndexType TINYINT NULL, + StatisticsName SYSNAME NULL, + PartitionNumber INT NULL, + ExtendedInfo XML NULL, + Command NVARCHAR(MAX) NOT NULL, + CommandType NVARCHAR(60) NOT NULL, + StartTime DATETIME NOT NULL, + EndTime DATETIME NOT NULL, + ErrorNumber INT NOT NULL, + ErrorMessage NVARCHAR(MAX) NULL +); +GO + +-- ============================================ +-- CommandExecute stored procedure +-- ============================================ +CREATE PROCEDURE dbo.CommandExecute + @Command NVARCHAR(MAX), + @CommandType NVARCHAR(60), + @DatabaseName SYSNAME = NULL, + @SchemaName SYSNAME = NULL, + @ObjectName SYSNAME = NULL, + @ObjectType CHAR(2) = NULL, + @IndexName SYSNAME = NULL, + @IndexType TINYINT = NULL, + @StatisticsName SYSNAME = NULL, + @PartitionNumber INT = NULL, + @ExtendedInfo XML = NULL +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @StartTime DATETIME = GETDATE(); + DECLARE @ErrorNumber INT = 0; + DECLARE @ErrorMessage NVARCHAR(MAX) = NULL; + + BEGIN TRY + EXEC (@Command); + END TRY + BEGIN CATCH + SET @ErrorNumber = ERROR_NUMBER(); + SET @ErrorMessage = ERROR_MESSAGE(); + END CATCH; + + INSERT INTO dbo.CommandLog ( + DatabaseName, SchemaName, ObjectName, ObjectType, IndexName, IndexType, StatisticsName, + PartitionNumber, ExtendedInfo, Command, CommandType, StartTime, EndTime, ErrorNumber, ErrorMessage + ) + VALUES ( + @DatabaseName, @SchemaName, @ObjectName, @ObjectType, @IndexName, @IndexType, @StatisticsName, + @PartitionNumber, @ExtendedInfo, @Command, @CommandType, @StartTime, GETDATE(), @ErrorNumber, @ErrorMessage + ); + + IF @ErrorNumber <> 0 + RAISERROR(@ErrorMessage, 16, 1); +END +GO + +-- ============================================ +-- IndexOptimize stored procedure +-- ============================================ +CREATE PROCEDURE dbo.IndexOptimize + @Databases NVARCHAR(MAX) = 'USER_DATABASES', + @FragmentationMedium TINYINT = 30, + @FragmentationHigh TINYINT = 70 +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @db SYSNAME; + DECLARE db_cursor CURSOR FOR + SELECT name FROM sys.databases + WHERE (@Databases = 'USER_DATABASES' AND database_id > 4) + OR name = @Databases; + + OPEN db_cursor; + FETCH NEXT FROM db_cursor INTO @db; + + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @sql NVARCHAR(MAX) = N' + USE [' + @db + ']; + + DECLARE @schema SYSNAME, @table SYSNAME, @index SYSNAME; + DECLARE @index_id INT, @frag FLOAT; + + DECLARE c CURSOR FOR + SELECT s.name, t.name, i.name, i.index_id, ips.avg_fragmentation_in_percent + FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, ''LIMITED'') ips + JOIN sys.indexes i ON i.object_id = ips.object_id AND i.index_id = ips.index_id + JOIN sys.tables t ON t.object_id = ips.object_id + JOIN sys.schemas s ON s.schema_id = t.schema_id + WHERE ips.index_id > 0 AND ips.page_count > 100; + + OPEN c; + FETCH NEXT FROM c INTO @schema, @table, @index, @index_id, @frag; + + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @cmd NVARCHAR(MAX); + SET @cmd = ''ALTER INDEX ['' + @index + ''] ON ['' + @schema + ''].['' + @table + ''] ''; + + IF @frag >= ' + CAST(@FragmentationHigh AS NVARCHAR) + ' + SET @cmd += ''REBUILD''; + ELSE IF @frag >= ' + CAST(@FragmentationMedium AS NVARCHAR) + ' + SET @cmd += ''REORGANIZE''; + ELSE + SET @cmd = NULL; + + IF @cmd IS NOT NULL + EXEC dbo.CommandExecute @Command = @cmd, + @CommandType = ''ALTER INDEX'', + @DatabaseName = ''' + @db + ''', + @SchemaName = @schema, + @ObjectName = @table, + @ObjectType = ''U'', + @IndexName = @index; + + FETCH NEXT FROM c INTO @schema, @table, @index, @index_id, @frag; + END; + + CLOSE c; + DEALLOCATE c; + '; + + EXEC sp_executesql @sql; + FETCH NEXT FROM db_cursor INTO @db; + END; + + CLOSE db_cursor; + DEALLOCATE db_cursor; +END +GO + +-- ============================================ +-- DatabaseIntegrityCheck stored procedure +-- ============================================ +CREATE PROCEDURE dbo.DatabaseIntegrityCheck + @Databases NVARCHAR(MAX) = 'USER_DATABASES' +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @db SYSNAME; + DECLARE db_cursor CURSOR FOR + SELECT name FROM sys.databases + WHERE (@Databases = 'USER_DATABASES' AND database_id > 4) + OR name = @Databases; + + OPEN db_cursor; + FETCH NEXT FROM db_cursor INTO @db; + + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @cmd NVARCHAR(MAX); + SET @cmd = 'DBCC CHECKDB([' + @db + ']) WITH NO_INFOMSGS, ALL_ERRORMSGS'; + + EXEC dbo.CommandExecute + @Command = @cmd, + @CommandType = 'DBCC CHECKDB', + @DatabaseName = @db; + + FETCH NEXT FROM db_cursor INTO @db; + END + + CLOSE db_cursor; + DEALLOCATE db_cursor; +END +GO + +-- ============================================ +-- Purge command log stored procedure +-- ============================================ +CREATE OR ALTER PROCEDURE dbo.sp_purge_commandlog + @DaysToKeep INT = 30 +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @DeleteBefore DATETIME = DATEADD(DAY, -@DaysToKeep, GETDATE()); + + DELETE FROM dbo.CommandLog + WHERE StartTime < @DeleteBefore; +END +GO diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF-Fix_UP.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF-Fix_UP.sql new file mode 100644 index 0000000000..21922679ff --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF-Fix_UP.sql @@ -0,0 +1,117 @@ +CREATE OR ALTER PROCEDURE [dbo].[usp_GetSelfAssessmentReport] + @SelfAssessmentID INT, + @CentreID INT +AS +BEGIN + SET NOCOUNT ON; + + -- Step 1: Materialize the LatestAssessmentResults into a temp table + IF OBJECT_ID('tempdb..#LatestAssessmentResults') IS NOT NULL + DROP TABLE #LatestAssessmentResults; + + SELECT + s.DelegateUserID, + CASE WHEN COALESCE(rr.LevelRAG, 0) = 3 THEN s.ID ELSE NULL END AS SelfAssessed, + CASE + WHEN sv.Verified IS NOT NULL AND sv.SignedOff = 1 AND COALESCE(rr.LevelRAG, 0) = 3 + THEN s.ID ELSE NULL + END AS Confirmed, + CASE WHEN sas.Optional = 1 THEN s.CompetencyID ELSE NULL END AS Optional + INTO #LatestAssessmentResults + FROM SelfAssessmentResults AS s + LEFT JOIN SelfAssessmentStructure AS sas + ON sas.SelfAssessmentID = @SelfAssessmentID + AND s.CompetencyID = sas.CompetencyID + LEFT JOIN SelfAssessmentResultSupervisorVerifications AS sv + ON s.ID = sv.SelfAssessmentResultId + AND sv.Superceded = 0 + LEFT JOIN CompetencyAssessmentQuestionRoleRequirements AS rr + ON s.CompetencyID = rr.CompetencyID + AND s.AssessmentQuestionID = rr.AssessmentQuestionID + AND sas.SelfAssessmentID = rr.SelfAssessmentID + AND s.Result = rr.LevelValue + WHERE sas.SelfAssessmentID = @SelfAssessmentID; + + CREATE NONCLUSTERED INDEX IX_LAR_DelegateUserID ON #LatestAssessmentResults(DelegateUserID); + + -- Step 2: Run the main query + SELECT + sa.Name AS SelfAssessment, + u.LastName + ', ' + u.FirstName AS Learner, + da.Active AS LearnerActive, + u.ProfessionalRegistrationNumber AS PRN, + jg.JobGroupName AS JobGroup, + da.Answer1 AS RegistrationAnswer1, + da.Answer2 AS RegistrationAnswer2, + da.Answer3 AS RegistrationAnswer3, + da.Answer4 AS RegistrationAnswer4, + da.Answer5 AS RegistrationAnswer5, + da.Answer6 AS RegistrationAnswer6, + oc.OtherCentres, + CASE + WHEN aa.ID IS NULL THEN 'Learner' + WHEN aa.IsCentreManager = 1 THEN 'Centre Manager' + WHEN aa.IsCentreAdmin = 1 AND aa.IsCentreManager = 0 THEN 'Centre Admin' + WHEN aa.IsSupervisor = 1 THEN 'Supervisor' + WHEN aa.IsNominatedSupervisor = 1 THEN 'Nominated supervisor' + END AS DLSRole, + da.DateRegistered AS Registered, + ca.StartedDate AS [Started], + ca.LastAccessed, + COUNT(DISTINCT LAR.Optional) AS [OptionalProficienciesAssessed], + COUNT(DISTINCT LAR.SelfAssessed) AS [SelfAssessedAchieved], + COUNT(DISTINCT LAR.Confirmed) AS [ConfirmedResults], + MAX(casv.Requested) AS SignOffRequested, + MAX(1 * casv.SignedOff) AS SignOffAchieved, + MIN(casv.Verified) AS ReviewedDate + FROM CandidateAssessments AS ca + INNER JOIN DelegateAccounts AS da + ON ca.DelegateUserID = da.UserID + AND da.CentreID = @CentreID + INNER JOIN Users AS u + ON u.ID = da.UserID + INNER JOIN SelfAssessments AS sa + ON ca.SelfAssessmentID = sa.ID + INNER JOIN CentreSelfAssessments AS csa + ON sa.ID = csa.SelfAssessmentID + INNER JOIN Centres AS c + ON csa.CentreID = c.CentreID + AND da.CentreID = c.CentreID + INNER JOIN JobGroups AS jg + ON u.JobGroupID = jg.JobGroupID + LEFT JOIN AdminAccounts AS aa + ON da.UserID = aa.UserID + AND aa.CentreID = da.CentreID + AND aa.Active = 1 + LEFT JOIN CandidateAssessmentSupervisors AS cas + ON ca.ID = cas.CandidateAssessmentID + LEFT JOIN CandidateAssessmentSupervisorVerifications AS casv + ON casv.CandidateAssessmentSupervisorID = cas.ID + LEFT JOIN SupervisorDelegates AS sd + ON cas.SupervisorDelegateId = sd.ID + LEFT JOIN #LatestAssessmentResults AS LAR + ON LAR.DelegateUserID = ca.DelegateUserID + OUTER APPLY dbo.GetOtherCentresForSelfAssessmentTVF(da.UserID, @SelfAssessmentID, c.CentreID) AS oc + WHERE + sa.ID = @SelfAssessmentID + AND sa.ArchivedDate IS NULL + AND c.Active = 1 + AND ca.RemovedDate IS NULL + AND ca.NonReportable = 0 + GROUP BY + sa.Name, + u.LastName + ', ' + u.FirstName, + da.Active, + u.ProfessionalRegistrationNumber, + jg.JobGroupName, + da.Answer1, da.Answer2, da.Answer3, da.Answer4, da.Answer5, da.Answer6, + da.DateRegistered, + ca.StartedDate, + ca.LastAccessed, + oc.OtherCentres, + aa.ID, aa.IsCentreManager, aa.IsCentreAdmin, aa.IsSupervisor, aa.IsNominatedSupervisor + ORDER BY + sa.Name, u.LastName + ', ' + u.FirstName; + +END; +GO \ No newline at end of file diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP.sql new file mode 100644 index 0000000000..eb99526d7a --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP.sql @@ -0,0 +1,145 @@ +CREATE OR ALTER FUNCTION dbo.GetOtherCentresForSelfAssessmentTVF +( + @UserID INT, + @SelfAssessmentID INT, + @ExcludeCentreID INT +) +RETURNS TABLE +AS +RETURN +( + SELECT + STUFF(( + SELECT DISTINCT + ', ' + c.CentreName + FROM Users AS u + INNER JOIN DelegateAccounts AS da ON u.ID = da.UserID + INNER JOIN Centres AS c ON da.CentreID = c.CentreID + INNER JOIN CentreSelfAssessments AS csa ON c.CentreID = csa.CentreID + WHERE u.ID = @UserID + AND da.Active = 1 + AND da.Approved = 1 + AND csa.SelfAssessmentID = @SelfAssessmentID + AND c.CentreID <> @ExcludeCentreID + FOR XML PATH(''), TYPE + ).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS OtherCentres +); +GO + +CREATE OR ALTER PROCEDURE [dbo].[usp_GetSelfAssessmentReport] + @SelfAssessmentID INT, + @CentreID INT +AS +BEGIN + SET NOCOUNT ON; + + -- Step 1: Materialize the LatestAssessmentResults into a temp table + IF OBJECT_ID('tempdb..#LatestAssessmentResults') IS NOT NULL + DROP TABLE #LatestAssessmentResults; + + SELECT + s.DelegateUserID, + CASE WHEN COALESCE(rr.LevelRAG, 0) = 3 THEN s.ID ELSE NULL END AS SelfAssessed, + CASE + WHEN sv.Verified IS NOT NULL AND sv.SignedOff = 1 AND COALESCE(rr.LevelRAG, 0) = 3 + THEN s.ID ELSE NULL + END AS Confirmed, + CASE WHEN sas.Optional = 1 THEN s.CompetencyID ELSE NULL END AS Optional + INTO #LatestAssessmentResults + FROM SelfAssessmentResults AS s + LEFT JOIN SelfAssessmentStructure AS sas + ON sas.SelfAssessmentID = @SelfAssessmentID + AND s.CompetencyID = sas.CompetencyID + LEFT JOIN SelfAssessmentResultSupervisorVerifications AS sv + ON s.ID = sv.SelfAssessmentResultId + AND sv.Superceded = 0 + LEFT JOIN CompetencyAssessmentQuestionRoleRequirements AS rr + ON s.CompetencyID = rr.CompetencyID + AND s.AssessmentQuestionID = rr.AssessmentQuestionID + AND sas.SelfAssessmentID = rr.SelfAssessmentID + AND s.Result = rr.LevelValue + WHERE sas.SelfAssessmentID = @SelfAssessmentID; + + CREATE NONCLUSTERED INDEX IX_LAR_DelegateUserID ON #LatestAssessmentResults(DelegateUserID); + + -- Step 2: Run the main query + SELECT + sa.Name AS SelfAssessment, + u.LastName + ', ' + u.FirstName AS Learner, + da.Active AS LearnerActive, + u.ProfessionalRegistrationNumber AS PRN, + jg.JobGroupName AS JobGroup, + da.Answer1, + da.Answer2, + da.Answer3, + da.Answer4, + da.Answer5, + da.Answer6, + oc.OtherCentres, + CASE + WHEN aa.ID IS NULL THEN 'Learner' + WHEN aa.IsCentreManager = 1 THEN 'Centre Manager' + WHEN aa.IsCentreAdmin = 1 AND aa.IsCentreManager = 0 THEN 'Centre Admin' + WHEN aa.IsSupervisor = 1 THEN 'Supervisor' + WHEN aa.IsNominatedSupervisor = 1 THEN 'Nominated supervisor' + END AS DLSRole, + da.DateRegistered AS Registered, + ca.StartedDate, + ca.LastAccessed, + COUNT(DISTINCT LAR.Optional) AS [OptionalProficienciesAssessed], + COUNT(DISTINCT LAR.SelfAssessed) AS [SelfAssessedAchieved], + COUNT(DISTINCT LAR.Confirmed) AS [ConfirmedResults], + MAX(casv.Requested) AS SignOffRequested, + MAX(1 * casv.SignedOff) AS SignOffAchieved, + MIN(casv.Verified) AS ReviewedDate + FROM CandidateAssessments AS ca + INNER JOIN DelegateAccounts AS da + ON ca.DelegateUserID = da.UserID + AND da.CentreID = @CentreID + INNER JOIN Users AS u + ON u.ID = da.UserID + INNER JOIN SelfAssessments AS sa + ON ca.SelfAssessmentID = sa.ID + INNER JOIN CentreSelfAssessments AS csa + ON sa.ID = csa.SelfAssessmentID + INNER JOIN Centres AS c + ON csa.CentreID = c.CentreID + AND da.CentreID = c.CentreID + INNER JOIN JobGroups AS jg + ON u.JobGroupID = jg.JobGroupID + LEFT JOIN AdminAccounts AS aa + ON da.UserID = aa.UserID + AND aa.CentreID = da.CentreID + AND aa.Active = 1 + LEFT JOIN CandidateAssessmentSupervisors AS cas + ON ca.ID = cas.CandidateAssessmentID + LEFT JOIN CandidateAssessmentSupervisorVerifications AS casv + ON casv.CandidateAssessmentSupervisorID = cas.ID + LEFT JOIN SupervisorDelegates AS sd + ON cas.SupervisorDelegateId = sd.ID + LEFT JOIN #LatestAssessmentResults AS LAR + ON LAR.DelegateUserID = ca.DelegateUserID + OUTER APPLY dbo.GetOtherCentresForSelfAssessmentTVF(da.UserID, @SelfAssessmentID, c.CentreID) AS oc + WHERE + sa.ID = @SelfAssessmentID + AND sa.ArchivedDate IS NULL + AND c.Active = 1 + AND ca.RemovedDate IS NULL + AND ca.NonReportable = 0 + GROUP BY + sa.Name, + u.LastName + ', ' + u.FirstName, + da.Active, + u.ProfessionalRegistrationNumber, + jg.JobGroupName, + da.Answer1, da.Answer2, da.Answer3, da.Answer4, da.Answer5, da.Answer6, + da.DateRegistered, + ca.StartedDate, + ca.LastAccessed, + oc.OtherCentres, + aa.ID, aa.IsCentreManager, aa.IsCentreAdmin, aa.IsSupervisor, aa.IsNominatedSupervisor + ORDER BY + sa.Name, u.LastName + ', ' + u.FirstName; + +END; +GO \ No newline at end of file diff --git a/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs b/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs index 2a48f0ae3d..f586fc92d6 100644 --- a/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs @@ -84,7 +84,7 @@ public void UpdateCentreDetailsForSuperAdmin( string centreName, int centreTypeId, int regionId, - string? centreEmail, + string? registrationEmail, string? ipPrefix, bool showOnMap ); @@ -180,7 +180,7 @@ ORDER BY CentreName" c.ContactSurname, c.ContactEmail, c.ContactTelephone, - c.AutoRegisterManagerEmail AS CentreEmail, + c.AutoRegisterManagerEmail AS RegistrationEmail, c.ShowOnMap, c.CMSAdministrators AS CmsAdministratorSpots, c.CMSManagers AS CmsManagerSpots, @@ -192,7 +192,8 @@ ORDER BY CentreName" c.ServerSpaceBytes, cty.CentreType, c.CandidateByteLimit, - c.ContractReviewDate + c.ContractReviewDate, + c.pwEmail as CentreEmail FROM Centres AS c INNER JOIN Regions AS r ON r.RegionID = c.RegionID INNER JOIN ContractTypes AS ct ON ct.ContractTypeID = c.ContractTypeId @@ -232,7 +233,7 @@ FROM Centres AS c c.ContactEmail, c.ContactTelephone, c.pwTelephone AS CentreTelephone, - c.AutoRegisterManagerEmail AS CentreEmail, + c.AutoRegisterManagerEmail AS RegistrationEmail, c.pwPostCode AS CentrePostcode, c.ShowOnMap, c.Long AS Longitude, @@ -253,7 +254,7 @@ FROM Centres AS c c.ServerSpaceBytes, c.CentreTypeID, ctp.CentreType, - c.pwEmail as RegistrationEmail + c.pwEmail as CentreEmail FROM Centres AS c INNER JOIN Regions AS r ON r.RegionID = c.RegionID INNER JOIN ContractTypes AS ct ON ct.ContractTypeID = c.ContractTypeId @@ -472,7 +473,7 @@ public void UpdateCentreDetailsForSuperAdmin( string centreName, int centreTypeId, int regionId, - string? centreEmail, + string? registrationEmail, string? ipPrefix, bool showOnMap ) @@ -482,7 +483,7 @@ bool showOnMap CentreName = @centreName, CentreTypeId = @centreTypeId, RegionId = @regionId, - AutoRegisterManagerEmail = @centreEmail, + AutoRegisterManagerEmail = @registrationEmail, IPPrefix = @ipPrefix, ShowOnMap = @showOnMap WHERE CentreId = @centreId", @@ -491,7 +492,7 @@ bool showOnMap centreName, centreTypeId, regionId, - centreEmail, + registrationEmail, ipPrefix, showOnMap, centreId diff --git a/DigitalLearningSolutions.Data/DataServices/CompetencyAssessmentDataService.cs b/DigitalLearningSolutions.Data/DataServices/CompetencyAssessmentDataService.cs index 983bdba073..3c2cc31532 100644 --- a/DigitalLearningSolutions.Data/DataServices/CompetencyAssessmentDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/CompetencyAssessmentDataService.cs @@ -1,16 +1,12 @@ namespace DigitalLearningSolutions.Data.DataServices { + using Dapper; + using DigitalLearningSolutions.Data.Models.CompetencyAssessments; + using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Data; using System.Linq; - using Dapper; - using DigitalLearningSolutions.Data.Models.Common; - using DigitalLearningSolutions.Data.Models.CompetencyAssessments; - using DigitalLearningSolutions.Data.Models.Frameworks; - using DocumentFormat.OpenXml.Wordprocessing; - using Microsoft.Extensions.Logging; - public interface ICompetencyAssessmentDataService { //GET DATA @@ -28,6 +24,18 @@ public interface ICompetencyAssessmentDataService CompetencyAssessmentTaskStatus GetOrInsertAndReturnAssessmentTaskStatus(int assessmentId, bool frameworkBased); + int[] GetLinkedFrameworkIds(int assessmentId); + + int? GetPrimaryLinkedFrameworkId(int assessmentId); + + int GetCompetencyCountByFrameworkId(int assessmentId, int frameworkId); + + IEnumerable GetCompetenciesForCompetencyAssessment(int competencyAssessmentId); + IEnumerable GetLinkedFrameworksForCompetencyAssessment(int competencyAssessmentId); + int[] GetLinkedFrameworkCompetencyIds(int competencyAssessmentId, int frameworkId); + CompetencyAssessmentFeatures? GetCompetencyAssessmentFeaturesTaskStatus(int competencyAssessmentId); + int? GetSelfAssessmentStructure(int competencyAssessmentId); + //UPDATE DATA bool UpdateCompetencyAssessmentName(int competencyAssessmentId, int adminId, string competencyAssessmentName); @@ -44,10 +52,31 @@ int categoryId bool UpdateBrandingTaskStatus(int assessmentId, bool taskStatus); bool UpdateVocabularyTaskStatus(int assessmentId, bool taskStatus); bool UpdateRoleProfileLinksTaskStatus(int assessmentId, bool taskStatus); + bool UpdateFrameworkLinksTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus); + bool RemoveSelfAssessmentFramework(int assessmentId, int frameworkId, int adminId); + bool UpdateSelectCompetenciesTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus); + bool UpdateOptionalCompetenciesTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus); + bool UpdateRoleRequirementsTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus); + void MoveCompetencyInSelfAssessment(int competencyAssessmentId, + int competencyId, + string direction + ); + void MoveCompetencyGroupInSelfAssessment(int competencyAssessmentId, + int groupId, + string direction + ); + public bool UpdateCompetencyAssessmentFeaturesTaskStatus(int id, bool descriptionStatus, bool providerandCategoryStatus, bool vocabularyStatus, + bool workingGroupStatus, bool AllframeworkCompetenciesStatus); + void UpdateSelfAssessmentFromFramework(int selfAssessmentId, int? frameworkId); + //INSERT DATA int InsertCompetencyAssessment(int adminId, int centreId, string competencyAssessmentName); bool InsertSelfAssessmentFramework(int adminId, int selfAssessmentId, int frameworkId); + bool InsertCompetenciesIntoAssessmentFromFramework(int[] selectedCompetencyIds, int frameworkId, int competencyAssessmentId); + bool InsertSelfAssessmentStructure(int selfAssessmentId, int? frameworkId); //DELETE DATA + bool RemoveFrameworkCompetenciesFromAssessment(int competencyAssessmentId, int frameworkId); + bool RemoveCompetencyFromAssessment(int competencyAssessmentId, int competencyId); } public class CompetencyAssessmentDataService : ICompetencyAssessmentDataService @@ -77,13 +106,15 @@ FROM AdminUsers sa.Archived, sa.LastEdit, STUFF(( - SELECT - ', ' + f.FrameworkName - FROM - Frameworks f - WHERE - f.ID = saf.FrameworkId - FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS LinkedFrameworks, + SELECT + ', ' + f.FrameworkName + FROM + SelfAssessmentFrameworks saf2 + INNER JOIN Frameworks f ON f.ID = saf2.FrameworkId + WHERE + saf2.SelfAssessmentId = sa.ID + FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '' + ) AS LinkedFrameworks, (SELECT ProfessionalGroup FROM NRPProfessionalGroups WHERE (ID = sa.NRPProfessionalGroupID)) AS NRPProfessionalGroup, @@ -100,8 +131,7 @@ FROM NRPRoles private const string SelfAssessmentTables = @" LEFT OUTER JOIN - SelfAssessmentReviews AS sar ON sac.ID = sar.SelfAssessmentCollaboratorID AND sar.Archived IS NULL AND sar.ReviewComplete IS NULL - LEFT OUTER JOIN SelfAssessmentFrameworks AS saf ON sa.ID = saf.SelfAssessmentId"; + SelfAssessmentReviews AS sar ON sac.ID = sar.SelfAssessmentCollaboratorID AND sar.Archived IS NULL AND sar.ReviewComplete IS NULL"; private readonly IDbConnection connection; private readonly ILogger logger; @@ -324,13 +354,27 @@ public bool UpdateCompetencyAssessmentVocabulary(int competencyAssessmentId, int public bool InsertSelfAssessmentFramework(int adminId, int selfAssessmentId, int frameworkId) { + bool isPrimary = Convert.ToInt32(connection.ExecuteScalar( + @"SELECT Count(1) FROM SelfAssessmentFrameworks + WHERE SelfAssessmentId = @selfAssessmentId AND IsPrimary = 1", new { selfAssessmentId })) == 0; + var numberOfAffectedRows = connection.Execute( - @"INSERT INTO SelfAssessmentFrameworks (SelfAssessmentId, FrameworkId, CreatedByAdminId) - SELECT @selfAssessmentId, @frameworkId, @adminId + @"INSERT INTO SelfAssessmentFrameworks (SelfAssessmentId, FrameworkId, CreatedByAdminId, IsPrimary) + SELECT @selfAssessmentId, @frameworkId, @adminId, @isPrimary WHERE NOT EXISTS (SELECT 1 FROM SelfAssessmentFrameworks WHERE SelfAssessmentId = @selfAssessmentId AND FrameworkId = @frameworkId)" + , + new { adminId, selfAssessmentId, frameworkId, isPrimary } + ); + if (numberOfAffectedRows < 1) + { + numberOfAffectedRows = connection.Execute( + @"UPDATE SelfAssessmentFrameworks + SET RemovedDate = NULL, RemovedByAdminId = NULL, AmendedByAdminId = @adminId + WHERE SelfAssessmentId = @selfAssessmentId AND FrameworkId = @frameworkId" , new { adminId, selfAssessmentId, frameworkId } ); + } if (numberOfAffectedRows < 1) { logger.LogWarning( @@ -459,5 +503,367 @@ public bool UpdateRoleProfileLinksTaskStatus(int assessmentId, bool taskStatus) } return true; } + + public int[] GetLinkedFrameworkIds(int assessmentId) + { + return [.. connection.Query( + @"SELECT FrameworkId + FROM SelfAssessmentFrameworks + WHERE (SelfAssessmentId = @assessmentId) AND (RemovedDate IS NULL) AND (IsPrimary = 0) + ORDER BY ID", + new { assessmentId } + )]; + } + + public bool RemoveSelfAssessmentFramework(int assessmentId, int frameworkId, int adminId) + { + var numberOfAffectedRows = connection.Execute( + @"UPDATE SelfAssessmentFrameworks SET RemovedDate = @removedDate, RemovedByAdminId = @adminId + WHERE SelfAssessmentId = @assessmentId AND FrameworkId = @frameworkId", + new { removedDate = DateTime.Now, assessmentId, frameworkId, adminId } + ); + if (numberOfAffectedRows < 1) + { + logger.LogWarning( + "Not updating SelfAssessmentFrameworks as db update failed. " + + $"assessmentId: {assessmentId}, frameworkId: {frameworkId}, adminId: {adminId}" + ); + return false; + } + return true; + } + + public int? GetPrimaryLinkedFrameworkId(int assessmentId) + { + return connection.QuerySingleOrDefault( + @"SELECT TOP(1) FrameworkId + FROM SelfAssessmentFrameworks + WHERE (SelfAssessmentId = @assessmentId) AND (RemovedDate IS NULL) AND (IsPrimary = 1) + ORDER BY ID DESC", + new { assessmentId } + ); + } + + public bool UpdateFrameworkLinksTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus) + { + var numberOfAffectedRows = connection.Execute( + @"UPDATE SelfAssessmentTaskStatus SET FrameworkLinksTaskStatus = @taskStatus + WHERE SelfAssessmentId = @assessmentId AND (@previousStatus IS NULL OR FrameworkLinksTaskStatus = @previousStatus)", + new { assessmentId, taskStatus, previousStatus } + ); + if (numberOfAffectedRows < 1) + { + logger.LogWarning( + "Not updating FrameworkLinksTaskStatus as db update failed. " + + $"assessmentId: {assessmentId}, taskStatus: {taskStatus}" + ); + return false; + } + return true; + } + + public int GetCompetencyCountByFrameworkId(int assessmentId, int frameworkId) + { + return connection.ExecuteScalar( + @"SELECT COUNT(sas.CompetencyID) AS Competencies + FROM SelfAssessmentStructure AS sas INNER JOIN + FrameworkCompetencies AS fc ON sas.CompetencyID = fc.CompetencyID INNER JOIN + SelfAssessmentFrameworks AS saf ON fc.FrameworkID = saf.FrameworkId AND sas.SelfAssessmentID = saf.SelfAssessmentId + WHERE (saf.SelfAssessmentId = @assessmentId) AND (saf.FrameworkId = @frameworkId)", + new { assessmentId, frameworkId } + ); + } + + public bool RemoveFrameworkCompetenciesFromAssessment(int competencyAssessmentId, int frameworkId) + { + var numberOfAffectedRows = connection.Execute( + @"DELETE FROM SelfAssessmentStructure + FROM SelfAssessmentStructure INNER JOIN + FrameworkCompetencies AS fc ON SelfAssessmentStructure.CompetencyID = fc.CompetencyID INNER JOIN + SelfAssessmentFrameworks AS saf ON fc.FrameworkID = saf.FrameworkId AND SelfAssessmentStructure.SelfAssessmentID = saf.SelfAssessmentId + WHERE (saf.SelfAssessmentId = @competencyAssessmentId) AND (saf.FrameworkId = @frameworkId)", + new { competencyAssessmentId, frameworkId } + ); + if (numberOfAffectedRows < 1) + { + logger.LogWarning( + "Not removing competencies linked to source framework as db update failed. " + + $"assessmentId: {competencyAssessmentId}, taskStatus: {frameworkId}" + ); + return false; + } + return true; + } + + public bool UpdateSelectCompetenciesTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus) + { + var numberOfAffectedRows = connection.Execute( + @"UPDATE SelfAssessmentTaskStatus SET SelectCompetenciesTaskStatus = @taskStatus + WHERE SelfAssessmentId = @assessmentId AND (@previousStatus IS NULL OR SelectCompetenciesTaskStatus = @previousStatus)", + new { assessmentId, taskStatus, previousStatus } + ); + if (numberOfAffectedRows < 1) + { + logger.LogWarning( + "Not updating SelectCompetenciesTaskStatus as db update failed. " + + $"assessmentId: {assessmentId}, taskStatus: {taskStatus}" + ); + return false; + } + return true; + } + public bool UpdateOptionalCompetenciesTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus) + { + var numberOfAffectedRows = connection.Execute( + @"UPDATE SelfAssessmentTaskStatus SET OptionalCompetenciesTaskStatus = @taskStatus + WHERE SelfAssessmentId = @assessmentId AND (@previousStatus IS NULL OR OptionalCompetenciesTaskStatus = @previousStatus)", + new { assessmentId, taskStatus, previousStatus } + ); + if (numberOfAffectedRows < 1) + { + logger.LogWarning( + "Not updating OptionalCompetenciesTaskStatus as db update failed. " + + $"assessmentId: {assessmentId}, taskStatus: {taskStatus}" + ); + return false; + } + return true; + } + public bool UpdateRoleRequirementsTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus) + { + var numberOfAffectedRows = connection.Execute( + @"UPDATE SelfAssessmentTaskStatus SET RoleRequirementsTaskStatus = @taskStatus + WHERE SelfAssessmentId = @assessmentId AND (@previousStatus IS NULL OR RoleRequirementsTaskStatus = @previousStatus)", + new { assessmentId, taskStatus, previousStatus } + ); + if (numberOfAffectedRows < 1) + { + logger.LogWarning( + "Not updating RoleRequirementsTaskStatus as db update failed. " + + $"assessmentId: {assessmentId}, taskStatus: {taskStatus}" + ); + return false; + } + return true; + } + + public IEnumerable GetCompetenciesForCompetencyAssessment(int competencyAssessmentId) + { + return connection.Query( + @"SELECT sas.ID AS StructureId, sas.CompetencyID, f.ID AS FrameworkId, f.FrameworkName, cg.ID AS GroupId, cg.Name AS GroupName, c.Name AS CompetencyName, c.Description AS CompetencyDescription, sas.Optional + FROM SelfAssessmentStructure AS sas INNER JOIN + Competencies AS c ON sas.CompetencyID = c.ID INNER JOIN + CompetencyGroups AS cg ON sas.CompetencyGroupID = cg.ID INNER JOIN + FrameworkCompetencies ON c.ID = FrameworkCompetencies.CompetencyID INNER JOIN + Frameworks AS f ON FrameworkCompetencies.FrameworkID = f.ID INNER JOIN + SelfAssessmentFrameworks ON f.ID = SelfAssessmentFrameworks.FrameworkId AND sas.SelfAssessmentID = SelfAssessmentFrameworks.SelfAssessmentId + WHERE (sas.SelfAssessmentID = @competencyAssessmentId) + ORDER BY sas.Ordering", new { competencyAssessmentId } + ); + } + + public IEnumerable GetLinkedFrameworksForCompetencyAssessment(int competencyAssessmentId) + { + return connection.Query( + @"SELECT f.ID, + FrameworkName, + f.OwnerAdminID, + f.BrandID, + f.CategoryID, + f.TopicID, + f.CreatedDate, + f.PublishStatusID, + f.UpdatedByAdminID + FROM SelfAssessmentFrameworks saf INNER JOIN + Frameworks AS f ON saf.FrameworkId = f.ID + WHERE (saf.SelfAssessmentId = @competencyAssessmentId) AND (saf.RemovedDate IS NULL) + ORDER BY f.FrameworkName", new { competencyAssessmentId } + ); + } + + public int[] GetLinkedFrameworkCompetencyIds(int competencyAssessmentId, int frameworkId) + { + return [.. connection.Query( + @"SELECT sas.CompetencyID + FROM SelfAssessmentStructure AS sas INNER JOIN + FrameworkCompetencies AS fc ON sas.CompetencyID = fc.CompetencyID + WHERE (sas.SelfAssessmentID = @competencyAssessmentId) AND (fc.FrameworkID = @frameworkId) + ORDER BY fc.Ordering", + new { competencyAssessmentId, frameworkId} + )]; + } + + public bool InsertCompetenciesIntoAssessmentFromFramework(int[] selectedCompetencyIds, int frameworkId, int competencyAssessmentId) + { + var currentMaxOrdering = connection.ExecuteScalar( + @"SELECT ISNULL(MAX(Ordering), 0) FROM SelfAssessmentStructure WHERE SelfAssessmentID = @competencyAssessmentId", + new { competencyAssessmentId } + ); + var numberOfAffectedRows = connection.Execute( + @"INSERT INTO SelfAssessmentStructure (SelfAssessmentID, CompetencyID, Ordering, CompetencyGroupID) + SELECT + @competencyAssessmentId, + FC.CompetencyID, + ROW_NUMBER() OVER (ORDER BY FCG.Ordering, FC.Ordering) + @currentMaxOrdering, + FCG.CompetencyGroupID + FROM FrameworkCompetencies AS FC + INNER JOIN FrameworkCompetencyGroups AS FCG ON FC.FrameworkCompetencyGroupID = FCG.ID + WHERE FC.FrameworkID = @frameworkId + AND FC.CompetencyID IN @selectedCompetencyIds AND FC.CompetencyID NOT IN (SELECT CompetencyID FROM SelfAssessmentStructure WHERE SelfAssessmentID = @competencyAssessmentId)", + new { selectedCompetencyIds, frameworkId, competencyAssessmentId, currentMaxOrdering } + ); + if (numberOfAffectedRows < 1) + { + logger.LogWarning( + "Not inserting competencies into assessment as db update failed. " + + $"assessmentId: {competencyAssessmentId}, frameworkId: {frameworkId}, selectedCompetencyIds: {selectedCompetencyIds}" + ); + return false; + } + return true; + } + public bool RemoveCompetencyFromAssessment(int competencyAssessmentId, int competencyId) + { + var numberOfAffectedRows = connection.Execute( + @"DELETE FROM SelfAssessmentStructure + WHERE SelfAssessmentID = @competencyAssessmentId AND CompetencyID = @competencyId", + new { competencyAssessmentId, competencyId } + ); + if (numberOfAffectedRows < 1) + { + logger.LogWarning( + "Not removing competency from assessment as db update failed. " + + $"assessmentId: {competencyAssessmentId}, competencyId: {competencyId}" + ); + return false; + } + return true; + } + + public void MoveCompetencyInSelfAssessment(int competencyAssessmentId, int competencyId, string direction) + { + connection.Execute( + "usp_MoveCompetencyInSelfAssessment", + new { SelfAssessmentID = competencyAssessmentId, CompetencyID = competencyId, Direction = direction }, + commandType: CommandType.StoredProcedure + ); + + } + + public void MoveCompetencyGroupInSelfAssessment(int competencyAssessmentId, int groupId, string direction) + { + connection.Execute( + "usp_MoveCompetencyGroupInSelfAssessment", + new { SelfAssessmentID = competencyAssessmentId, GroupID = groupId, Direction = direction }, + commandType: CommandType.StoredProcedure + ); + } + + public bool UpdateCompetencyAssessmentFeaturesTaskStatus(int id, bool descriptionStatus, bool providerandCategoryStatus, bool vocabularyStatus, + bool workingGroupStatus, bool AllframeworkCompetenciesStatus) + { + var numberOfAffectedRows = connection.Execute( + @"IF EXISTS (SELECT 1 FROM SelfAssessmentTaskStatus WHERE SelfAssessmentId = @id) + BEGIN + UPDATE SelfAssessmentTaskStatus + SET IntroductoryTextTaskStatus = CASE WHEN @descriptionStatus = 1 THEN 1 ELSE NULL END, + BrandingTaskStatus = CASE WHEN @providerandCategoryStatus = 1 THEN 1 ELSE NULL END, + VocabularyTaskStatus = CASE WHEN @vocabularyStatus = 1 THEN 1 ELSE NULL END, + WorkingGroupTaskStatus = CASE WHEN @workingGroupStatus = 1 THEN 1 ELSE NULL END, + FrameworkLinksTaskStatus = CASE WHEN @AllframeworkCompetenciesStatus = 1 THEN 1 ELSE NULL END + WHERE SelfAssessmentId = @id; + END + ELSE + BEGIN + INSERT INTO SelfAssessmentTaskStatus + (SelfAssessmentId, IntroductoryTextTaskStatus, BrandingTaskStatus, VocabularyTaskStatus, WorkingGroupTaskStatus, FrameworkLinksTaskStatus) + VALUES + ( + @id, + CASE WHEN @descriptionStatus = 1 THEN 1 ELSE NULL END, + CASE WHEN @providerandCategoryStatus = 1 THEN 1 ELSE NULL END, + CASE WHEN @vocabularyStatus = 1 THEN 1 ELSE NULL END, + CASE WHEN @workingGroupStatus = 1 THEN 1 ELSE NULL END, + CASE WHEN @AllframeworkCompetenciesStatus = 1 THEN 1 ELSE NULL END + ); + END", + new { id, descriptionStatus, providerandCategoryStatus, vocabularyStatus, workingGroupStatus, AllframeworkCompetenciesStatus } + ); + if (numberOfAffectedRows < 1) + { + logger.LogWarning( + "Not updating SelfAssessmentTaskStatus as db update failed. " + + $"SelfAssessmentId: {id}, IntroductoryTextTaskStatus: {descriptionStatus}, BrandingTaskStatus: {providerandCategoryStatus}, " + + $"VocabularyTaskStatus: {vocabularyStatus}, WorkingGroupTaskStatus: {workingGroupStatus}, FrameworkLinksTaskStatus: {AllframeworkCompetenciesStatus}" + ); + return false; + } + return true; + } + + public CompetencyAssessmentFeatures? GetCompetencyAssessmentFeaturesTaskStatus(int competencyAssessmentId) + { + return connection.QueryFirstOrDefault( + @"SELECT s.ID, s.Name AS CompetencyAssessmentName, sts.IntroductoryTextTaskStatus AS DescriptionStatus, sts.BrandingTaskStatus AS ProviderandCategoryStatus, + sts.VocabularyTaskStatus AS VocabularyStatus, sts.WorkingGroupTaskStatus AS WorkingGroupStatus, sts.FrameworkLinksTaskStatus AS AllframeworkCompetenciesStatus + FROM SelfAssessments s INNER JOIN + SelfAssessmentTaskStatus sts ON s.ID = sts.SelfAssessmentId + WHERE s.ID = @competencyAssessmentId", + new { competencyAssessmentId } + ); + + } + + public void UpdateSelfAssessmentFromFramework( int selfAssessmentId, int? frameworkId) + { + + var numberOfAffectedRows = connection.Execute( + @"UPDATE s + SET + [Description] = COALESCE(F.[Description], 'No description provided'), + BrandID = F.BrandID, + CategoryID = F.CategoryID, + CreatedByCentreID = AU.CentreID, + CreatedByAdminID = F.OwnerAdminID + FROM SelfAssessments s + INNER JOIN Frameworks F ON F.ID = @frameworkId + INNER JOIN AdminUsers AU ON F.OwnerAdminID = AU.AdminID + WHERE s.id = @selfAssessmentId;" + , + new {selfAssessmentId, frameworkId } + ); + } + public bool InsertSelfAssessmentStructure(int selfAssessmentId, int? frameworkId) + { + + var numberOfAffectedRows = connection.Execute( + @"INSERT INTO SelfAssessmentStructure (SelfAssessmentID, CompetencyID, Ordering, CompetencyGroupID) + SELECT s.ID, FC.CompetencyID, ROW_NUMBER() OVER( ORDER BY FCG.Ordering, FC.Ordering ), FCG.CompetencyGroupID + FROM FrameworkCompetencies AS FC + INNER JOIN FrameworkCompetencyGroups AS FCG ON FC.FrameworkCompetencyGroupID = FCG.ID INNER JOIN + SelfAssessments s ON s.id = @selfAssessmentId + WHERE FC.FrameworkID = @frameworkId" + , + new { selfAssessmentId, frameworkId } + ); + if (numberOfAffectedRows < 1) + { + logger.LogWarning( + "Not inserting SelfAssessmentStructure record as db insert failed. " + + $"selfAssessmentId: {selfAssessmentId}, frameworkId: {frameworkId}" + ); + return false; + } + + return true; + } + public int? GetSelfAssessmentStructure(int competencyAssessmentId) + { + return connection.QueryFirstOrDefault( + @"SELECT 1 from dbo.SelfAssessmentStructure where selfassessmentid = @competencyAssessmentId", + new { competencyAssessmentId } + ); + + } } } diff --git a/DigitalLearningSolutions.Data/DataServices/CompetencyLearningResourcesDataService.cs b/DigitalLearningSolutions.Data/DataServices/CompetencyLearningResourcesDataService.cs index 323188bf45..f00b2b1a71 100644 --- a/DigitalLearningSolutions.Data/DataServices/CompetencyLearningResourcesDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/CompetencyLearningResourcesDataService.cs @@ -14,6 +14,7 @@ public interface ICompetencyLearningResourcesDataService IEnumerable GetCompetencyResourceAssessmentQuestionParameters(IEnumerable competencyLearningResourceIds); int AddCompetencyLearningResource(int resourceRefID, string originalResourceName, string description, string resourceType, string link, string catalogue, decimal rating, int competencyID, int adminId); + IEnumerable GetActiveCompetencyLearningResourcesByCompetencyIdAndReferenceId(int competencyId, int referenceId); } public class CompetencyLearningResourcesDataService : ICompetencyLearningResourcesDataService @@ -120,5 +121,20 @@ FROM CompetencyResourceAssessmentQuestionParameters new { competencyLearningResourceIds } ); } + public IEnumerable GetActiveCompetencyLearningResourcesByCompetencyIdAndReferenceId(int competencyId, int referenceId) + { + return connection.Query( + @"SELECT + clr.ID, + clr.CompetencyID, + clr.LearningResourceReferenceID, + clr.AdminID, + lrr.ResourceRefID AS LearningHubResourceReferenceId + FROM CompetencyLearningResources AS clr + INNER JOIN LearningResourceReferences AS lrr ON lrr.ID = clr.LearningResourceReferenceID + WHERE CompetencyID = @competencyId AND ResourceRefID = @referenceId AND clr.RemovedDate IS NULL", + new { competencyId, referenceId } + ); + } } } diff --git a/DigitalLearningSolutions.Data/DataServices/FrameworkDataService.cs b/DigitalLearningSolutions.Data/DataServices/FrameworkDataService.cs index 125a66aaca..79245643d8 100644 --- a/DigitalLearningSolutions.Data/DataServices/FrameworkDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/FrameworkDataService.cs @@ -51,9 +51,9 @@ public interface IFrameworkDataService CollaboratorNotification? GetCollaboratorNotification(int id, int invitedByAdminId); // Competencies/groups: - IEnumerable GetFrameworkCompetencyGroups(int frameworkId); + IEnumerable GetFrameworkCompetencyGroups(int frameworkId, int? assessmentId); - IEnumerable GetFrameworkCompetenciesUngrouped(int frameworkId); + IEnumerable GetFrameworkCompetenciesUngrouped(int frameworkId, int? assessmentId); CompetencyGroupBase? GetCompetencyGroupBaseById(int Id); @@ -62,6 +62,7 @@ public interface IFrameworkDataService int GetMaxFrameworkCompetencyID(); int GetMaxFrameworkCompetencyGroupID(); + int GetFrameworkCompetencyGroupId(int frameworkId, int competencyGroupId); // Assessment questions: IEnumerable GetAllCompetencyQuestions(int adminId); @@ -129,9 +130,9 @@ bool zeroBased IEnumerable GetAllCompetenciesForAdminId(string name, int adminId); - int InsertCompetency(string name, string? description, int adminId); + int InsertCompetency(string name, string? description, int adminId, bool alwaysShowDescription = false); - int InsertFrameworkCompetency(int competencyId, int? frameworkCompetencyGroupID, int adminId, int frameworkId, bool alwaysShowDescription = false); + int InsertFrameworkCompetency(int competencyId, int? frameworkCompetencyGroupID, int adminId, int frameworkId, bool addDefaultQuestions = true); int AddCollaboratorToFramework(int frameworkId, string userEmail, bool canModify, int? centreID); @@ -198,7 +199,7 @@ int adminId void UpdateFrameworkConfig(int frameworkId, int adminId, string? frameworkConfig); - void UpdateFrameworkCompetencyGroup( + bool UpdateFrameworkCompetencyGroup( int frameworkCompetencyGroupId, int competencyGroupId, string name, @@ -251,9 +252,9 @@ string direction void RemoveCustomFlag(int flagId); void RemoveCollaboratorFromFramework(int frameworkId, int id); - void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int competencyGroupId, int adminId); + void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int competencyGroupId, int frameworkId, int adminId); - void DeleteFrameworkCompetency(int frameworkCompetencyId, int adminId); + void DeleteFrameworkCompetency(int frameworkCompetencyId, int? frameworkCompetencyGroupId, int frameworkId, int adminId); void DeleteFrameworkDefaultQuestion( int frameworkId, @@ -265,6 +266,7 @@ bool deleteFromExisting void DeleteCompetencyAssessmentQuestion(int frameworkCompetencyId, int assessmentQuestionId, int adminId); void DeleteCompetencyLearningResource(int competencyLearningResourceId, int adminId); + void UpdateFrameworkCompetencyFrameworkCompetencyGroup(int? competencyGroupId, int frameworkCompetencyGroupId, int adminId); } public class FrameworkDataService : IFrameworkDataService @@ -275,14 +277,37 @@ public class FrameworkDataService : IFrameworkDataService OwnerAdminID, (SELECT Forename + ' ' + Surname + (CASE WHEN Active = 1 THEN '' ELSE ' (Inactive)' END) AS Expr1 FROM AdminUsers WHERE (AdminID = FW.OwnerAdminID)) AS Owner, BrandID, - CategoryID, + FW.CategoryID, TopicID, CreatedDate, PublishStatusID, UpdatedByAdminID, (SELECT Forename + ' ' + Surname + (CASE WHEN Active = 1 THEN '' ELSE ' (Inactive)' END) AS Expr1 FROM AdminUsers AS AdminUsers_1 WHERE (AdminID = FW.UpdatedByAdminID)) AS UpdatedBy, - CASE WHEN FW.OwnerAdminID = @adminId THEN 3 WHEN fwc.CanModify = 1 THEN 2 WHEN fwc.CanModify = 0 THEN 1 ELSE 0 END AS UserRole, - fwr.ID AS FrameworkReviewID"; + CASE + WHEN (aa.UserID = (SELECT UserID FROM AdminAccounts WHERE ID = @adminId)) THEN 3 + WHEN (fwc.CanModify = 1) OR + (SELECT COUNT(*) + FROM FrameworkCollaborators fc + JOIN AdminAccounts aa1 ON fc.AdminID = aa1.ID + WHERE fc.FrameworkID = fw.ID + AND fc.CanModify = 1 AND fc.IsDeleted = 0 + AND aa1.UserID = (SELECT aa2.UserID FROM AdminAccounts aa2 WHERE aa2.ID = @adminId)) > 0 THEN 2 + WHEN (fwc.CanModify = 0) OR + (SELECT COUNT(*) + FROM FrameworkCollaborators fc + JOIN AdminAccounts aa3 ON fc.AdminID = aa3.ID + WHERE fc.FrameworkID = fw.ID + AND fc.CanModify = 0 AND fc.IsDeleted = 0 + AND aa3.UserID = (SELECT aa4.UserID FROM AdminAccounts aa4 WHERE aa4.ID = @adminId)) > 0 THEN 1 + ELSE 0 + END AS UserRole, + (SELECT TOP(1) fwr.ID + FROM FrameworkCollaborators fc + INNER JOIN AdminAccounts aa3 ON fc.AdminID = aa3.ID + LEFT OUTER JOIN FrameworkReviews AS fwr ON fc.ID = fwr.FrameworkCollaboratorID AND fwr.Archived IS NULL AND fwr.ReviewComplete IS NULL + WHERE fc.FrameworkID = fw.ID AND fc.IsDeleted = 0 + AND aa3.UserID = (SELECT aa4.UserID FROM AdminAccounts aa4 WHERE aa4.ID = @adminId) + AND aa3.Active = 1 ORDER BY fwr.ID DESC) AS FrameworkReviewID"; private const string BrandedFrameworkFields = @", FW.Description, FW.FrameworkConfig AS Vocabulary, (SELECT BrandName @@ -302,9 +327,8 @@ FROM CourseTopics private const string FlagFields = @"fl.ID AS FlagId, fl.FrameworkId, fl.FlagName, fl.FlagGroup, fl.FlagTagClass"; private const string FrameworkTables = - @"Frameworks AS FW LEFT OUTER JOIN - FrameworkCollaborators AS fwc ON fwc.FrameworkID = FW.ID AND fwc.AdminID = @adminId AND COALESCE(IsDeleted, 0) = 0 - LEFT OUTER JOIN FrameworkReviews AS fwr ON fwc.ID = fwr.FrameworkCollaboratorID AND fwr.Archived IS NULL AND fwr.ReviewComplete IS NULL"; + @"Frameworks AS FW INNER JOIN AdminAccounts AS aa ON aa.ID = fw.OwnerAdminID + LEFT OUTER JOIN FrameworkCollaborators AS fwc ON fwc.FrameworkID = FW.ID AND fwc.AdminID = @adminId AND COALESCE(IsDeleted, 0) = 0 "; private const string AssessmentQuestionFields = @"SELECT AQ.ID, AQ.Question, AQ.MinValue, AQ.MaxValue, AQ.AssessmentQuestionInputTypeID, AQI.InputTypeName, AQ.AddedByAdminId, CASE WHEN AQ.AddedByAdminId = @adminId THEN 1 ELSE 0 END AS UserIsOwner, AQ.CommentsPrompt, AQ.CommentsHint"; @@ -617,7 +641,7 @@ FROM [FrameworkCompetencyGroups] return existingId; } - public int InsertCompetency(string name, string? description, int adminId) + public int InsertCompetency(string name, string? description, int adminId, bool alwaysShowDescription = false) { if ((name.Length == 0) | (adminId < 1)) { @@ -629,10 +653,10 @@ public int InsertCompetency(string name, string? description, int adminId) description = (description?.Trim() == "" ? null : description); var existingId = connection.QuerySingle( - @"INSERT INTO Competencies ([Name], [Description], UpdatedByAdminID) + @"INSERT INTO Competencies ([Name], [Description], UpdatedByAdminID, AlwaysShowDescription) OUTPUT INSERTED.Id - VALUES (@name, @description, @adminId)", - new { name, description, adminId } + VALUES (@name, @description, @adminId, @alwaysShowDescription)", + new { name, description, adminId, alwaysShowDescription } ); return existingId; @@ -643,7 +667,7 @@ public int InsertFrameworkCompetency( int? frameworkCompetencyGroupID, int adminId, int frameworkId, - bool alwaysShowDescription = false + bool addDefaultQuestions = true ) { if ((competencyId < 1) | (adminId < 1) | (frameworkId < 1)) @@ -678,9 +702,11 @@ public int InsertFrameworkCompetency( var numberOfAffectedRows = connection.Execute( @"INSERT INTO FrameworkCompetencies ([CompetencyID], FrameworkCompetencyGroupID, UpdatedByAdminID, Ordering, FrameworkID) VALUES (@competencyId, @frameworkCompetencyGroupID, @adminId, COALESCE - ((SELECT MAX(Ordering) AS OrderNum - FROM [FrameworkCompetencies] - WHERE ([FrameworkCompetencyGroupID] = @frameworkCompetencyGroupID)), 0)+1, @frameworkId)", + ((SELECT MAX(Ordering) AS OrderNum + FROM [FrameworkCompetencies] + WHERE ((@frameworkCompetencyGroupID IS NULL AND FrameworkCompetencyGroupID IS NULL) OR + (@frameworkCompetencyGroupID IS NOT NULL AND FrameworkCompetencyGroupID = @frameworkCompetencyGroupID)) AND + FrameworkID = @frameworkId ), 0)+1, @frameworkId)", new { competencyId, frameworkCompetencyGroupID, adminId, frameworkId } ); if (numberOfAffectedRows < 1) @@ -705,8 +731,10 @@ FROM [FrameworkCompetencies] new { competencyId, frameworkCompetencyGroupID } ); } - - AddDefaultQuestionsToCompetency(competencyId, frameworkId); + if (addDefaultQuestions) + { + AddDefaultQuestionsToCompetency(competencyId, frameworkId); + } return existingId; } @@ -833,17 +861,22 @@ public void RemoveCustomFlag(int flagId) ); } - public IEnumerable GetFrameworkCompetencyGroups(int frameworkId) + public IEnumerable GetFrameworkCompetencyGroups(int frameworkId, int? assessmentId) { + var assessmentFilter = assessmentId.HasValue ? + @$"AND c.ID NOT IN (SELECT CompetencyID + FROM SelfAssessmentStructure + WHERE (SelfAssessmentID = {assessmentId}))" + : string.Empty; var result = connection.Query( - @"SELECT fcg.ID, fcg.CompetencyGroupID, cg.Name, fcg.Ordering, fc.ID, c.ID AS CompetencyID, c.Name, c.Description, fc.Ordering, COUNT(caq.AssessmentQuestionID) AS AssessmentQuestions + @$"SELECT fcg.ID, fcg.CompetencyGroupID, cg.Name, fcg.Ordering, fc.ID, c.ID AS CompetencyID, c.Name, c.Description, fc.Ordering, + (SELECT COUNT(*) FROM CompetencyAssessmentQuestions caq WHERE caq.CompetencyID = c.ID) AS AssessmentQuestions ,(SELECT COUNT(*) FROM CompetencyLearningResources clr WHERE clr.CompetencyID = c.ID AND clr.RemovedDate IS NULL) AS CompetencyLearningResourcesCount FROM FrameworkCompetencyGroups AS fcg INNER JOIN CompetencyGroups AS cg ON fcg.CompetencyGroupID = cg.ID LEFT OUTER JOIN FrameworkCompetencies AS fc ON fcg.ID = fc.FrameworkCompetencyGroupID LEFT OUTER JOIN - Competencies AS c ON fc.CompetencyID = c.ID LEFT OUTER JOIN - CompetencyAssessmentQuestions AS caq ON c.ID = caq.CompetencyID - WHERE (fcg.FrameworkID = @frameworkId) + Competencies AS c ON fc.CompetencyID = c.ID + WHERE (fcg.FrameworkID = @frameworkId) {assessmentFilter} GROUP BY fcg.ID, fcg.CompetencyGroupID, cg.Name, fcg.Ordering, fc.ID, c.ID, c.Name, c.Description, fc.Ordering ORDER BY fcg.Ordering, fc.Ordering", (frameworkCompetencyGroup, frameworkCompetency) => @@ -854,29 +887,36 @@ FROM FrameworkCompetencyGroups AS fcg INNER JOIN }, new { frameworkId } ); - return result.GroupBy(frameworkCompetencyGroup => frameworkCompetencyGroup.CompetencyGroupID).Select( - group => - { - var groupedFrameworkCompetencyGroup = group.First(); - groupedFrameworkCompetencyGroup.FrameworkCompetencies = group.Where(frameworkCompetencyGroup => frameworkCompetencyGroup.FrameworkCompetencies.Count > 0) - .Select( - frameworkCompetencyGroup => frameworkCompetencyGroup.FrameworkCompetencies.Single() - ).ToList(); - return groupedFrameworkCompetencyGroup; - } - ); + return result + .GroupBy(fcg => fcg.CompetencyGroupID) + .Select(group => + { + var groupedFrameworkCompetencyGroup = group.First(); + + // Flatten all FrameworkCompetencies from all instances in this group + groupedFrameworkCompetencyGroup.FrameworkCompetencies = group + .SelectMany(g => g.FrameworkCompetencies) + .Where(fc => fc != null). + Distinct().ToList(); + + return groupedFrameworkCompetencyGroup; + }); } - public IEnumerable GetFrameworkCompetenciesUngrouped(int frameworkId) + public IEnumerable GetFrameworkCompetenciesUngrouped(int frameworkId, int? assessmentId) { + var assessmentFilter = assessmentId.HasValue ? + @$"AND c.ID NOT IN (SELECT CompetencyID + FROM SelfAssessmentStructure + WHERE (SelfAssessmentID = {assessmentId}))" + : string.Empty; return connection.Query( - @"SELECT fc.ID, c.ID AS CompetencyID, c.Name, c.Description, fc.Ordering, COUNT(caq.AssessmentQuestionID) AS AssessmentQuestions,(select COUNT(CompetencyId) from CompetencyLearningResources where CompetencyID=c.ID) AS CompetencyLearningResourcesCount + @$"SELECT fc.ID, c.ID AS CompetencyID, c.Name, c.Description, fc.Ordering, + (SELECT COUNT(*) FROM CompetencyAssessmentQuestions caq WHERE caq.CompetencyID = c.ID) AS AssessmentQuestions,(select COUNT(CompetencyId) from CompetencyLearningResources where CompetencyID=c.ID) AS CompetencyLearningResourcesCount FROM FrameworkCompetencies AS fc INNER JOIN Competencies AS c ON fc.CompetencyID = c.ID - LEFT OUTER JOIN - CompetencyAssessmentQuestions AS caq ON c.ID = caq.CompetencyID WHERE fc.FrameworkID = @frameworkId - AND fc.FrameworkCompetencyGroupID IS NULL + AND fc.FrameworkCompetencyGroupID IS NULL {assessmentFilter} GROUP BY fc.ID, c.ID, c.Name, c.Description, fc.Ordering ORDER BY fc.Ordering", new { frameworkId } @@ -973,7 +1013,7 @@ FROM FrameworkCompetencies AS fc ); } - public void UpdateFrameworkCompetencyGroup( + public bool UpdateFrameworkCompetencyGroup( int frameworkCompetencyGroupId, int competencyGroupId, string name, @@ -986,7 +1026,7 @@ int adminId logger.LogWarning( $"Not updating framework competency group as it failed server side validation. AdminId: {adminId}, frameworkCompetencyGroupId: {frameworkCompetencyGroupId}, competencyGroupId: {competencyGroupId}, name: {name}" ); - return; + return false; } var usedElsewhere = connection.QuerySingle( @@ -1005,6 +1045,7 @@ int adminId SET CompetencyGroupID = @newCompetencyGroupId, UpdatedByAdminID = @adminId WHERE ID = @frameworkCompetencyGroupId", new { newCompetencyGroupId, adminId, frameworkCompetencyGroupId } + ); if (numberOfAffectedRows < 1) { @@ -1012,22 +1053,32 @@ int adminId "Not updating competency group id as db update failed. " + $"newCompetencyGroupId: {newCompetencyGroupId}, admin id: {adminId}, frameworkCompetencyGroupId: {frameworkCompetencyGroupId}" ); + return false; + } + else + { + return true; } } + else + { + return false; + } } else { var numberOfAffectedRows = connection.Execute( @"UPDATE CompetencyGroups SET Name = @name, UpdatedByAdminID = @adminId, Description = @description - WHERE ID = @competencyGroupId", + WHERE ID = @competencyGroupId AND (Name <> @name OR ISNULL(Description, '') <> ISNULL(@description, ''))", new { name, adminId, competencyGroupId, description } ); if (numberOfAffectedRows < 1) { - logger.LogWarning( - "Not updating competency group name as db update failed. " + - $"Name: {name}, admin id: {adminId}, competencyGroupId: {competencyGroupId}" - ); + return false; + } + else + { + return true; } } } @@ -1119,7 +1170,7 @@ public void MoveFrameworkCompetency(int frameworkCompetencyId, bool singleStep, ); } - public void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int competencyGroupId, int adminId) + public void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int competencyGroupId, int frameworkId, int adminId) { if ((frameworkCompetencyGroupId < 1) | (adminId < 1)) { @@ -1150,7 +1201,23 @@ public void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int c new { frameworkCompetencyGroupId } ); - if (numberOfAffectedRows < 1) + if (numberOfAffectedRows > 0) + { + connection.Execute( + @"WITH Ranked AS ( +    SELECT ID, +            ROW_NUMBER() OVER (PARTITION BY FrameworkID ORDER BY Ordering) AS NewOrder +    FROM FrameworkCompetencyGroups + Where FrameworkID = @frameworkID + ) + UPDATE fcg + SET fcg.Ordering = r.NewOrder + FROM FrameworkCompetencyGroups fcg + JOIN Ranked r ON fcg.ID = r.ID;", + new { frameworkId } + ); + } + else { logger.LogWarning( "Not deleting framework competency group as db update failed. " + @@ -1195,7 +1262,7 @@ public void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int c } } - public void DeleteFrameworkCompetency(int frameworkCompetencyId, int adminId) + public void DeleteFrameworkCompetency(int frameworkCompetencyId, int? frameworkCompetencyGroupId, int frameworkId, int adminId) { var competencyId = connection.QuerySingle( @"SELECT CompetencyID FROM FrameworkCompetencies WHERE ID = @frameworkCompetencyId", @@ -1219,7 +1286,24 @@ public void DeleteFrameworkCompetency(int frameworkCompetencyId, int adminId) @"DELETE FROM FrameworkCompetencies WHERE ID = @frameworkCompetencyId", new { frameworkCompetencyId } ); - if (numberOfAffectedRows < 1) + if (numberOfAffectedRows > 0) + { + connection.Execute( + @"WITH Ranked AS ( +    SELECT ID, +            ROW_NUMBER() OVER (PARTITION BY FrameworkID ORDER BY Ordering) AS NewOrder +    FROM FrameworkCompetencies + Where (FrameworkCompetencyGroupID = @frameworkCompetencyGroupID) OR (FrameworkCompetencyGroupID IS NULL AND @frameworkCompetencyGroupID IS NULL) AND + FrameworkID = @frameworkID + ) + UPDATE fc + SET fc.Ordering = r.NewOrder + FROM FrameworkCompetencies fc + JOIN Ranked r ON fc.ID = r.ID;", + new { frameworkCompetencyGroupId, frameworkId } + ); + } + else { logger.LogWarning( "Not deleting framework competency as db update failed. " + @@ -1823,9 +1907,20 @@ FROM Competencies AS C INNER JOIN public int GetAdminUserRoleForFrameworkId(int adminId, int frameworkId) { return connection.QuerySingle( - @"SELECT CASE WHEN FW.OwnerAdminID = @adminId THEN 3 WHEN COALESCE (fwc.CanModify, 0) = 1 THEN 2 WHEN COALESCE (fwc.CanModify, 0) = 0 THEN 1 ELSE 0 END AS UserRole - FROM Frameworks AS FW LEFT OUTER JOIN - FrameworkCollaborators AS fwc ON fwc.FrameworkID = FW.ID AND fwc.AdminID = @adminId AND fwc.IsDeleted = 0 + @"SELECT CASE + WHEN (aa.UserID = (SELECT UserID FROM AdminAccounts WHERE ID = @adminId)) THEN 3 + WHEN (fwc.CanModify = 1) OR + (SELECT COUNT(*) + FROM FrameworkCollaborators fc + JOIN AdminAccounts aa1 ON fc.AdminID = aa1.ID + WHERE fc.FrameworkID = fw.ID + AND fc.CanModify = 1 AND fc.IsDeleted = 0 + AND aa1.UserID = (SELECT aa2.UserID FROM AdminAccounts aa2 WHERE aa2.ID = @adminId)) > 0 THEN 2 + WHEN fwc.CanModify = 0 THEN 1 ELSE 0 + END AS UserRole + FROM Frameworks AS FW INNER JOIN + AdminAccounts AS aa ON aa.ID = fw.OwnerAdminID LEFT OUTER JOIN + FrameworkCollaborators AS fwc ON fwc.FrameworkID = FW.ID AND fwc.AdminID = @adminId AND fwc.IsDeleted = 0 WHERE (FW.ID = @frameworkId)", new { adminId, frameworkId } ); @@ -1984,7 +2079,7 @@ FROM FrameworkComments public IEnumerable GetReviewersForFrameworkId(int frameworkId) { return connection.Query( - @"SELECT + @"SELECT DISTINCT fc.ID, fc.FrameworkID, fc.AdminID, @@ -1995,8 +2090,11 @@ public IEnumerable GetReviewersForFrameworkId(int frameworkI FROM FrameworkCollaborators fc INNER JOIN AdminUsers AS au ON fc.AdminID = au.AdminID LEFT OUTER JOIN FrameworkReviews ON fc.ID = FrameworkReviews.FrameworkCollaboratorID - WHERE (fc.FrameworkID = @FrameworkID) AND (FrameworkReviews.ID IS NULL) AND (fc.IsDeleted=0) OR - (fc.FrameworkID = @FrameworkID) AND (FrameworkReviews.Archived IS NOT NULL) AND (fc.IsDeleted=0)", + WHERE ((fc.FrameworkID = @FrameworkID) AND (FrameworkReviews.ID IS NULL) AND (fc.IsDeleted=0)) OR + ((fc.FrameworkID = @FrameworkID) AND (FrameworkReviews.Archived IS NOT NULL) AND (fc.IsDeleted=0)) + AND + (fc.ID Not in ( Select FrameworkCollaboratorID from FrameworkReviews where FrameworkID = @FrameworkID AND + FrameworkReviews.Archived IS NULL AND FrameworkCollaboratorID = fc.ID))", new { frameworkId } ); } @@ -2049,10 +2147,13 @@ FROM FrameworkReviews AS FR INNER JOIN { return connection.Query( @"SELECT FR.ID, FR.FrameworkID, FR.FrameworkCollaboratorID, FC.UserEmail, CAST(CASE WHEN FC.AdminID IS NULL THEN 0 ELSE 1 END AS bit) AS IsRegistered, FR.ReviewRequested, FR.ReviewComplete, FR.SignedOff, FR.FrameworkCommentID, FC1.Comments AS Comment, FR.SignOffRequired - FROM FrameworkReviews AS FR INNER JOIN - FrameworkCollaborators AS FC ON FR.FrameworkCollaboratorID = FC.ID LEFT OUTER JOIN - FrameworkComments AS FC1 ON FR.FrameworkCommentID = FC1.ID - WHERE FR.ID = @reviewId AND FR.FrameworkID = @frameworkId AND FC.AdminID = @adminId AND FR.Archived IS NULL AND IsDeleted = 0", + FROM FrameworkReviews AS FR INNER JOIN + FrameworkCollaborators AS FC ON FR.FrameworkCollaboratorID = FC.ID INNER JOIN + AdminAccounts AS aa ON aa.ID = FC.AdminID LEFT OUTER JOIN + FrameworkComments AS FC1 ON FR.FrameworkCommentID = FC1.ID + WHERE FR.ID = @reviewId AND FR.FrameworkID = @frameworkId AND + aa.UserID = (SELECT aa1.UserID FROM AdminAccounts aa1 WHERE aa1.ID = @adminId) AND + FR.Archived IS NULL AND IsDeleted = 0", new { frameworkId, adminId, reviewId } ).FirstOrDefault(); } @@ -2096,7 +2197,7 @@ public void SubmitFrameworkReview(int frameworkId, int reviewId, bool signedOff, AU1.Email AS OwnerEmail, FW.FrameworkName FROM FrameworkReviews AS FR - INNER JOIN FrameworkCollaborators AS FC ON FR.FrameworkCollaboratorID = FC.ID AND FWC.IsDeleted = 0 + INNER JOIN FrameworkCollaborators AS FC ON FR.FrameworkCollaboratorID = FC.ID AND FC.IsDeleted = 0 INNER JOIN AdminUsers AS AU ON FC.AdminID = AU.AdminID INNER JOIN Frameworks AS FW ON FR.FrameworkID = FW.ID INNER JOIN AdminUsers AS AU1 ON FW.OwnerAdminID = AU1.AdminID @@ -2407,7 +2508,7 @@ public IEnumerable GetBulkCompetenciesForFramework(int framework else { return connection.Query( - @"SELECT fc.ID, cg.Name AS CompetencyGroup, cg.Description AS GroupDescription, c.Name AS Competency, c.Description AS CompetencyDescription, c.AlwaysShowDescription, STUFF(( + @"SELECT fc.ID, ISNULL(cg.Name, '') AS CompetencyGroup, cg.Description AS GroupDescription, c.Name AS Competency, c.Description AS CompetencyDescription, c.AlwaysShowDescription, STUFF(( SELECT ', ' + f.FlagName FROM Flags AS f INNER JOIN CompetencyFlags AS cf ON f.ID = cf.FlagID @@ -2415,12 +2516,12 @@ FROM Flags AS f FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS FlagsCsv FROM Competencies AS c INNER JOIN - FrameworkCompetencies AS fc ON c.ID = fc.CompetencyID INNER JOIN - FrameworkCompetencyGroups AS fcg ON fc.FrameworkCompetencyGroupID = fcg.ID INNER JOIN + FrameworkCompetencies AS fc ON c.ID = fc.CompetencyID LEFT JOIN + FrameworkCompetencyGroups AS fcg ON fc.FrameworkCompetencyGroupID = fcg.ID LEFT JOIN CompetencyGroups AS cg ON fcg.CompetencyGroupID = cg.ID WHERE (fc.FrameworkID = @frameworkId) GROUP BY fc.ID, c.ID, cg.Name, cg.Description, c.Name, c.Description, c.AlwaysShowDescription, fcg.Ordering, fc.Ordering - ORDER BY fcg.Ordering, fc.Ordering", + ORDER BY COALESCE(fcg.Ordering,99999), fc.Ordering, fc.ID", new { frameworkId } ); } @@ -2430,12 +2531,38 @@ public List GetFrameworkCompetencyOrder(int frameworkId, List framewor { return connection.Query( @"SELECT fc.ID - FROM FrameworkCompetencies AS fc INNER JOIN + FROM FrameworkCompetencies AS fc LEFT JOIN FrameworkCompetencyGroups AS fcg ON fc.FrameworkCompetencyGroupID = fcg.ID WHERE (fc.FrameworkID = @frameworkId) AND (fc.ID IN @frameworkCompetencyIds) - ORDER BY fcg.Ordering, fc.Ordering", + ORDER BY COALESCE(fcg.Ordering,99999), fc.Ordering, fc.ID", new { frameworkId, frameworkCompetencyIds } ).ToList(); } + + public int GetFrameworkCompetencyGroupId(int frameworkId, int competencyGroupId) + { + return connection.Query( + @"SELECT MAX(ID) FROM FrameworkCompetencyGroups + WHERE FrameworkID = @frameworkId AND CompetencyGroupID = @competencyGroupId", + new { frameworkId, competencyGroupId } + ).Single(); + } + + public void UpdateFrameworkCompetencyFrameworkCompetencyGroup(int? competencyGroupId, int frameworkCompetencyGroupId, int adminId) + { + var numberOfAffectedRows = connection.Execute( + @"UPDATE FrameworkCompetencies + SET FrameworkCompetencyGroupId = @frameworkCompetencyGroupId, UpdatedByAdminID = @adminId + WHERE ID = @competencyGroupId AND FrameworkCompetencyGroupId <> @frameworkCompetencyGroupId", + new { frameworkCompetencyGroupId, competencyGroupId, adminId } + ); + if (numberOfAffectedRows < 1) + { + logger.LogWarning( + "Not updating framework competencies framework competency group id as db update failed. " + + $"frameworkCompetencyGroupId: {frameworkCompetencyGroupId}, competencyGroupId: {competencyGroupId}." + ); + } + } } } diff --git a/DigitalLearningSolutions.Data/DataServices/LoginDataService.cs b/DigitalLearningSolutions.Data/DataServices/LoginDataService.cs new file mode 100644 index 0000000000..109d5a9273 --- /dev/null +++ b/DigitalLearningSolutions.Data/DataServices/LoginDataService.cs @@ -0,0 +1,63 @@ +namespace DigitalLearningSolutions.Data.DataServices +{ + using Dapper; + using System.Data; + + public interface ILoginDataService + { + void UpdateLastAccessedForUsersTable(int Id); + + void UpdateLastAccessedForDelegatesAccountsTable(int Id); + + void UpdateLastAccessedForAdminAccountsTable(int Id); + } + + public class LoginDataService : ILoginDataService + { + private readonly IDbConnection connection; + + public LoginDataService(IDbConnection connection) + { + this.connection = connection; + } + + public void UpdateLastAccessedForUsersTable(int Id) + { + connection.Execute( + @"UPDATE Users SET + LastAccessed = GetUtcDate() + WHERE ID = @Id", + new + { + Id + } + ); + } + + public void UpdateLastAccessedForDelegatesAccountsTable(int Id) + { + connection.Execute( + @"UPDATE DelegateAccounts SET + LastAccessed = GetUtcDate() + WHERE ID = @Id", + new + { + Id + } + ); + } + + public void UpdateLastAccessedForAdminAccountsTable(int Id) + { + connection.Execute( + @"UPDATE AdminAccounts SET + LastAccessed = GetUtcDate() + WHERE ID = @Id", + new + { + Id + } + ); + } + } +} diff --git a/DigitalLearningSolutions.Data/DataServices/PlatformReportsDataService.cs b/DigitalLearningSolutions.Data/DataServices/PlatformReportsDataService.cs index b127b9b42d..8486fe3db2 100644 --- a/DigitalLearningSolutions.Data/DataServices/PlatformReportsDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/PlatformReportsDataService.cs @@ -1,10 +1,9 @@ namespace DigitalLearningSolutions.Data.DataServices { using Dapper; + using DigitalLearningSolutions.Data.Factories; using DigitalLearningSolutions.Data.Models.PlatformReports; - using DigitalLearningSolutions.Data.Models.SelfAssessments; using DigitalLearningSolutions.Data.Models.TrackingSystem; - using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Data; @@ -37,9 +36,9 @@ IEnumerable GetFilteredCourseActivity( ); DateTime? GetStartOfCourseActivity(); } - public class PlatformReportsDataService : IPlatformReportsDataService + public class PlatformReportsDataService(IReadOnlyDbConnectionFactory factory) : IPlatformReportsDataService { - private readonly IDbConnection connection; + private readonly IDbConnection connection = factory.CreateConnection(); private readonly string selectSelfAssessmentActivity = @"SELECT Cast(al.ActivityDate As Date) As ActivityDate, SUM(CAST(al.Enrolled AS Int)) AS Enrolled, SUM(CAST((al.Submitted | al.SignedOff) AS Int)) AS Completed FROM ReportSelfAssessmentActivityLog AS al WITH (NOLOCK) INNER JOIN Centres AS ce WITH (NOLOCK) ON al.CentreID = ce.CentreID INNER JOIN @@ -60,10 +59,6 @@ private string GetSelfAssessmentWhereClause(bool supervised) return supervised ? " (sa.SupervisorResultsReview = 1 OR SupervisorSelfAssessmentReview = 1)" : " (sa.SupervisorResultsReview = 0 AND SupervisorSelfAssessmentReview = 0)"; } - public PlatformReportsDataService(IDbConnection connection) - { - this.connection = connection; - } public PlatformUsageSummary GetPlatformUsageSummary() { return connection.QueryFirstOrDefault( diff --git a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/CandidateAssessmentsDataService.cs b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/CandidateAssessmentsDataService.cs index bb1dc7649f..aae3988660 100644 --- a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/CandidateAssessmentsDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/CandidateAssessmentsDataService.cs @@ -12,28 +12,31 @@ public IEnumerable GetSelfAssessmentsForCandidate(int del { return connection.Query( @"SELECT SelfAssessment.Id, - SelfAssessment.Name, - SelfAssessment.Description, - SelfAssessment.IncludesSignposting, - SelfAssessment.IncludeRequirementsFilters, - SelfAssessment. IsSupervisorResultsReviewed, - SelfAssessment.ReviewerCommentsLabel, - SelfAssessment. Vocabulary, - SelfAssessment. NumberOfCompetencies, - SelfAssessment.StartedDate, - SelfAssessment.LastAccessed, - SelfAssessment.CompleteByDate, - SelfAssessment.CandidateAssessmentId, - SelfAssessment.UserBookmark, - SelfAssessment.UnprocessedUpdates, - SelfAssessment.LaunchCount, - SelfAssessment. IsSelfAssessment, - SelfAssessment.SubmittedDate, - SelfAssessment. CentreName, - SelfAssessment.EnrolmentMethodId, - Signoff.SignedOff, - Signoff.Verified, - EnrolledByForename +' '+EnrolledBySurname AS EnrolledByFullName + SelfAssessment.Name, + SelfAssessment.Description, + SelfAssessment.IncludesSignposting, + SelfAssessment.IncludeRequirementsFilters, + SelfAssessment. IsSupervisorResultsReviewed, + SelfAssessment.ReviewerCommentsLabel, + SelfAssessment. Vocabulary, + SelfAssessment. NumberOfCompetencies, + SelfAssessment.StartedDate, + SelfAssessment.LastAccessed, + SelfAssessment.CompleteByDate, + SelfAssessment.CandidateAssessmentId, + SelfAssessment.UserBookmark, + SelfAssessment.UnprocessedUpdates, + SelfAssessment.LaunchCount, + SelfAssessment. IsSelfAssessment, + SelfAssessment.SubmittedDate, + SelfAssessment. CentreName, + SelfAssessment.EnrolmentMethodId, + SelfAssessment.RetirementDate, + SelfAssessment.EnrolmentCutoffDate, + SelfAssessment.RetirementReason, + Signoff.SignedOff, + Signoff.Verified, + EnrolledByForename +' '+EnrolledBySurname AS EnrolledByFullName FROM (SELECT CA.SelfAssessmentID AS Id, SA.Name, @@ -56,7 +59,10 @@ public IEnumerable GetSelfAssessmentsForCandidate(int del CR.CentreName AS CentreName, CA.EnrolmentMethodId, uEnrolledBy.FirstName AS EnrolledByForename, - uEnrolledBy.LastName AS EnrolledBySurname + uEnrolledBy.LastName AS EnrolledBySurname, + SA.RetirementDate, + SA.EnrolmentCutoffDate, + SA.RetirementReason FROM Centres AS CR INNER JOIN CandidateAssessments AS CA INNER JOIN SelfAssessments AS SA ON CA.SelfAssessmentID = SA.ID ON CR.CentreID = CA.CentreID INNER JOIN @@ -71,7 +77,7 @@ Competencies AS C RIGHT OUTER JOIN AND (ISNULL(@adminIdCategoryID, 0) = 0 OR sa.CategoryID = @adminIdCategoryId) GROUP BY CA.SelfAssessmentID, SA.Name, SA.Description, SA.IncludesSignposting, SA.SupervisorResultsReview, - SA.ReviewerCommentsLabel, SA.IncludeRequirementsFilters, + SA.ReviewerCommentsLabel, SA.IncludeRequirementsFilters, SA.RetirementDate,SA.EnrolmentCutoffDate,SA.RetirementReason, COALESCE(SA.Vocabulary, 'Capability'), CA.StartedDate, CA.LastAccessed, CA.CompleteByDate, CA.ID, CA.UserBookmark, CA.UnprocessedUpdates, CA.LaunchCount, CA.SubmittedDate, CR.CentreName,CA.EnrolmentMethodId, @@ -193,6 +199,7 @@ CandidateAssessments AS CA LEFT OUTER JOIN SA.LinearNavigation, SA.UseDescriptionExpanders, SA.ManageOptionalCompetenciesPrompt, + CAST(CASE WHEN CA.SelfAssessmentProcessAgreed IS NOT NULL THEN 1 ELSE 0 END AS BIT) AS SelfAssessmentProcessAgreed, CAST(CASE WHEN SA.SupervisorSelfAssessmentReview = 1 OR SA.SupervisorResultsReview = 1 THEN 1 ELSE 0 END AS BIT) AS IsSupervised, CASE WHEN (SELECT COUNT(*) FROM SelfAssessmentSupervisorRoles WHERE SelfAssessmentID = @selfAssessmentId AND AllowDelegateNomination = 1) > 0 @@ -241,7 +248,7 @@ GROUP BY CA.LaunchCount, CA.SubmittedDate, SA.LinearNavigation, SA.UseDescriptionExpanders, SA.ManageOptionalCompetenciesPrompt, SA.SupervisorSelfAssessmentReview, SA.SupervisorResultsReview, SA.ReviewerCommentsLabel,SA.EnforceRoleRequirementsForSignOff, SA.ManageSupervisorsDescription,CA.NonReportable, - U.FirstName , U.LastName,SA.MinimumOptionalCompetencies", + U.FirstName , U.LastName,SA.MinimumOptionalCompetencies, CA.SelfAssessmentProcessAgreed", new { delegateUserId, selfAssessmentId } ); } @@ -325,6 +332,22 @@ public void SetCompleteByDate(int selfAssessmentId, int delegateUserId, DateTime } } + public void MarkProgressAgreed(int selfAssessmentId, int delegateUserId) + { + var numberOfAffectedRows = connection.Execute( + @"UPDATE CandidateAssessments SET SelfAssessmentProcessAgreed = GETDATE() + WHERE SelfAssessmentID = @selfAssessmentId AND DelegateUserID = @delegateUserId", + new { selfAssessmentId, delegateUserId } + ); + if (numberOfAffectedRows < 1) + { + logger.LogWarning( + "SelfAssessmentProcessAgreed not set as db update failed. " + + $"Self assessment id: {selfAssessmentId}, Delegate User id: {delegateUserId}" + ); + } + } + public void SetUpdatedFlag(int selfAssessmentId, int delegateUserId, bool status) { var numberOfAffectedRows = connection.Execute( @@ -490,11 +513,13 @@ public IEnumerable GetAccessor(int selfAssessmentId, int delegateUserI { return connection.Query( @"SELECT CASE WHEN AccessorPRN IS NOT NULL THEN AccessorName+', '+AccessorPRN ELSE AccessorName END AS AccessorList,AccessorName,AccessorPRN - FROM (SELECT COALESCE(au.Forename + ' ' + au.Surname + (CASE WHEN au.Active = 1 THEN '' ELSE ' (Inactive)' END), sd.SupervisorEmail) AS AccessorName, + FROM (SELECT DISTINCT COALESCE(au.Forename + ' ' + au.Surname + (CASE WHEN au.Active = 1 THEN '' ELSE ' (Inactive)' END), sd.SupervisorEmail) AS AccessorName, u.ProfessionalRegistrationNumber AS AccessorPRN FROM SupervisorDelegates AS sd INNER JOIN CandidateAssessmentSupervisors AS cas ON sd.ID = cas.SupervisorDelegateId + INNER JOIN SelfAssessmentResultSupervisorVerifications AS srsv + ON cas.ID = srsv.CandidateAssessmentSupervisorID INNER JOIN CandidateAssessments AS ca ON cas.CandidateAssessmentID = ca.ID LEFT OUTER JOIN AdminUsers AS au @@ -504,7 +529,8 @@ LEFT OUTER JOIN SelfAssessmentSupervisorRoles AS sasr ON cas.SelfAssessmentSupervisorRoleID = sasr.ID INNER JOIN Users AS u ON U.PrimaryEmail = au.Email WHERE - (sd.Removed IS NULL) AND (cas.Removed IS NULL) AND (ca.DelegateUserID = @DelegateUserID) AND (ca.SelfAssessmentID = @selfAssessmentId)) Accessor + (ca.DelegateUserID = @DelegateUserID) AND (ca.SelfAssessmentID = @selfAssessmentId) + AND (srsv.Verified IS NOT NULL)) Accessor ORDER BY AccessorName, AccessorPRN DESC", new { selfAssessmentId, delegateUserID } ); diff --git a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs index ed3800215e..cdb71bb90d 100644 --- a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs @@ -1,128 +1,124 @@ -namespace DigitalLearningSolutions.Data.DataServices.SelfAssessmentDataService -{ - using Dapper; - using DigitalLearningSolutions.Data.Models.SelfAssessments.Export; - using Microsoft.Extensions.Logging; - using System.Collections.Generic; - using System.Data; - - public interface IDCSAReportDataService - { - IEnumerable GetDelegateCompletionStatusForCentre(int centreId); - IEnumerable GetOutcomeSummaryForCentre(int centreId); - } - public partial class DCSAReportDataService : IDCSAReportDataService - { - private readonly IDbConnection connection; - private readonly ILogger logger; - - public DCSAReportDataService(IDbConnection connection, ILogger logger) - { - this.connection = connection; - this.logger = logger; - } - - public IEnumerable GetDelegateCompletionStatusForCentre(int centreId) - { - return connection.Query( - @"SELECT DATEPART(month, ca.StartedDate) AS EnrolledMonth, DATEPART(yyyy, ca.StartedDate) AS EnrolledYear, u.FirstName, u.LastName, COALESCE (ucd.Email, u.PrimaryEmail) AS Email, da.Answer1 AS CentreField1, da.Answer2 AS CentreField2, da.Answer3 AS CentreField3, - CASE WHEN (ca.SubmittedDate IS NOT NULL) THEN 'Submitted' WHEN (ca.UserBookmark LIKE N'/LearningPortal/SelfAssessment/1/Review' AND ca.SubmittedDate IS NULL) THEN 'Reviewing' ELSE 'Incomplete' END AS Status - FROM CandidateAssessments AS ca INNER JOIN - DelegateAccounts AS da ON ca.DelegateUserID = da.UserID INNER JOIN - Users AS u ON da.UserID = u.ID LEFT OUTER JOIN - UserCentreDetails AS ucd ON da.CentreID = ucd.CentreID AND u.ID = ucd.UserID - WHERE (ca.SelfAssessmentID = 1) AND (da.CentreID = @centreId) AND (u.Active = 1) AND (da.Active = 1)", - new { centreId } - ); - } - - public IEnumerable GetOutcomeSummaryForCentre(int centreId) - { - return connection.Query( - @"SELECT DATEPART(month, ca.StartedDate) AS EnrolledMonth, DATEPART(yyyy, ca.StartedDate) AS EnrolledYear, jg.JobGroupName AS JobGroup, da.Answer1 AS CentreField1, da.Answer2 AS CentreField2, da.Answer3 AS CentreField3, CASE WHEN (ca.SubmittedDate IS NOT NULL) - THEN 'Submitted' WHEN (ca.UserBookmark LIKE N'/LearningPortal/SelfAssessment/1/Review' AND ca.SubmittedDate IS NULL) THEN 'Reviewing' ELSE 'Incomplete' END AS Status, - (SELECT COUNT(*) AS LearningLaunched - FROM CandidateAssessmentLearningLogItems AS calli INNER JOIN - LearningLogItems AS lli ON calli.LearningLogItemID = lli.LearningLogItemID - WHERE (NOT (lli.LearningResourceReferenceID IS NULL)) AND (calli.CandidateAssessmentID = ca.ID)) + - (SELECT COUNT(*) AS FilteredLearning - FROM FilteredLearningActivity AS fla - WHERE (CandidateId = da.ID)) AS LearningCompleted, - (SELECT COUNT(*) AS LearningLaunched - FROM CandidateAssessmentLearningLogItems AS calli INNER JOIN - LearningLogItems AS lli ON calli.LearningLogItemID = lli.LearningLogItemID - WHERE (NOT (lli.LearningResourceReferenceID IS NULL)) AND (calli.CandidateAssessmentID = ca.ID) AND (NOT (lli.CompletedDate IS NULL))) + - (SELECT COUNT(*) AS FilteredCompleted - FROM FilteredLearningActivity AS fla - WHERE (CandidateId = da.ID) AND (CompletedDate IS NOT NULL)) AS LearningCompleted, - (SELECT AVG(sar.Result) AS AvgConfidence - FROM SelfAssessmentResults AS sar INNER JOIN - Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN - SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID - WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 1)) AS DataInformationAndContentConfidence, - (SELECT AVG(sar.Result) AS AvgConfidence - FROM SelfAssessmentResults AS sar INNER JOIN - Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN - SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID - WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 1)) AS DataInformationAndContentRelevance, - (SELECT AVG(sar.Result) AS AvgConfidence - FROM SelfAssessmentResults AS sar INNER JOIN - Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN - SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID - WHERE (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 2)) AS TeachingLearningAndSelfDevelopmentConfidence, - (SELECT AVG(sar.Result) AS AvgConfidence - FROM SelfAssessmentResults AS sar INNER JOIN - Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN - SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID - WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 2)) AS TeachingLearningAndSelfDevelopmentRelevance, - (SELECT AVG(sar.Result) AS AvgConfidence - FROM SelfAssessmentResults AS sar INNER JOIN - Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN - SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID - WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 3)) AS CommunicationCollaborationAndParticipationConfidence, - (SELECT AVG(sar.Result) AS AvgConfidence - FROM SelfAssessmentResults AS sar INNER JOIN - Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN - SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID - WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 3)) AS CommunicationCollaborationAndParticipationRelevance, - (SELECT AVG(sar.Result) AS AvgConfidence - FROM SelfAssessmentResults AS sar INNER JOIN - Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN - SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID - WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 4)) AS TechnicalProficiencyConfidence, - (SELECT AVG(sar.Result) AS AvgConfidence - FROM SelfAssessmentResults AS sar INNER JOIN - Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN - SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID - WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 4)) AS TechnicalProficiencyRelevance, - (SELECT AVG(sar.Result) AS AvgConfidence - FROM SelfAssessmentResults AS sar INNER JOIN - Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN - SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID - WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 5)) AS CreationInnovationAndResearchConfidence, - (SELECT AVG(sar.Result) AS AvgConfidence - FROM SelfAssessmentResults AS sar INNER JOIN - Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN - SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID - WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 5)) AS CreationInnovationAndResearchRelevance, - (SELECT AVG(sar.Result) AS AvgConfidence - FROM SelfAssessmentResults AS sar INNER JOIN - Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN - SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID - WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 6)) AS DigitalIdentityWellbeingSafetyAndSecurityConfidence, - (SELECT AVG(sar.Result) AS AvgConfidence - FROM SelfAssessmentResults AS sar INNER JOIN - Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN - SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID - WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 6)) AS DigitalIdentityWellbeingSafetyAndSecurityRelevance - FROM CandidateAssessments AS ca INNER JOIN - DelegateAccounts AS da ON ca.DelegateUserID = da.UserID INNER JOIN - Users AS u ON da.UserID = u.ID INNER JOIN - JobGroups AS jg ON u.JobGroupID = jg.JobGroupID - WHERE (ca.SelfAssessmentID = 1) AND (da.CentreID = @centreId) AND (u.Active = 1) AND (da.Active = 1)", - new { centreId } - ); - } - - } -} +namespace DigitalLearningSolutions.Data.DataServices.SelfAssessmentDataService +{ + using Dapper; + using DigitalLearningSolutions.Data.Factories; + using DigitalLearningSolutions.Data.Models.SelfAssessments.Export; + using Microsoft.Extensions.Logging; + using System.Collections.Generic; + using System.Data; + + public interface IDCSAReportDataService + { + IEnumerable GetDelegateCompletionStatusForCentre(int centreId); + IEnumerable GetOutcomeSummaryForCentre(int centreId); + } + public partial class DCSAReportDataService(IReadOnlyDbConnectionFactory factory, ILogger logger) : IDCSAReportDataService + { + private readonly IDbConnection connection = factory.CreateConnection(); + private readonly ILogger logger = logger; + + public IEnumerable GetDelegateCompletionStatusForCentre(int centreId) + { + return connection.Query( + @"SELECT DATEPART(month, ca.StartedDate) AS EnrolledMonth, DATEPART(yyyy, ca.StartedDate) AS EnrolledYear, u.FirstName, u.LastName, COALESCE (ucd.Email, u.PrimaryEmail) AS Email, da.Answer1 AS RegistrationAnswer1, da.Answer2 AS RegistrationAnswer2, da.Answer3 AS RegistrationAnswer3, + da.Answer4 AS RegistrationAnswer4, da.Answer5 AS RegistrationAnswer5, da.Answer6 AS RegistrationAnswer6, CASE WHEN (ca.SubmittedDate IS NOT NULL) THEN 'Submitted' WHEN (ca.UserBookmark LIKE N'/LearningPortal/SelfAssessment/1/Review' AND ca.SubmittedDate IS NULL) THEN 'Reviewing' ELSE 'Incomplete' END AS Status + FROM CandidateAssessments AS ca INNER JOIN + DelegateAccounts AS da ON ca.DelegateUserID = da.UserID INNER JOIN + Users AS u ON da.UserID = u.ID LEFT OUTER JOIN + UserCentreDetails AS ucd ON da.CentreID = ucd.CentreID AND u.ID = ucd.UserID + WHERE (ca.SelfAssessmentID = 1) AND (da.CentreID = @centreId) AND (u.Active = 1) AND (da.Active = 1)", + new { centreId } + ); + } + + public IEnumerable GetOutcomeSummaryForCentre(int centreId) + { + return connection.Query( + @"SELECT DATEPART(month, ca.StartedDate) AS EnrolledMonth, DATEPART(yyyy, ca.StartedDate) AS EnrolledYear, jg.JobGroupName AS JobGroup, da.Answer1 AS RegistrationAnswer1, da.Answer2 AS RegistrationAnswer2, da.Answer3 AS RegistrationAnswer3, + da.Answer4 AS RegistrationAnswer4, da.Answer5 AS RegistrationAnswer5, da.Answer6 AS RegistrationAnswer6, CASE WHEN (ca.SubmittedDate IS NOT NULL) + THEN 'Submitted' WHEN (ca.UserBookmark LIKE N'/LearningPortal/SelfAssessment/1/Review' AND ca.SubmittedDate IS NULL) THEN 'Reviewing' ELSE 'Incomplete' END AS Status, + (SELECT COUNT(*) AS LearningLaunched + FROM CandidateAssessmentLearningLogItems AS calli INNER JOIN + LearningLogItems AS lli ON calli.LearningLogItemID = lli.LearningLogItemID + WHERE (NOT (lli.LearningResourceReferenceID IS NULL)) AND (calli.CandidateAssessmentID = ca.ID)) + + (SELECT COUNT(*) AS FilteredLearning + FROM FilteredLearningActivity AS fla + WHERE (CandidateId = da.ID)) AS LearningLaunched, + (SELECT COUNT(*) AS LearningLaunched + FROM CandidateAssessmentLearningLogItems AS calli INNER JOIN + LearningLogItems AS lli ON calli.LearningLogItemID = lli.LearningLogItemID + WHERE (NOT (lli.LearningResourceReferenceID IS NULL)) AND (calli.CandidateAssessmentID = ca.ID) AND (NOT (lli.CompletedDate IS NULL))) + + (SELECT COUNT(*) AS FilteredCompleted + FROM FilteredLearningActivity AS fla + WHERE (CandidateId = da.ID) AND (CompletedDate IS NOT NULL)) AS LearningCompleted, + (SELECT AVG(sar.Result) AS AvgConfidence + FROM SelfAssessmentResults AS sar INNER JOIN + Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN + SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID + WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 1)) AS DataInformationAndContentConfidence, + (SELECT AVG(sar.Result) AS AvgConfidence + FROM SelfAssessmentResults AS sar INNER JOIN + Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN + SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID + WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 1)) AS DataInformationAndContentRelevance, + (SELECT AVG(sar.Result) AS AvgConfidence + FROM SelfAssessmentResults AS sar INNER JOIN + Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN + SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID + WHERE (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 2)) AS TeachingLearningAndSelfDevelopmentConfidence, + (SELECT AVG(sar.Result) AS AvgConfidence + FROM SelfAssessmentResults AS sar INNER JOIN + Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN + SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID + WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 2)) AS TeachingLearningAndSelfDevelopmentRelevance, + (SELECT AVG(sar.Result) AS AvgConfidence + FROM SelfAssessmentResults AS sar INNER JOIN + Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN + SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID + WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 3)) AS CommunicationCollaborationAndParticipationConfidence, + (SELECT AVG(sar.Result) AS AvgConfidence + FROM SelfAssessmentResults AS sar INNER JOIN + Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN + SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID + WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 3)) AS CommunicationCollaborationAndParticipationRelevance, + (SELECT AVG(sar.Result) AS AvgConfidence + FROM SelfAssessmentResults AS sar INNER JOIN + Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN + SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID + WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 4)) AS TechnicalProficiencyConfidence, + (SELECT AVG(sar.Result) AS AvgConfidence + FROM SelfAssessmentResults AS sar INNER JOIN + Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN + SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID + WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 4)) AS TechnicalProficiencyRelevance, + (SELECT AVG(sar.Result) AS AvgConfidence + FROM SelfAssessmentResults AS sar INNER JOIN + Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN + SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID + WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 5)) AS CreationInnovationAndResearchConfidence, + (SELECT AVG(sar.Result) AS AvgConfidence + FROM SelfAssessmentResults AS sar INNER JOIN + Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN + SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID + WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 5)) AS CreationInnovationAndResearchRelevance, + (SELECT AVG(sar.Result) AS AvgConfidence + FROM SelfAssessmentResults AS sar INNER JOIN + Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN + SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID + WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 6)) AS DigitalIdentityWellbeingSafetyAndSecurityConfidence, + (SELECT AVG(sar.Result) AS AvgConfidence + FROM SelfAssessmentResults AS sar INNER JOIN + Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN + SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID + WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 6)) AS DigitalIdentityWellbeingSafetyAndSecurityRelevance + FROM CandidateAssessments AS ca INNER JOIN + DelegateAccounts AS da ON ca.DelegateUserID = da.UserID INNER JOIN + Users AS u ON da.UserID = u.ID INNER JOIN + JobGroups AS jg ON u.JobGroupID = jg.JobGroupID + WHERE (ca.SelfAssessmentID = 1) AND (da.CentreID = @centreId) AND (u.Active = 1) AND (da.Active = 1)", + new { centreId } + ); + } + + } +} diff --git a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAssessmentDataService.cs b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAssessmentDataService.cs index 324a1050a7..ad32f35ba2 100644 --- a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAssessmentDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAssessmentDataService.cs @@ -16,6 +16,8 @@ public interface ISelfAssessmentDataService { //Self Assessments string? GetSelfAssessmentNameById(int selfAssessmentId); + SelfAssessment? GetSelfAssessmentById(int selfAssessmentId); + SelfAssessment GetSelfAssessmentRetirementDateById(int selfAssessmentId); // CompetencyDataService IEnumerable GetCompetencyIdsForSelfAssessment(int selfAssessmentId); @@ -89,6 +91,7 @@ int competencyId void SetBookmark(int selfAssessmentId, int delegateUserId, string bookmark); + void MarkProgressAgreed(int selfAssessmentId, int delegateUserId); IEnumerable GetCandidateAssessments(int delegateUserId, int selfAssessmentId); // SelfAssessmentSupervisorDataService @@ -204,6 +207,67 @@ FROM SelfAssessments return name; } + public SelfAssessment? GetSelfAssessmentById(int selfAssessmentId) + { + return connection.Query( + @"SELECT [ID] + ,[Name] + ,[Description] + ,[IncludesSignposting] + ,[BrandID] + ,[CreatedDate] + ,[CreatedByCentreID] + ,[CreatedByAdminID] + ,[ArchivedDate] + ,[ArchivedByAdminID] + ,[IncludeDevelopment] + ,[ParentSelfAssessmentID] + ,[NRPProfessionalGroupID] + ,[NRPSubGroupID] + ,[NRPRoleID] + ,[PublishStatusID] + ,[UpdatedByAdminID] + ,[National] + ,[Public] + ,[Archived] + ,[LastEdit] + ,[SupervisorSelfAssessmentReview] + ,[SupervisorResultsReview] + ,[RAGResults] + ,[LinearNavigation] + ,[CategoryID] + ,[UseDescriptionExpanders] + ,[ManageOptionalCompetenciesPrompt] + ,[Vocabulary] + ,[SignOffRequestorStatement] + ,[SignOffSupervisorStatement] + ,[QuestionLabel] + ,[DescriptionLabel] + ,[EnforceRoleRequirementsForSignOff] + ,[ReviewerCommentsLabel] + ,[ManageSupervisorsDescription] + ,[IncludeRequirementsFilters] + ,[MinimumOptionalCompetencies] + ,[RetirementDate] + ,[EnrolmentCutoffDate] + ,[RetirementReason] + FROM SelfAssessments + WHERE ID = @selfAssessmentId", + new { selfAssessmentId } + ).SingleOrDefault(); + } + public SelfAssessment GetSelfAssessmentRetirementDateById(int selfAssessmentId) + { + var date = connection.QueryFirstOrDefault( + @"SELECT Id,Name,[RetirementDate] + FROM SelfAssessments + WHERE ID = @selfAssessmentId" + , + new { selfAssessmentId } + ); + return date; + } + public (IEnumerable, int) GetSelfAssessmentDelegates(string searchString, int offSet, int itemsPerPage, string sortBy, string sortDirection, int? selfAssessmentId, int centreId, bool? isDelegateActive, bool? removed, bool? submitted, bool? signedOff) { diff --git a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAsssessmentReportDataService.cs b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAsssessmentReportDataService.cs index 50168d4e64..3a593d515c 100644 --- a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAsssessmentReportDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAsssessmentReportDataService.cs @@ -1,135 +1,49 @@ -namespace DigitalLearningSolutions.Data.DataServices.SelfAssessmentDataService -{ - using Dapper; - using DigitalLearningSolutions.Data.Models.SelfAssessments.Export; - using DigitalLearningSolutions.Data.Models.SelfAssessments; - using Microsoft.Extensions.Logging; - using System.Collections.Generic; - using System.Data; - - public interface ISelfAssessmentReportDataService - { - IEnumerable GetSelfAssessmentsForReportList(int centreId, int? categoryId); - IEnumerable GetSelfAssessmentReportDataForCentre(int centreId, int selfAssessmentId); - } - public partial class SelfAssessmentReportDataService : ISelfAssessmentReportDataService - { - private readonly IDbConnection connection; - private readonly ILogger logger; - - public SelfAssessmentReportDataService(IDbConnection connection, ILogger logger) - { - this.connection = connection; - this.logger = logger; - } - - public IEnumerable GetSelfAssessmentsForReportList(int centreId, int? categoryId) - { - return connection.Query( - @"SELECT csa.SelfAssessmentID AS Id, sa.Name, - (SELECT COUNT (DISTINCT da.UserID) AS Learners - FROM CandidateAssessments AS ca1 INNER JOIN - DelegateAccounts AS da ON ca1.DelegateUserID = da.UserID - WHERE (da.CentreID = @centreId) AND (ca1.RemovedDate IS NULL) AND (ca1.SelfAssessmentID = csa.SelfAssessmentID) AND ca1.NonReportable=0) AS LearnerCount - FROM CentreSelfAssessments AS csa INNER JOIN - SelfAssessments AS sa ON csa.SelfAssessmentID = sa.ID - WHERE (csa.CentreID = @centreId) AND (sa.CategoryID = @categoryId) AND (sa.SupervisorResultsReview = 1) AND (sa.ArchivedDate IS NULL) OR - (csa.CentreID = @centreId) AND (sa.CategoryID = @categoryId) AND (sa.ArchivedDate IS NULL) AND (sa.SupervisorSelfAssessmentReview = 1) OR - (csa.CentreID = @centreId) AND (sa.SupervisorResultsReview = 1) AND (sa.ArchivedDate IS NULL) AND (@categoryId = 0) OR - (csa.CentreID = @centreId) AND (sa.ArchivedDate IS NULL) AND (sa.SupervisorSelfAssessmentReview = 1) AND (@categoryId = 0) - ORDER BY sa.Name", - new { centreId, categoryId = categoryId ??= 0 } - ); - } - - public IEnumerable GetSelfAssessmentReportDataForCentre(int centreId, int selfAssessmentId) - { - return connection.Query( - @"WITH LatestAssessmentResults AS - ( - SELECT s.DelegateUserID - , CASE WHEN COALESCE (rr.LevelRAG, 0) = 3 THEN s.ID ELSE NULL END AS SelfAssessed - , CASE WHEN sv.Verified IS NOT NULL AND sv.SignedOff = 1 AND COALESCE (rr.LevelRAG, 0) = 3 THEN s.ID ELSE NULL END AS Confirmed - , CASE WHEN sas.Optional = 1 THEN s.CompetencyID ELSE NULL END AS Optional - FROM SelfAssessmentResults AS s LEFT OUTER JOIN - SelfAssessmentStructure AS sas ON sas.SelfAssessmentID = @selfAssessmentId AND s.CompetencyID = sas.CompetencyID LEFT OUTER JOIN - SelfAssessmentResultSupervisorVerifications AS sv ON s.ID = sv.SelfAssessmentResultId AND sv.Superceded = 0 LEFT OUTER JOIN - CompetencyAssessmentQuestionRoleRequirements AS rr ON s.CompetencyID = rr.CompetencyID AND s.AssessmentQuestionID = rr.AssessmentQuestionID AND sas.SelfAssessmentID = rr.SelfAssessmentID AND s.Result = rr.LevelValue - WHERE (sas.SelfAssessmentID = @selfAssessmentId) - ) - SELECT - sa.Name AS SelfAssessment - , u.LastName + ', ' + u.FirstName AS Learner - , da.Active AS LearnerActive - , u.ProfessionalRegistrationNumber AS PRN - , jg.JobGroupName AS JobGroup - , CASE WHEN c.CustomField1PromptID = 10 THEN da.Answer1 WHEN c.CustomField2PromptID = 10 THEN da.Answer2 WHEN c.CustomField3PromptID = 10 THEN da.Answer3 WHEN c.CustomField4PromptID = 10 THEN da.Answer4 WHEN c.CustomField5PromptID = 10 THEN da.Answer5 WHEN c.CustomField6PromptID = 10 THEN da.Answer6 ELSE '' END AS 'ProgrammeCourse' - , CASE WHEN c.CustomField1PromptID = 4 THEN da.Answer1 WHEN c.CustomField2PromptID = 4 THEN da.Answer2 WHEN c.CustomField3PromptID = 4 THEN da.Answer3 WHEN c.CustomField4PromptID = 4 THEN da.Answer4 WHEN c.CustomField5PromptID = 4 THEN da.Answer5 WHEN c.CustomField6PromptID = 4 THEN da.Answer6 ELSE '' END AS 'Organisation' - , CASE WHEN c.CustomField1PromptID = 1 THEN da.Answer1 WHEN c.CustomField2PromptID = 1 THEN da.Answer2 WHEN c.CustomField3PromptID = 1 THEN da.Answer3 WHEN c.CustomField4PromptID = 1 THEN da.Answer4 WHEN c.CustomField5PromptID = 1 THEN da.Answer5 WHEN c.CustomField6PromptID = 1 THEN da.Answer6 ELSE '' END AS 'DepartmentTeam' - , dbo.GetOtherCentresForSelfAssessment(da.UserID, @SelfAssessmentID, c.CentreID) AS OtherCentres - , CASE - WHEN aa.ID IS NULL THEN 'Learner' - WHEN aa.IsCentreManager = 1 THEN 'Centre Manager' - WHEN aa.IsCentreAdmin = 1 AND aa.IsCentreManager = 0 THEN 'Centre Admin' - WHEN aa.IsSupervisor = 1 THEN 'Supervisor' - WHEN aa.IsNominatedSupervisor = 1 THEN 'Nominated supervisor' - END AS DLSRole - , da.DateRegistered AS Registered - , ca.StartedDate AS Started - , ca.LastAccessed - , COALESCE(COUNT(DISTINCT LAR.Optional), NULL) AS [OptionalProficienciesAssessed] - , COALESCE(COUNT(DISTINCT LAR.SelfAssessed), NULL) AS [SelfAssessedAchieved] - , COALESCE(COUNT(DISTINCT LAR.Confirmed), NULL) AS [ConfirmedResults] - , max(casv.Requested) AS SignOffRequested - , max(1*casv.SignedOff) AS SignOffAchieved - , min(casv.Verified) AS ReviewedDate - FROM - CandidateAssessments AS ca INNER JOIN - DelegateAccounts AS da ON ca.DelegateUserID = da.UserID and da.CentreID = @centreId INNER JOIN - Users as u ON u.ID = da.UserID INNER JOIN - SelfAssessments AS sa INNER JOIN - CentreSelfAssessments AS csa ON sa.ID = csa.SelfAssessmentID INNER JOIN - Centres AS c ON csa.CentreID = c.CentreID ON da.CentreID = c.CentreID AND ca.SelfAssessmentID = sa.ID INNER JOIN - JobGroups AS jg ON u.JobGroupID = jg.JobGroupID LEFT OUTER JOIN - AdminAccounts AS aa ON da.UserID = aa.UserID AND aa.CentreID = da.CentreID AND aa.Active = 1 LEFT OUTER JOIN - CandidateAssessmentSupervisors AS cas ON ca.ID = cas.CandidateAssessmentID left JOIN - CandidateAssessmentSupervisorVerifications AS casv ON casv.CandidateAssessmentSupervisorID = cas.ID LEFT JOIN - SupervisorDelegates AS sd ON cas.SupervisorDelegateId = sd.ID - LEFT OUTER JOIN LatestAssessmentResults AS LAR ON LAR.DelegateUserID = ca.DelegateUserID - WHERE - (sa.ID = @SelfAssessmentID) AND (sa.ArchivedDate IS NULL) AND (c.Active = 1) AND (ca.RemovedDate IS NULL AND ca.NonReportable = 0) - Group by sa.Name - , u.LastName + ', ' + u.FirstName - , da.Active - , u.ProfessionalRegistrationNumber - , c.CustomField1PromptID - , c.CustomField2PromptID - , c.CustomField3PromptID - , c.CustomField4PromptID - , c.CustomField5PromptID - , c.CustomField6PromptID - , c.CentreID - , jg.JobGroupName - , da.ID - , da.Answer1 - , da.Answer2 - , da.Answer3 - , da.Answer4 - , da.Answer5 - , da.Answer6 - , da.DateRegistered - , da.UserID - , aa.ID - , aa.IsCentreManager - , aa.IsCentreAdmin - , aa.IsSupervisor - , aa.IsNominatedSupervisor - , ca.StartedDate - , ca.LastAccessed - ORDER BY - SelfAssessment, u.LastName + ', ' + u.FirstName", - new { centreId, selfAssessmentId } - ); - } - } -} +namespace DigitalLearningSolutions.Data.DataServices.SelfAssessmentDataService +{ + using Dapper; + using DigitalLearningSolutions.Data.Models.SelfAssessments.Export; + using DigitalLearningSolutions.Data.Models.SelfAssessments; + using Microsoft.Extensions.Logging; + using System.Collections.Generic; + using System.Data; + using DigitalLearningSolutions.Data.Factories; + + public interface ISelfAssessmentReportDataService + { + IEnumerable GetSelfAssessmentsForReportList(int centreId, int? categoryId); + IEnumerable GetSelfAssessmentReportDataForCentre(int centreId, int selfAssessmentId); + } + public partial class SelfAssessmentReportDataService(IReadOnlyDbConnectionFactory factory, ILogger logger) : ISelfAssessmentReportDataService + { + private readonly IDbConnection connection = factory.CreateConnection(); + private readonly ILogger logger = logger; + + public IEnumerable GetSelfAssessmentsForReportList(int centreId, int? categoryId) + { + return connection.Query( + @"SELECT csa.SelfAssessmentID AS Id, sa.Name, + (SELECT COUNT (DISTINCT da.UserID) AS Learners + FROM CandidateAssessments AS ca1 INNER JOIN + DelegateAccounts AS da ON ca1.DelegateUserID = da.UserID + WHERE (da.CentreID = @centreId) AND (ca1.RemovedDate IS NULL) AND (ca1.SelfAssessmentID = csa.SelfAssessmentID) AND ca1.NonReportable=0) AS LearnerCount + FROM CentreSelfAssessments AS csa INNER JOIN + SelfAssessments AS sa ON csa.SelfAssessmentID = sa.ID + WHERE (csa.CentreID = @centreId) AND (sa.CategoryID = @categoryId) AND (sa.SupervisorResultsReview = 1) AND (sa.ArchivedDate IS NULL) OR + (csa.CentreID = @centreId) AND (sa.CategoryID = @categoryId) AND (sa.ArchivedDate IS NULL) AND (sa.SupervisorSelfAssessmentReview = 1) OR + (csa.CentreID = @centreId) AND (sa.SupervisorResultsReview = 1) AND (sa.ArchivedDate IS NULL) AND (@categoryId = 0) OR + (csa.CentreID = @centreId) AND (sa.ArchivedDate IS NULL) AND (sa.SupervisorSelfAssessmentReview = 1) AND (@categoryId = 0) + ORDER BY sa.Name", + new { centreId, categoryId = categoryId ??= 0 } + ); + } + + public IEnumerable GetSelfAssessmentReportDataForCentre(int centreId, int selfAssessmentId) + { + return connection.Query("usp_GetSelfAssessmentReport", + new { selfAssessmentId, centreId }, + commandType: CommandType.StoredProcedure, + commandTimeout: 150 + ); + } + } +} diff --git a/DigitalLearningSolutions.Data/DataServices/SupervisorDataService.cs b/DigitalLearningSolutions.Data/DataServices/SupervisorDataService.cs index 3409396ab1..6dbc94777b 100644 --- a/DigitalLearningSolutions.Data/DataServices/SupervisorDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/SupervisorDataService.cs @@ -547,6 +547,13 @@ public bool RemoveSupervisorDelegateById(int supervisorDelegateId, int delegateU WHERE cas.SupervisorDelegateId = @supervisorDelegateId AND cas.Removed IS NULL AND sarsv.Verified IS NULL", new { supervisorDelegateId } ); + connection.Execute( + @"DELETE FROM casv FROM CandidateAssessmentSupervisorVerifications casv INNER JOIN + CandidateAssessmentSupervisors cas ON casv.CandidateAssessmentSupervisorID = cas.ID + WHERE cas.SupervisorDelegateId = @supervisorDelegateId + AND casv.Verified IS NULL AND casv.SignedOff = 0", new { supervisorDelegateId } + ); + var numberOfAffectedRows = connection.Execute( @"UPDATE SupervisorDelegates SET Removed = getUTCDate() WHERE ID = @supervisorDelegateId AND Removed IS NULL AND (DelegateUserID = @delegateUserId OR SupervisorAdminID = @adminId)", @@ -689,7 +696,7 @@ FROM SelfAssessmentResults AS sar2 AdminAccounts AS aa ON sd.SupervisorAdminID = aa.ID WHERE (sd.SupervisorAdminID = @adminId) AND (cas.Removed IS NULL) AND (sasv.Verified IS NULL) AND (sd.Removed IS NULL) AND (aa.CategoryID is null or sa.CategoryID = aa.CategoryID) - GROUP BY sa.ID, ca.ID, sd.ID, u.FirstName, u.LastName, sa.Name,cast(sasv.Requested as date)", new { adminId } + GROUP BY sa.ID, ca.ID, sd.ID, u.FirstName, u.LastName, sa.Name", new { adminId } ); } @@ -789,7 +796,8 @@ FROM CandidateAssessments AS CA WHERE (DelegateUserID = @delegateUserId) AND (RemovedDate IS NULL) AND (CompletedDate IS NULL))) AND ((rp.SupervisorSelfAssessmentReview = 1) OR (rp.SupervisorResultsReview = 1)) - AND (ISNULL(@categoryId, 0) = 0 OR rp.CategoryID = @categoryId)", new { delegateUserId, centreId, categoryId } + AND (ISNULL(@categoryId, 0) = 0 OR rp.CategoryID = @categoryId) AND + ((CAST(rp.RetirementDate AS DATE) >= CAST(GETUTCDATE() AS DATE)) OR rp.RetirementDate IS NULL)", new { delegateUserId, centreId, categoryId } ); } diff --git a/DigitalLearningSolutions.Data/DataServices/UserDataService/AdminUserDataService.cs b/DigitalLearningSolutions.Data/DataServices/UserDataService/AdminUserDataService.cs index d93094c97b..9f293ad394 100644 --- a/DigitalLearningSolutions.Data/DataServices/UserDataService/AdminUserDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/UserDataService/AdminUserDataService.cs @@ -105,6 +105,7 @@ FROM AdminAccounts AS aa aa.IsSupervisor, aa.IsTrainer, aa.CategoryID, + aa.LastAccessed, CASE WHEN aa.CategoryID IS NULL THEN 'All' ELSE cc.CategoryName @@ -369,7 +370,7 @@ public IEnumerable GetAdminAccountsByUserId(int userId) } string BaseSelectQuery = $@"SELECT aa.ID, aa.UserID, aa.CentreID, aa.Active, aa.IsCentreAdmin, aa.IsReportsViewer, aa.IsSuperAdmin, aa.IsCentreManager, - aa.IsContentManager, aa.IsContentCreator, aa.IsSupervisor, aa.IsTrainer, aa.CategoryID, aa.IsFrameworkDeveloper, aa.IsFrameworkContributor,aa.ImportOnly, + aa.LastAccessed, aa.IsContentManager, aa.IsContentCreator, aa.IsSupervisor, aa.IsTrainer, aa.CategoryID, aa.IsFrameworkDeveloper, aa.IsFrameworkContributor,aa.ImportOnly, aa.IsWorkforceManager, aa.IsWorkforceContributor, aa.IsLocalWorkforceManager, aa.IsNominatedSupervisor, u.ID, u.PrimaryEmail, u.FirstName, u.LastName, u.Active, u.FailedLoginCount, c.CentreID, c.CentreName, diff --git a/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserCardDataService.cs b/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserCardDataService.cs index 8e2a424ed5..f2068623a9 100644 --- a/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserCardDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserCardDataService.cs @@ -87,6 +87,7 @@ FROM DelegateAccounts AS da c.CentreName, da.CentreID, da.DateRegistered, + da.LastAccessed, da.RegistrationConfirmationHash, c.Active AS CentreActive, COALESCE(ucd.Email, u.PrimaryEmail) AS EmailAddress, @@ -124,6 +125,7 @@ FROM AdminAccounts aa c.CentreName, da.CentreID, da.DateRegistered, + da.LastAccessed, da.RegistrationConfirmationHash, c.Active AS CentreActive, COALESCE(ucd.Email, u.PrimaryEmail) AS EmailAddress, @@ -350,6 +352,8 @@ public List GetDelegateUserCardsForExportByCentreId(String sea if (sortBy == "SearchableName") orderBy = " ORDER BY LTRIM(LastName) " + sortDirection + ", LTRIM(FirstName) "; + else if(sortBy == "LastAccessed") + orderBy = " ORDER BY LastAccessed " + sortDirection; else orderBy = " ORDER BY DateRegistered " + sortDirection; diff --git a/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserDataService.cs b/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserDataService.cs index ca48e7e67b..dd583caffa 100644 --- a/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserDataService.cs @@ -111,6 +111,7 @@ FROM DelegateAccounts AS da ce.CentreName, ce.Active AS CentreActive, da.DateRegistered, + da.LastAccessed, da.CandidateNumber, da.Answer1, da.Answer2, diff --git a/DigitalLearningSolutions.Data/DataServices/UserDataService/UserDataService.cs b/DigitalLearningSolutions.Data/DataServices/UserDataService/UserDataService.cs index aba887ccf5..178e2f62dc 100644 --- a/DigitalLearningSolutions.Data/DataServices/UserDataService/UserDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/UserDataService/UserDataService.cs @@ -308,6 +308,7 @@ public partial class UserDataService : IUserDataService u.ProfessionalRegistrationNumber, u.ProfileImage, u.Active, + u.LastAccessed, u.ResetPasswordID, u.TermsAgreed, u.FailedLoginCount, @@ -619,6 +620,7 @@ public void UpdateUserDetailsAccount(string firstName, string lastName, string p ce.CentreName, ce.Active AS CentreActive, da.DateRegistered, + da.LastAccessed, da.CandidateNumber, da.Approved, da.SelfReg, diff --git a/DigitalLearningSolutions.Data/DigitalLearningSolutions.Data.csproj b/DigitalLearningSolutions.Data/DigitalLearningSolutions.Data.csproj index 2d7bea0631..9f8678de17 100644 --- a/DigitalLearningSolutions.Data/DigitalLearningSolutions.Data.csproj +++ b/DigitalLearningSolutions.Data/DigitalLearningSolutions.Data.csproj @@ -11,15 +11,16 @@ - + + - + diff --git a/DigitalLearningSolutions.Data/Factories/ReadOnlyConnectionFactory.cs b/DigitalLearningSolutions.Data/Factories/ReadOnlyConnectionFactory.cs new file mode 100644 index 0000000000..0015ca8775 --- /dev/null +++ b/DigitalLearningSolutions.Data/Factories/ReadOnlyConnectionFactory.cs @@ -0,0 +1,29 @@ +namespace DigitalLearningSolutions.Data.Factories +{ + using Microsoft.Data.SqlClient; + using Microsoft.Extensions.Configuration; + using System; + using System.Data; + public interface IReadOnlyDbConnectionFactory + { + IDbConnection CreateConnection(); + } + public class ReadOnlyDbConnectionFactory : IReadOnlyDbConnectionFactory + { + private const string ReadOnlyConnectionName = "ReadOnlyConnection"; + private readonly string connectionString; + + public ReadOnlyDbConnectionFactory(IConfiguration config) + { + // Ensure the connection string is not null to avoid CS8601 + connectionString = config.GetConnectionString(ReadOnlyConnectionName) + ?? throw new InvalidOperationException($"Connection string '{ReadOnlyConnectionName}' is not configured."); + } + + public IDbConnection CreateConnection() + { + // Ensure the connection is not enlisted in the trasaction scope to avoid distributed transaction errors: + return new SqlConnection(connectionString + ";Enlist=false"); + } + } +} diff --git a/DigitalLearningSolutions.Data/Helpers/GenericSortingHelper.cs b/DigitalLearningSolutions.Data/Helpers/GenericSortingHelper.cs index 7f4380d941..0165d19655 100644 --- a/DigitalLearningSolutions.Data/Helpers/GenericSortingHelper.cs +++ b/DigitalLearningSolutions.Data/Helpers/GenericSortingHelper.cs @@ -250,6 +250,9 @@ public static class DelegateSortByOptions public static readonly (string DisplayText, string PropertyName) Name = ("Name", nameof(DelegateUserCard.SearchableName)); + public static readonly (string DisplayText, string PropertyName) LastAccessed = + ("Last accessed date", nameof(DelegateUserCard.LastAccessed)); + public static readonly (string DisplayText, string PropertyName) RegistrationDate = ("Registration Date", nameof(DelegateUserCard.DateRegistered)); } diff --git a/DigitalLearningSolutions.Data/Models/CompetencyAssessments/Competency.cs b/DigitalLearningSolutions.Data/Models/CompetencyAssessments/Competency.cs new file mode 100644 index 0000000000..d28685ef92 --- /dev/null +++ b/DigitalLearningSolutions.Data/Models/CompetencyAssessments/Competency.cs @@ -0,0 +1,15 @@ +namespace DigitalLearningSolutions.Data.Models.CompetencyAssessments +{ + public class Competency + { + public int StructureId { get; set; } + public int CompetencyID { get; set; } + public int FrameworkId { get; set; } + public string? FrameworkName { get; set; } + public string? GroupName { get; set; } + public int GroupId { get; set; } + public string? CompetencyName { get; set; } + public string? CompetencyDescription { get; set; } + public bool Optional { get; set; } + } +} diff --git a/DigitalLearningSolutions.Data/Models/CompetencyAssessments/CompetencyAssessmentFeatures.cs b/DigitalLearningSolutions.Data/Models/CompetencyAssessments/CompetencyAssessmentFeatures.cs new file mode 100644 index 0000000000..f68bfe7a67 --- /dev/null +++ b/DigitalLearningSolutions.Data/Models/CompetencyAssessments/CompetencyAssessmentFeatures.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DigitalLearningSolutions.Data.Models.CompetencyAssessments +{ + public class CompetencyAssessmentFeatures + { + public int ID { get; set; } + public string CompetencyAssessmentName { get; set; } = string.Empty; + public int UserRole { get; set; } + public bool DescriptionStatus { get; set; } + public bool ProviderandCategoryStatus { get; set; } + public bool VocabularyStatus { get; set; } + public bool WorkingGroupStatus { get; set; } + public bool AllframeworkCompetenciesStatus { get; set; } + } +} diff --git a/DigitalLearningSolutions.Data/Models/CompetencyAssessments/LinkedFrameworks.cs b/DigitalLearningSolutions.Data/Models/CompetencyAssessments/LinkedFrameworks.cs new file mode 100644 index 0000000000..c7fb0e4f2d --- /dev/null +++ b/DigitalLearningSolutions.Data/Models/CompetencyAssessments/LinkedFrameworks.cs @@ -0,0 +1,8 @@ +namespace DigitalLearningSolutions.Data.Models.CompetencyAssessments +{ + using DigitalLearningSolutions.Data.Models.Frameworks; + public class LinkedFramework : BaseFramework + { + public int AssessmentFrameworkCompetencyCount { get; set; } = 0; + } +} diff --git a/DigitalLearningSolutions.Data/Models/Frameworks/BaseFramework.cs b/DigitalLearningSolutions.Data/Models/Frameworks/BaseFramework.cs index 0291d3d13f..38adaf1cb8 100644 --- a/DigitalLearningSolutions.Data/Models/Frameworks/BaseFramework.cs +++ b/DigitalLearningSolutions.Data/Models/Frameworks/BaseFramework.cs @@ -22,7 +22,6 @@ public class BaseFramework : BaseSearchableItem public string? UpdatedBy { get; set; } public int UserRole { get; set; } public int? FrameworkReviewID { get; set; } - public override string SearchableName { get => SearchableNameOverrideForFuzzySharp ?? FrameworkName; diff --git a/DigitalLearningSolutions.Data/Models/Frameworks/FrameworkCompetency.cs b/DigitalLearningSolutions.Data/Models/Frameworks/FrameworkCompetency.cs index a42d3d10fe..cf2117457c 100644 --- a/DigitalLearningSolutions.Data/Models/Frameworks/FrameworkCompetency.cs +++ b/DigitalLearningSolutions.Data/Models/Frameworks/FrameworkCompetency.cs @@ -1,6 +1,7 @@ namespace DigitalLearningSolutions.Data.Models.Frameworks { using DigitalLearningSolutions.Data.Models.SearchSortFilterPaginate; + using System.Collections.Generic; using System.ComponentModel.DataAnnotations; public class FrameworkCompetency : BaseSearchableItem { @@ -15,6 +16,7 @@ public class FrameworkCompetency : BaseSearchableItem public int CompetencyLearningResourcesCount { get; set; } public string? FrameworkName { get; set; } public bool? AlwaysShowDescription { get; set; } + public IEnumerable CompetencyFlags { get; set; } = new List(); public override string SearchableName { diff --git a/DigitalLearningSolutions.Data/Models/SelfAssessments/CurrentSelfAssessment.cs b/DigitalLearningSolutions.Data/Models/SelfAssessments/CurrentSelfAssessment.cs index ccd5af82f5..408c06a647 100644 --- a/DigitalLearningSolutions.Data/Models/SelfAssessments/CurrentSelfAssessment.cs +++ b/DigitalLearningSolutions.Data/Models/SelfAssessments/CurrentSelfAssessment.cs @@ -24,5 +24,6 @@ public class CurrentSelfAssessment : SelfAssessment public int? DelegateUserId { get; set; } public string? DelegateName { get; set; } public string? EnrolledByFullName { get; set; } + public bool SelfAssessmentProcessAgreed { get; set; } } } diff --git a/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSADelegateCompletionStatus.cs b/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSADelegateCompletionStatus.cs index 31a69d5bc1..ee33332727 100644 --- a/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSADelegateCompletionStatus.cs +++ b/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSADelegateCompletionStatus.cs @@ -1,16 +1,31 @@ -namespace DigitalLearningSolutions.Data.Models.SelfAssessments.Export -{ - using System; - public class DCSADelegateCompletionStatus - { - public int? EnrolledMonth { get; set; } - public int? EnrolledYear { get; set; } - public string? FirstName { get; set; } - public string? LastName { get; set; } - public string? Email { get; set; } - public string? CentreField1 { get; set; } - public string? CentreField2 { get; set; } - public string? CentreField3 { get; set; } - public string? Status { get; set; } - } -} +namespace DigitalLearningSolutions.Data.Models.SelfAssessments.Export +{ + using System; + public class DCSADelegateCompletionStatus + { + public int? EnrolledMonth { get; set; } + public int? EnrolledYear { get; set; } + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? Email { get; set; } + public string? RegistrationAnswer1 { get; set; } + public string? RegistrationAnswer2 { get; set; } + public string? RegistrationAnswer3 { get; set; } + public string? RegistrationAnswer4 { get; set; } + public string? RegistrationAnswer5 { get; set; } + public string? RegistrationAnswer6 { get; set; } + public string? Status { get; set; } + + + public string?[] CentreRegistrationPrompts => + new[] + { + RegistrationAnswer1, + RegistrationAnswer2, + RegistrationAnswer3, + RegistrationAnswer4, + RegistrationAnswer5, + RegistrationAnswer6, + }; + } +} diff --git a/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSAOutcomeSummary.cs b/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSAOutcomeSummary.cs index adac779073..673130a83f 100644 --- a/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSAOutcomeSummary.cs +++ b/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSAOutcomeSummary.cs @@ -1,28 +1,42 @@ -namespace DigitalLearningSolutions.Data.Models.SelfAssessments.Export -{ - using System; - public class DCSAOutcomeSummary - { - public int? EnrolledMonth { get; set; } - public int? EnrolledYear { get; set; } - public string? JobGroup { get; set; } - public string? CentreField1 { get; set; } - public string? CentreField2 { get; set; } - public string? CentreField3 { get; set; } - public string? Status { get; set; } - public int? LearningLaunched { get; set; } - public int? LearningCompleted { get; set; } - public int? DataInformationAndContentConfidence { get; set; } - public int? DataInformationAndContentRelevance { get; set; } - public int? TeachinglearningAndSelfDevelopmentConfidence { get; set; } - public int? TeachinglearningAndSelfDevelopmentRelevance { get; set; } - public int? CommunicationCollaborationAndParticipationConfidence { get; set; } - public int? CommunicationCollaborationAndParticipationRelevance { get; set; } - public int? TechnicalProficiencyConfidence { get; set; } - public int? TechnicalProficiencyRelevance { get; set; } - public int? CreationInnovationAndResearchConfidence { get; set; } - public int? CreationInnovationAndResearchRelevance { get; set; } - public int? DigitalIdentityWellbeingSafetyAndSecurityConfidence { get; set; } - public int? DigitalIdentityWellbeingSafetyAndSecurityRelevance { get; set; } - } -} +namespace DigitalLearningSolutions.Data.Models.SelfAssessments.Export +{ + using System; + public class DCSAOutcomeSummary + { + public int? EnrolledMonth { get; set; } + public int? EnrolledYear { get; set; } + public string? JobGroup { get; set; } + public string? RegistrationAnswer1 { get; set; } + public string? RegistrationAnswer2 { get; set; } + public string? RegistrationAnswer3 { get; set; } + public string? RegistrationAnswer4 { get; set; } + public string? RegistrationAnswer5 { get; set; } + public string? RegistrationAnswer6 { get; set; } + public string? Status { get; set; } + public int? LearningLaunched { get; set; } + public int? LearningCompleted { get; set; } + public int? DataInformationAndContentConfidence { get; set; } + public int? DataInformationAndContentRelevance { get; set; } + public int? TeachinglearningAndSelfDevelopmentConfidence { get; set; } + public int? TeachinglearningAndSelfDevelopmentRelevance { get; set; } + public int? CommunicationCollaborationAndParticipationConfidence { get; set; } + public int? CommunicationCollaborationAndParticipationRelevance { get; set; } + public int? TechnicalProficiencyConfidence { get; set; } + public int? TechnicalProficiencyRelevance { get; set; } + public int? CreationInnovationAndResearchConfidence { get; set; } + public int? CreationInnovationAndResearchRelevance { get; set; } + public int? DigitalIdentityWellbeingSafetyAndSecurityConfidence { get; set; } + public int? DigitalIdentityWellbeingSafetyAndSecurityRelevance { get; set; } + + public string?[] CentreRegistrationPrompts => + new[] + { + RegistrationAnswer1, + RegistrationAnswer2, + RegistrationAnswer3, + RegistrationAnswer4, + RegistrationAnswer5, + RegistrationAnswer6, + }; + } +} diff --git a/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/SelfAssessmentReportData.cs b/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/SelfAssessmentReportData.cs index 27c3d31bc7..847162bf47 100644 --- a/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/SelfAssessmentReportData.cs +++ b/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/SelfAssessmentReportData.cs @@ -1,26 +1,41 @@ -namespace DigitalLearningSolutions.Data.Models.SelfAssessments.Export -{ - using System; - public class SelfAssessmentReportData - { - public string? SelfAssessment { get; set; } - public string? Learner { get; set; } - public bool LearnerActive { get; set; } - public string? PRN { get; set; } - public string? JobGroup { get; set; } - public string? ProgrammeCourse { get; set; } - public string? Organisation { get; set; } - public string? DepartmentTeam { get; set; } - public string? OtherCentres { get; set; } - public string? DLSRole { get; set; } - public DateTime? Registered { get; set; } - public DateTime? Started { get; set; } - public DateTime? LastAccessed { get; set; } - public int? OptionalProficienciesAssessed { get; set; } - public int? SelfAssessedAchieved { get; set; } - public int? ConfirmedResults { get; set; } - public DateTime? SignOffRequested { get; set; } - public bool SignOffAchieved { get; set; } - public DateTime? ReviewedDate { get; set; } - } -} +namespace DigitalLearningSolutions.Data.Models.SelfAssessments.Export +{ + using System; + public class SelfAssessmentReportData + { + public string? SelfAssessment { get; set; } + public string? Learner { get; set; } + public bool LearnerActive { get; set; } + public string? PRN { get; set; } + public string? JobGroup { get; set; } + public string? RegistrationAnswer1 { get; set; } + public string? RegistrationAnswer2 { get; set; } + public string? RegistrationAnswer3 { get; set; } + public string? RegistrationAnswer4 { get; set; } + public string? RegistrationAnswer5 { get; set; } + public string? RegistrationAnswer6 { get; set; } + public string? OtherCentres { get; set; } + public string? DLSRole { get; set; } + public DateTime? Registered { get; set; } + public DateTime? Started { get; set; } + public DateTime? LastAccessed { get; set; } + public int? OptionalProficienciesAssessed { get; set; } + public int? SelfAssessedAchieved { get; set; } + public int? ConfirmedResults { get; set; } + public DateTime? SignOffRequested { get; set; } + public bool SignOffAchieved { get; set; } + public DateTime? ReviewedDate { get; set; } + + // we need this for iteration across the registration answers from Delegate Accounts which match the custom fields of Centres. + public string?[] CentreRegistrationPrompts => + new[] + { + RegistrationAnswer1, + RegistrationAnswer2, + RegistrationAnswer3, + RegistrationAnswer4, + RegistrationAnswer5, + RegistrationAnswer6, + }; + } +} diff --git a/DigitalLearningSolutions.Data/Models/SelfAssessments/SelfAssessment.cs b/DigitalLearningSolutions.Data/Models/SelfAssessments/SelfAssessment.cs index 1beabfb672..dbd1da0de6 100644 --- a/DigitalLearningSolutions.Data/Models/SelfAssessments/SelfAssessment.cs +++ b/DigitalLearningSolutions.Data/Models/SelfAssessments/SelfAssessment.cs @@ -1,4 +1,6 @@ -namespace DigitalLearningSolutions.Data.Models.SelfAssessments +using System; + +namespace DigitalLearningSolutions.Data.Models.SelfAssessments { public class SelfAssessment : CurrentLearningItem { @@ -11,6 +13,10 @@ public class SelfAssessment : CurrentLearningItem public string? ManageOptionalCompetenciesPrompt { get; set; } public string? QuestionLabel { get; set; } public string? DescriptionLabel { get; set; } + public DateTime? RetirementDate { get; set; } + + public DateTime? EnrolmentCutoffDate { get; set; } + public string? RetirementReason { get; set; } } } diff --git a/DigitalLearningSolutions.Data/Models/SessionData/Supervisor/SessionEnrolOnCompetencyAssessment.cs b/DigitalLearningSolutions.Data/Models/SessionData/Supervisor/SessionEnrolOnCompetencyAssessment.cs index 1fd5fee129..a3271d569d 100644 --- a/DigitalLearningSolutions.Data/Models/SessionData/Supervisor/SessionEnrolOnCompetencyAssessment.cs +++ b/DigitalLearningSolutions.Data/Models/SessionData/Supervisor/SessionEnrolOnCompetencyAssessment.cs @@ -6,5 +6,6 @@ public class SessionEnrolOnCompetencyAssessment public int? SelfAssessmentID { get; set; } public DateTime? CompleteByDate { get; set; } public int? SelfAssessmentSupervisorRoleId { get; set; } + public bool ActionConfirmed { get; set; } } } diff --git a/DigitalLearningSolutions.Data/Models/SuperAdmin/SuperAdminDelegateAccount.cs b/DigitalLearningSolutions.Data/Models/SuperAdmin/SuperAdminDelegateAccount.cs index a18268f104..aac978b35a 100644 --- a/DigitalLearningSolutions.Data/Models/SuperAdmin/SuperAdminDelegateAccount.cs +++ b/DigitalLearningSolutions.Data/Models/SuperAdmin/SuperAdminDelegateAccount.cs @@ -19,6 +19,7 @@ public SuperAdminDelegateAccount(DelegateEntity delegateEntity) LearningHubAuthId = delegateEntity.UserAccount.LearningHubAuthId; RegistrationConfirmationHash = delegateEntity.DelegateAccount.RegistrationConfirmationHash; DateRegistered = delegateEntity.DelegateAccount.DateRegistered; + LastAccessed = delegateEntity.DelegateAccount.LastAccessed; SelfReg = delegateEntity.DelegateAccount.SelfReg; Active = delegateEntity.DelegateAccount.Active; EmailVerified = delegateEntity.UserAccount.EmailVerified; diff --git a/DigitalLearningSolutions.Data/Models/User/AdminAccount.cs b/DigitalLearningSolutions.Data/Models/User/AdminAccount.cs index eaff1da52f..51a109785b 100644 --- a/DigitalLearningSolutions.Data/Models/User/AdminAccount.cs +++ b/DigitalLearningSolutions.Data/Models/User/AdminAccount.cs @@ -1,5 +1,7 @@ namespace DigitalLearningSolutions.Data.Models.User { + using System; + public class AdminAccount { public int Id { get; set; } @@ -26,6 +28,7 @@ public class AdminAccount public bool IsWorkforceContributor { get; set; } public bool IsLocalWorkforceManager { get; set; } public bool IsNominatedSupervisor { get; set; } + public DateTime? LastAccessed { get; set; } public bool IsCmsAdministrator => ImportOnly && IsContentManager; public bool IsCmsManager => IsContentManager && !ImportOnly; diff --git a/DigitalLearningSolutions.Data/Models/User/AdminEntity.cs b/DigitalLearningSolutions.Data/Models/User/AdminEntity.cs index e070aca124..a9b71e5143 100644 --- a/DigitalLearningSolutions.Data/Models/User/AdminEntity.cs +++ b/DigitalLearningSolutions.Data/Models/User/AdminEntity.cs @@ -3,6 +3,7 @@ using DigitalLearningSolutions.Data.Helpers; using DigitalLearningSolutions.Data.Models.Centres; using DigitalLearningSolutions.Data.Models.SearchSortFilterPaginate; + using System; public class AdminEntity : BaseSearchableItem { @@ -72,6 +73,7 @@ public override string SearchableName public bool IsSuperAdmin => AdminAccount.IsSuperAdmin; public bool IsReportsViewer => AdminAccount.IsReportsViewer; public bool IsActive => AdminAccount.Active; + public DateTime? LastAccessed => AdminAccount.LastAccessed; } } diff --git a/DigitalLearningSolutions.Data/Models/User/DelegateAccount.cs b/DigitalLearningSolutions.Data/Models/User/DelegateAccount.cs index 3f720e350b..66d458fd13 100644 --- a/DigitalLearningSolutions.Data/Models/User/DelegateAccount.cs +++ b/DigitalLearningSolutions.Data/Models/User/DelegateAccount.cs @@ -12,6 +12,7 @@ public class DelegateAccount public bool CentreActive { get; set; } public string CandidateNumber { get; set; } = string.Empty; public DateTime DateRegistered { get; set; } + public DateTime? LastAccessed { get; set; } public string? Answer1 { get; set; } public string? Answer2 { get; set; } public string? Answer3 { get; set; } diff --git a/DigitalLearningSolutions.Data/Models/User/DelegateUser.cs b/DigitalLearningSolutions.Data/Models/User/DelegateUser.cs index 7033b8d13f..f007b355b1 100644 --- a/DigitalLearningSolutions.Data/Models/User/DelegateUser.cs +++ b/DigitalLearningSolutions.Data/Models/User/DelegateUser.cs @@ -9,6 +9,7 @@ public class DelegateUser : User public int UserId { get; set; } public string CandidateNumber { get; set; } = string.Empty; public DateTime? DateRegistered { get; set; } + public DateTime? LastAccessed { get; set; } public int JobGroupId { get; set; } public string? JobGroupName { get; set; } public string? Answer1 { get; set; } diff --git a/DigitalLearningSolutions.Data/Models/User/DelegateUserCard.cs b/DigitalLearningSolutions.Data/Models/User/DelegateUserCard.cs index ed04402308..1e9caf4e9d 100644 --- a/DigitalLearningSolutions.Data/Models/User/DelegateUserCard.cs +++ b/DigitalLearningSolutions.Data/Models/User/DelegateUserCard.cs @@ -23,6 +23,7 @@ public DelegateUserCard(DelegateEntity delegateEntity) Password = delegateEntity.UserAccount.PasswordHash; CandidateNumber = delegateEntity.DelegateAccount.CandidateNumber; DateRegistered = delegateEntity.DelegateAccount.DateRegistered; + LastAccessed = delegateEntity.DelegateAccount.LastAccessed; JobGroupId = delegateEntity.UserAccount.JobGroupId; JobGroupName = delegateEntity.UserAccount.JobGroupName; Answer1 = delegateEntity.DelegateAccount.Answer1; diff --git a/DigitalLearningSolutions.Data/Models/User/UserAccount.cs b/DigitalLearningSolutions.Data/Models/User/UserAccount.cs index 4e1c975bf1..6afca34e14 100644 --- a/DigitalLearningSolutions.Data/Models/User/UserAccount.cs +++ b/DigitalLearningSolutions.Data/Models/User/UserAccount.cs @@ -14,6 +14,7 @@ public class UserAccount public string? ProfessionalRegistrationNumber { get; set; } public byte[]? ProfileImage { get; set; } public bool Active { get; set; } + public DateTime? LastAccessed { get; set; } public int? ResetPasswordId { get; set; } public DateTime? TermsAgreed { get; set; } public int FailedLoginCount { get; set; } diff --git a/DigitalLearningSolutions.Web.AutomatedUiTests/DigitalLearningSolutions.Web.AutomatedUiTests.csproj b/DigitalLearningSolutions.Web.AutomatedUiTests/DigitalLearningSolutions.Web.AutomatedUiTests.csproj index a26a174269..4da1fb06bd 100644 --- a/DigitalLearningSolutions.Web.AutomatedUiTests/DigitalLearningSolutions.Web.AutomatedUiTests.csproj +++ b/DigitalLearningSolutions.Web.AutomatedUiTests/DigitalLearningSolutions.Web.AutomatedUiTests.csproj @@ -9,17 +9,17 @@ - + - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/DigitalLearningSolutions.Web.IntegrationTests/DigitalLearningSolutions.Web.IntegrationTests.csproj b/DigitalLearningSolutions.Web.IntegrationTests/DigitalLearningSolutions.Web.IntegrationTests.csproj index 2ca3bc89c9..fa0060eb19 100644 --- a/DigitalLearningSolutions.Web.IntegrationTests/DigitalLearningSolutions.Web.IntegrationTests.csproj +++ b/DigitalLearningSolutions.Web.IntegrationTests/DigitalLearningSolutions.Web.IntegrationTests.csproj @@ -10,12 +10,12 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/LearningPortal/SelfAssessmentTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/LearningPortal/SelfAssessmentTests.cs index 977c4445e3..475f7f4a1f 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/LearningPortal/SelfAssessmentTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/LearningPortal/SelfAssessmentTests.cs @@ -870,5 +870,64 @@ public void SelfAssessmentOverview_Should_Return_View_With_Optional_Filter_Appli result.Should().BeViewResult().ModelAs().CompetencyGroups.ToList()[0].Count().Should().Be(1); } + + [Test] + public void SelfAssessment_should_return_description_view_when_process_agreed_or_not_supervised() + { + // Given + var selfAssessment = SelfAssessmentTestHelper.CreateDefaultSelfAssessment(); + selfAssessment.IsSupervised = false; // or set SelfAssessmentProcessAgreed = true + A.CallTo(() => selfAssessmentService.GetSelfAssessmentForCandidateById(DelegateUserId, SelfAssessmentId)) + .Returns(selfAssessment); + A.CallTo(() => selfAssessmentService.GetAllSupervisorsForSelfAssessmentId(SelfAssessmentId, DelegateUserId)) + .Returns(new List()); + var expectedModel = new SelfAssessmentDescriptionViewModel(selfAssessment, new List()); + + // When + var result = controller.SelfAssessment(SelfAssessmentId); + + // Then + result.Should().BeViewResult() + .WithViewName("SelfAssessments/SelfAssessmentDescription") + .Model.Should().BeEquivalentTo(expectedModel); + } + + [Test] + public void ProcessAgreed_should_return_agree_view_when_modelstate_invalid() + { + // Given + var model = new SelfAssessmentProcessViewModel { SelfAssessmentID = SelfAssessmentId }; + controller.ModelState.AddModelError("Test", "Error"); + + // When + var result = controller.ProcessAgreed(model); + + // Then + result.Should().BeViewResult() + .WithViewName("SelfAssessments/AgreeSelfAssessmentProcess") + .Model.Should().Be(model); + } + + [Test] + public void ProcessAgreed_should_mark_progress_and_redirect_to_self_assessment() + { + // Given + var selfAssessment = SelfAssessmentTestHelper.CreateDefaultSelfAssessment(); + var model = new SelfAssessmentProcessViewModel { SelfAssessmentID = SelfAssessmentId }; + A.CallTo(() => selfAssessmentService.GetSelfAssessmentForCandidateById(DelegateUserId, SelfAssessmentId)) + .Returns(selfAssessment); + + // When + var result = controller.ProcessAgreed(model); + + // Then + A.CallTo(() => selfAssessmentService.MarkProgressAgreed(SelfAssessmentId, DelegateUserId)) + .MustHaveHappenedOnceExactly(); + + result.Should().BeRedirectToActionResult() + .WithActionName("SelfAssessment") + .WithRouteValue("selfAssessmentId", SelfAssessmentId); + } + } } diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/Register/RegisterDelegateByCentreControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/Register/RegisterDelegateByCentreControllerTests.cs index 0d4a77fc61..0040c2b514 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/Register/RegisterDelegateByCentreControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/Register/RegisterDelegateByCentreControllerTests.cs @@ -176,7 +176,7 @@ public void LearnerInformationPost_updates_tempdata_correctly() const string answer4 = "answer4"; const string answer5 = "answer5"; const string answer6 = "answer6"; - const string professionalRegistrationNumber = "PRN1234"; + const string professionalRegistrationNumber = "PR123456"; var model = new LearnerInformationViewModel { JobGroup = jobGroupId, diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/SuperAdmin/CentresControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/SuperAdmin/CentresControllerTests.cs index 495ecf0cc3..18105d7dbb 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/SuperAdmin/CentresControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/SuperAdmin/CentresControllerTests.cs @@ -83,7 +83,7 @@ public void EditCentreDetails_updates_centre_and_redirects_with_successful_save( CentreTypeId = 1, CentreType = "NHS Organisation", RegionName = "National", - CentreEmail = "no.email@hee.nhs.uk", + RegistrationEmail = "no.email@hee.nhs.uk", IpPrefix = "12.33.4", ShowOnMap = true, RegionId = 13 @@ -99,7 +99,7 @@ public void EditCentreDetails_updates_centre_and_redirects_with_successful_save( model.CentreName, model.CentreTypeId, model.RegionId, - model.CentreEmail, + model.RegistrationEmail, model.IpPrefix, model.ShowOnMap)) .MustHaveHappenedOnceExactly(); diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/SupervisorController/SupervisorControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/SupervisorController/SupervisorControllerTests.cs index 7330042c4e..7dddbd0e55 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/SupervisorController/SupervisorControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/SupervisorController/SupervisorControllerTests.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; + using Microsoft.FeatureManagement; using NUnit.Framework; using System.Collections.Generic; using System.Linq; @@ -49,6 +50,7 @@ public class SupervisorControllerTests private IPdfService pdfService = null!; private SupervisorController controller = null!; private ICourseCategoriesService courseCategoriesService = null!; + private IFeatureManager featureManager = null!; [SetUp] public void Setup() @@ -73,6 +75,7 @@ public void Setup() candidateAssessmentDownloadFileService = A.Fake(); pdfService = A.Fake(); courseCategoriesService = A.Fake(); + featureManager = A.Fake(); A.CallTo(() => candidateAssessmentDownloadFileService.GetCandidateAssessmentDownloadFileForCentre(A._, A._, A._)) .Returns(new byte[] { }); @@ -108,7 +111,8 @@ public void Setup() candidateAssessmentDownloadFileService, clockUtility, pdfService, - courseCategoriesService + courseCategoriesService, + featureManager ); controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = user } }; @@ -140,7 +144,8 @@ public void ExportCandidateAssessment_should_return_file_object_with_file_name_i candidateAssessmentDownloadFileService, clockUtility, pdfService, - courseCategoriesService + courseCategoriesService, + featureManager ); string expectedFileName = $"{((selfAssessmentName.Length > 30) ? selfAssessmentName.Substring(0, 30) : selfAssessmentName)} - {delegateName} - {clockUtility.UtcNow:yyyy-MM-dd}.xlsx"; diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/DelegateProgressControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/DelegateProgressControllerTests.cs index 4934bf16b3..d5f6b60186 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/DelegateProgressControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/DelegateProgressControllerTests.cs @@ -1,7 +1,5 @@ namespace DigitalLearningSolutions.Web.Tests.Controllers.TrackingSystem.Delegates { - using System; - using System.Collections.Generic; using DigitalLearningSolutions.Data.Models; using DigitalLearningSolutions.Data.Models.Courses; using DigitalLearningSolutions.Data.Models.CustomPrompts; @@ -15,8 +13,11 @@ using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.DelegateProgress; using FakeItEasy; using FluentAssertions.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using NUnit.Framework; + using System; + using System.Collections.Generic; public class DelegateProgressControllerTests { @@ -386,7 +387,11 @@ public void Delegate_removal_for_delegate_with_no_active_progress_returns_not_fo ); // Then - result.Should().BeNotFoundResult(); + result.Should() + .BeRedirectToActionResult() + .WithActionName("StatusCode") + .WithControllerName("LearningSolutions") + .WithRouteValue("code", 410); } [Test] diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/EditDelegateControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/EditDelegateControllerTests.cs index 5c5df8ac44..c08f3dbe35 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/EditDelegateControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/EditDelegateControllerTests.cs @@ -175,7 +175,7 @@ public void Index_post_returns_view_with_model_error_with_invalid_prn() result.As().Model.Should().BeOfType(); AssertModelStateErrorIsExpected( result, - "Invalid professional registration number format - Only alphanumeric characters (a-z, A-Z and 0-9) and hyphens (-) allowed" + ErrorMessagesTestHelper.InvalidFormatError ); A.CallTo(() => userService.GetDelegateById(A._)).MustNotHaveHappened(); } diff --git a/DigitalLearningSolutions.Web.Tests/DigitalLearningSolutions.Web.Tests.csproj b/DigitalLearningSolutions.Web.Tests/DigitalLearningSolutions.Web.Tests.csproj index ef4dd6e67f..6b1d9df392 100644 --- a/DigitalLearningSolutions.Web.Tests/DigitalLearningSolutions.Web.Tests.csproj +++ b/DigitalLearningSolutions.Web.Tests/DigitalLearningSolutions.Web.Tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/DigitalLearningSolutions.Web.Tests/Helpers/ProfessionalRegistrationNumberHelperTests.cs b/DigitalLearningSolutions.Web.Tests/Helpers/ProfessionalRegistrationNumberHelperTests.cs index d78c74f042..a7270f280d 100644 --- a/DigitalLearningSolutions.Web.Tests/Helpers/ProfessionalRegistrationNumberHelperTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Helpers/ProfessionalRegistrationNumberHelperTests.cs @@ -1,11 +1,12 @@ namespace DigitalLearningSolutions.Web.Tests.Helpers { - using System.Linq; using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.Tests.TestHelpers; using FluentAssertions; using FluentAssertions.Execution; using Microsoft.AspNetCore.Mvc.ModelBinding; using NUnit.Framework; + using System.Linq; public class ProfessionalRegistrationNumberHelperTests { @@ -70,7 +71,7 @@ public void ValidateProfessionalRegistrationNumber_does_not_set_errors_when_vali { // Given var state = new ModelStateDictionary(); - const string validPrn = "abc-123"; + const string validPrn = "AB123456"; // When ProfessionalRegistrationNumberHelper.ValidateProfessionalRegistrationNumber( @@ -104,22 +105,13 @@ public void ValidateProfessionalRegistrationNumber_sets_error_when_hasPrn_is_not } } - [TestCase(null, "Enter a professional registration number")] - [TestCase("", "Enter a professional registration number")] - [TestCase("123", "Professional registration number must be between 5 and 20 characters")] - [TestCase("0123456789-0123456789", "Professional registration number must be between 5 and 20 characters")] - [TestCase( - "01234_", - "Invalid professional registration number format - Only alphanumeric characters (a-z, A-Z and 0-9) and hyphens (-) allowed" - )] - [TestCase( - "01234 ", - "Invalid professional registration number format - Only alphanumeric characters (a-z, A-Z and 0-9) and hyphens (-) allowed" - )] - [TestCase( - "01234$", - "Invalid professional registration number format - Only alphanumeric characters (a-z, A-Z and 0-9) and hyphens (-) allowed" - )] + [TestCase(null, ErrorMessagesTestHelper.MissingNumberError)] + [TestCase("", ErrorMessagesTestHelper.MissingNumberError)] + [TestCase("1234", ErrorMessagesTestHelper.LengthError)] + [TestCase("1234", ErrorMessagesTestHelper.LengthError)] + [TestCase("01234_", ErrorMessagesTestHelper.InvalidFormatError)] + [TestCase("01234 ", ErrorMessagesTestHelper.InvalidFormatError)] + [TestCase("01234$", ErrorMessagesTestHelper.InvalidFormatError)] public void ValidateProfessionalRegistrationNumber_sets_error_when_prn_is_invalid( string prn, string expectedError diff --git a/DigitalLearningSolutions.Web.Tests/Services/LoginServiceTests.cs b/DigitalLearningSolutions.Web.Tests/Services/LoginServiceTests.cs index f8dad9821e..a77c38f7b8 100644 --- a/DigitalLearningSolutions.Web.Tests/Services/LoginServiceTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Services/LoginServiceTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using DigitalLearningSolutions.Data.DataServices; using DigitalLearningSolutions.Data.Enums; using DigitalLearningSolutions.Data.Helpers; using DigitalLearningSolutions.Data.Models; @@ -32,6 +33,7 @@ private static readonly (string?, List<(int centreId, string centreName, string private LoginService loginService = null!; private IUserService userService = null!; private IUserVerificationService userVerificationService = null!; + private ILoginDataService loginDataService = null!; [SetUp] public void Setup() @@ -39,7 +41,7 @@ public void Setup() userVerificationService = A.Fake(x => x.Strict()); userService = A.Fake(x => x.Strict()); - loginService = new LoginService(userService, userVerificationService); + loginService = new LoginService(userService, userVerificationService, loginDataService); } [Test] diff --git a/DigitalLearningSolutions.Web.Tests/TestHelpers/ErrorMessagesTestHelper.cs b/DigitalLearningSolutions.Web.Tests/TestHelpers/ErrorMessagesTestHelper.cs new file mode 100644 index 0000000000..7636944e7a --- /dev/null +++ b/DigitalLearningSolutions.Web.Tests/TestHelpers/ErrorMessagesTestHelper.cs @@ -0,0 +1,17 @@ + +namespace DigitalLearningSolutions.Web.Tests.TestHelpers +{ + public static class ErrorMessagesTestHelper + { + public const string InvalidFormatError = + "Invalid professional registration number format. " + + "Valid formats include: 7 digits (e.g., 1234567), 1–2 letters followed by 6 digits (e.g., AB123456), " + + "4–8 digits, an optional 'P' plus 5–6 digits, 'C' or 'P' plus 6 digits, " + + "an optional letter plus 5–6 digits, 'L' plus 4–6 digits, " + + "or 2 digits followed by a hyphen and 4–5 alphanumeric characters (e.g., 12-AB123)."; + + public const string MissingNumberError = "Enter a professional registration number"; + public const string LengthError = "Professional registration number must be between 5 and 20 characters"; + + } +} diff --git a/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessments.cs b/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessments.cs index aee8442d14..4464dee99c 100644 --- a/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessments.cs +++ b/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessments.cs @@ -1,20 +1,20 @@ namespace DigitalLearningSolutions.Web.Controllers.CompetencyAssessmentsController { + using DigitalLearningSolutions.Data.Enums; using DigitalLearningSolutions.Data.Models.CompetencyAssessments; + using DigitalLearningSolutions.Data.Models.Frameworks; + using DigitalLearningSolutions.Data.Models.SelfAssessments; + using DigitalLearningSolutions.Web.Attributes; + using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.Models.Enums; using DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments; + using GDS.MultiPageFormData.Enums; using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Logging; + using Serilog.Extensions.Hosting; using System.Collections.Generic; - using DigitalLearningSolutions.Data.Enums; - using DigitalLearningSolutions.Web.Attributes; - using DigitalLearningSolutions.Web.Models.Enums; - using DigitalLearningSolutions.Web.Helpers; using System.Linq; - using AspNetCoreGeneratedDocument; - using Microsoft.AspNetCore.Mvc.Rendering; - using DigitalLearningSolutions.Data.Models.Centres; - using DigitalLearningSolutions.Data.Models.Frameworks; - using Microsoft.CodeAnalysis.CSharp.Syntax; public partial class CompetencyAssessmentsController { @@ -83,7 +83,6 @@ public IActionResult ViewCompetencyAssessments(string tabname, string? searchStr isWorkforceManager ); } - var currentTab = tabname == "All" ? CompetencyAssessmentsTab.AllCompetencyAssessments : CompetencyAssessmentsTab.MyCompetencyAssessments; CompetencyAssessmentsViewModel? model = new CompetencyAssessmentsViewModel( isWorkforceManager, @@ -97,12 +96,18 @@ public IActionResult ViewCompetencyAssessments(string tabname, string? searchStr [Route("/CompetencyAssessments/{actionName}/Name/{competencyAssessmentId}")] [Route("/CompetencyAssessments/Framework/{frameworkId}/{actionName}/Name")] + [Route("/CompetencyAssessments/Framework/{frameworkId}/{competencyAssessmentId}/{actionName}/Name")] [Route("/CompetencyAssessments/{actionName}/Name")] [SetSelectedTab(nameof(NavMenuTab.CompetencyAssessments))] public IActionResult CompetencyAssessmentName(string actionName, int competencyAssessmentId = 0, int? frameworkId = null) { var adminId = GetAdminID(); var competencyAssessmentBase = new CompetencyAssessmentBase(); + if ((frameworkId.HasValue && frameworkId.Value != 0 && actionName == "New")) + { + var data = new CompetencyAssessmentFeaturesViewModel(); + SetcompetencyAssessmentFeaturesData(data); + } if (competencyAssessmentId > 0) { competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId); @@ -130,6 +135,7 @@ public IActionResult CompetencyAssessmentName(string actionName, int competencyA [HttpPost] [Route("/CompetencyAssessments/{actionName}/Name/{competencyAssessmentId}")] [Route("/CompetencyAssessments/Framework/{frameworkId}/{actionName}/Name")] + [Route("/CompetencyAssessments/Framework/{frameworkId}/{competencyAssessmentId}/{actionName}/Name")] [Route("/CompetencyAssessments/{actionName}/Name")] [SetSelectedTab(nameof(NavMenuTab.CompetencyAssessments))] public IActionResult SaveProfileName(CompetencyAssessmentBase competencyAssessmentBase, string actionName, int competencyAssessmentId = 0, int? frameworkId = null) @@ -154,6 +160,7 @@ public IActionResult SaveProfileName(CompetencyAssessmentBase competencyAssessme return View("Name", competencyAssessmentBase); } competencyAssessmentId = competencyAssessmentService.InsertCompetencyAssessment(adminId, userCentreId, competencyAssessmentBase.CompetencyAssessmentName, frameworkId); + if(frameworkId.HasValue && frameworkId.Value != 0) return RedirectToAction("CompetencyAssessmentFeatures", new { competencyAssessmentId, frameworkId }); } else { @@ -164,6 +171,9 @@ public IActionResult SaveProfileName(CompetencyAssessmentBase competencyAssessme ModelState.AddModelError(nameof(CompetencyAssessmentBase.CompetencyAssessmentName), "Another competency assessment exists with that name. Please choose a different name."); return View("Name", competencyAssessmentBase); } + if (frameworkId.HasValue && frameworkId.Value != 0 + && competencyAssessmentId != 0 + && actionName == "Edit") return RedirectToAction("CompetencyAssessmentFeatures", new { competencyAssessmentId, frameworkId }); } return RedirectToAction("ManageCompetencyAssessment", new { competencyAssessmentId, frameworkId }); } @@ -389,5 +399,332 @@ public IActionResult SaveVocabulary(EditVocabularyViewModel model) competencyAssessmentService.UpdateVocabularyTaskStatus(model.ID, model.TaskStatus ?? false); return RedirectToAction("ManageCompetencyAssessment", new { competencyAssessmentId = model.ID }); } + [Route("/CompetencyAssessments/{competencyAssessmentId}/Frameworks/{actionName}")] + public IActionResult SelectFrameworkSources(int competencyAssessmentId, string actionName) + { + var adminId = GetAdminID(); + var frameworks = frameworkService.GetAllFrameworks(adminId); + var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId); + if (competencyAssessmentBase == null) + { + logger.LogWarning($"Failed to load Vocabulary page for competencyAssessmentId: {competencyAssessmentId} adminId: {adminId}"); + return StatusCode(500); + } + if (competencyAssessmentBase.UserRole < 2) + { + return StatusCode(403); + } + var primaryFrameworkId = competencyAssessmentService.GetPrimaryLinkedFrameworkId(competencyAssessmentId); + var additionalFrameworks = competencyAssessmentService.GetLinkedFrameworkIds(competencyAssessmentId); + var competencyAssessmentTaskStatus = competencyAssessmentService.GetCompetencyAssessmentTaskStatus(competencyAssessmentId, null); + var model = new SelectFrameworkSourcesViewModel(competencyAssessmentBase, frameworks, additionalFrameworks, primaryFrameworkId, competencyAssessmentTaskStatus.FrameworkLinksTaskStatus, actionName); + return View(model); + } + [HttpPost] + [Route("/CompetencyAssessments/{competencyAssessmentId}/Frameworks/{actionName}")] + public IActionResult SelectFrameworkSources(SelectFrameworkSourcesFormData model, string actionName) + { + var adminId = GetAdminID(); + var competencyAssessmentId = model.CompetencyAssessmentId; + if (!ModelState.IsValid) + { + + var frameworks = frameworkService.GetAllFrameworks(adminId); + var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId); + if (competencyAssessmentBase == null) + { + logger.LogWarning($"Failed to load Vocabulary page for competencyAssessmentId: {competencyAssessmentId} adminId: {adminId}"); + return StatusCode(500); + } + if (competencyAssessmentBase.UserRole < 2) + { + return StatusCode(403); + } + var primaryFrameworkId = competencyAssessmentService.GetPrimaryLinkedFrameworkId(competencyAssessmentId); + var additionalFrameworks = competencyAssessmentService.GetLinkedFrameworkIds(competencyAssessmentId); + var viewModel = new SelectFrameworkSourcesViewModel(competencyAssessmentBase, frameworks, additionalFrameworks, primaryFrameworkId, model.TaskStatus, model.ActionName); + return View("SelectFrameworkSources", viewModel); + } + if (actionName == "AddFramework") + { + competencyAssessmentService.InsertSelfAssessmentFramework(adminId, competencyAssessmentId, model.FrameworkId); + return RedirectToAction("SelectFrameworkSources", new { competencyAssessmentId, actionName = "Summary" }); + } + else + { + competencyAssessmentService.UpdateFrameworkLinksTaskStatus(model.CompetencyAssessmentId, model.TaskStatus ?? false, null); + return RedirectToAction("ManageCompetencyAssessment", new { competencyAssessmentId = model.CompetencyAssessmentId }); + } + } + [Route("/CompetencyAssessments/{competencyAssessmentId}/Frameworks/{frameworkId}/Remove")] + public IActionResult RemoveFramework(int frameworkId, int competencyAssessmentId) + { + var frameworkCompetencyCount = competencyAssessmentService.GetCompetencyCountByFrameworkId(competencyAssessmentId, frameworkId); + if (frameworkCompetencyCount > 0) + { + var adminId = GetAdminID(); + var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId); + var framework = frameworkService.GetFrameworkDetailByFrameworkId(frameworkId, adminId); + var model = new ConfirmRemoveFrameworkSourceViewModel(competencyAssessmentBase, framework, frameworkCompetencyCount); + return View("ConfirmRemoveFrameworkSource", model); + } + else + { + var adminId = GetAdminID(); + competencyAssessmentService.RemoveSelfAssessmentFramework(competencyAssessmentId, frameworkId, adminId); + } + return RedirectToAction("SelectFrameworkSources", new { competencyAssessmentId, actionName = "Summary" }); + } + [HttpPost] + [Route("/CompetencyAssessments/{competencyAssessmentId}/Frameworks/{frameworkId}/Remove")] + public IActionResult RemoveFramework(ConfirmRemoveFrameworkSourceViewModel model) + { + if (!ModelState.IsValid) + { + return View("ConfirmRemoveFrameworkSource", model); + } + var adminId = GetAdminID(); + competencyAssessmentService.RemoveFrameworkCompetenciesFromAssessment(model.CompetencyAssessmentId, model.FrameworkId); + competencyAssessmentService.RemoveSelfAssessmentFramework(model.CompetencyAssessmentId, model.FrameworkId, adminId); + return RedirectToAction("SelectFrameworkSources", new { model.CompetencyAssessmentId, actionName = "Summary" }); + } + [Route("/CompetencyAssessments/{competencyAssessmentId}/Competencies")] + public IActionResult ViewSelectedCompetencies(int competencyAssessmentId) + { + + var competencies = competencyAssessmentService.GetCompetenciesForCompetencyAssessment(competencyAssessmentId); + var linkedFrameworks = competencyAssessmentService.GetLinkedFrameworksForCompetencyAssessment(competencyAssessmentId); + if (!competencies.Any()) + { + return RedirectToAction("AddCompetenciesSelectFramework", new { competencyAssessmentId }); + } + var adminId = GetAdminID(); + var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId); + if (competencyAssessmentBase == null) + { + logger.LogWarning($"Failed to load Competencies page for competencyAssessmentId: {competencyAssessmentId} adminId: {adminId}"); + return StatusCode(500); + } + if (competencyAssessmentBase.UserRole < 2) + { + return StatusCode(403); + } + var competencyAssessmentTaskStatus = competencyAssessmentService.GetCompetencyAssessmentTaskStatus(competencyAssessmentId, null); + var model = new ViewSelectedCompetenciesViewModel(competencyAssessmentBase, competencies, linkedFrameworks, competencyAssessmentTaskStatus.SelectCompetenciesTaskStatus); + return View(model); + } + [Route("/CompetencyAssessments/{competencyAssessmentId}/Competencies/Add/SelectFramework")] + public IActionResult AddCompetenciesSelectFramework(int competencyAssessmentId) + { + var linkedFrameworks = competencyAssessmentService.GetLinkedFrameworksForCompetencyAssessment(competencyAssessmentId); + if (!linkedFrameworks.Any()) + { + return RedirectToAction("SelectFrameworkSources", new { competencyAssessmentId, actionName = "AddFramework" }); + } + var adminId = GetAdminID(); + var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId); + if (competencyAssessmentBase == null) + { + logger.LogWarning($"Failed to load Competencies page for competencyAssessmentId: {competencyAssessmentId} adminId: {adminId}"); + return StatusCode(500); + } + if (competencyAssessmentBase.UserRole < 2) + { + return StatusCode(403); + } + + var model = new AddCompetenciesSelectFrameworkViewModel(competencyAssessmentBase, linkedFrameworks); + return View(model); + } + [HttpPost] + [Route("/CompetencyAssessments/{competencyAssessmentId}/Competencies/Add/SelectFramework")] + public IActionResult AddCompetenciesSelectFramework(AddCompetenciesSelectFrameworkFormData formdata) + { + if (!ModelState.IsValid) + { + var competencyAssessmentId = formdata.ID; + var linkedFrameworks = competencyAssessmentService.GetLinkedFrameworksForCompetencyAssessment(competencyAssessmentId); + var adminId = GetAdminID(); + var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId); + var model = new AddCompetenciesSelectFrameworkViewModel(competencyAssessmentBase, linkedFrameworks); + model.FrameworkId = formdata.FrameworkId; + return View("AddCompetenciesSelectFramework", model); + } + else + { + return RedirectToAction("AddCompetencies", new { competencyAssessmentId = formdata.ID, frameworkId = formdata.FrameworkId }); + } + } + [Route("/CompetencyAssessments/{competencyAssessmentId}/Competencies/Add/{frameworkId}")] + public IActionResult AddCompetencies(int competencyAssessmentId, int frameworkId) + { + var adminId = GetAdminID(); + var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId); + if (competencyAssessmentBase == null) + { + logger.LogWarning($"Failed to load Competencies page for competencyAssessmentId: {competencyAssessmentId} adminId: {adminId}"); + return StatusCode(500); + } + if (competencyAssessmentBase.UserRole < 2) + { + return StatusCode(403); + } + var framework = frameworkService.GetBaseFrameworkByFrameworkId(frameworkId, adminId); + var selectedFrameworkCompetencies = competencyAssessmentService.GetLinkedFrameworkCompetencyIds(competencyAssessmentId, frameworkId); + var groupedCompetencies = frameworkService.GetFrameworkCompetencyGroups(frameworkId, competencyAssessmentId); + var ungroupedCompetencies = frameworkService.GetFrameworkCompetenciesUngrouped(frameworkId, competencyAssessmentId); + var competencyIds = ungroupedCompetencies.Select(c => c.CompetencyID).ToArray(); + var competencyFlags = frameworkService.GetSelectedCompetencyFlagsByCompetecyIds(competencyIds); + foreach (var competency in ungroupedCompetencies) + competency.CompetencyFlags = competencyFlags.Where(f => f.CompetencyId == competency.CompetencyID); + foreach (var group in groupedCompetencies) + { + competencyIds = group.FrameworkCompetencies.Select(c => c.CompetencyID).ToArray(); + competencyFlags = frameworkService.GetSelectedCompetencyFlagsByCompetecyIds(competencyIds); + foreach (var competency in group.FrameworkCompetencies) + competency.CompetencyFlags = competencyFlags.Where(f => f.CompetencyId == competency.CompetencyID); + } + var model = new AddCompetenciesViewModel(competencyAssessmentBase, groupedCompetencies, ungroupedCompetencies, frameworkId, framework.FrameworkName, selectedFrameworkCompetencies); + return View(model); + } + [HttpPost] + [Route("/CompetencyAssessments/{competencyAssessmentId}/Competencies/Add/{frameworkId}")] + public IActionResult AddComptencies(AddCompetenciesFormData model, int competencyAssessmentId, int frameworkId) + { + if (!ModelState.IsValid) + { + //reload model and view + } + if (model.SelectedCompetencyIds != null) + { + competencyAssessmentService.InsertCompetenciesIntoAssessmentFromFramework(model.SelectedCompetencyIds, frameworkId, competencyAssessmentId); + } + competencyAssessmentService.UpdateSelectCompetenciesTaskStatus(competencyAssessmentId, false, null); + return RedirectToAction("ViewSelectedCompetencies", new { competencyAssessmentId }); + } + [Route("/CompetencyAssessments/{competencyAssessmentId}/Competencies/Delete/{competencyId}")] + public IActionResult DeleteCompetency(int competencyAssessmentId, int competencyId) + { + competencyAssessmentService.RemoveCompetencyFromAssessment(competencyAssessmentId, competencyId); + return RedirectToAction("ViewSelectedCompetencies", new { competencyAssessmentId }); + } + public IActionResult MoveCompetencyInSelfAssessment(int competencyAssessmentId, int competencyId, string direction) + { + var adminId = GetAdminID(); + var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId); + if (competencyAssessmentBase == null) + { + logger.LogWarning($"Failed to load Competencies page for competencyAssessmentId: {competencyAssessmentId} adminId: {adminId}"); + return StatusCode(500); + } + if (competencyAssessmentBase.UserRole < 2) + { + return StatusCode(403); + } + competencyAssessmentService.MoveCompetencyInSelfAssessment(competencyAssessmentId, competencyId, direction); + return new RedirectResult(Url.Action("ViewSelectedCompetencies", new { competencyAssessmentId }) + "#competency-" + competencyId.ToString()); + } + public IActionResult MoveCompetencyGroupInSelfAssessment(int competencyAssessmentId, int groupId, string direction) + { + var adminId = GetAdminID(); + var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId); + if (competencyAssessmentBase == null) + { + logger.LogWarning($"Failed to load Competencies page for competencyAssessmentId: {competencyAssessmentId} adminId: {adminId}"); + return StatusCode(500); + } + if (competencyAssessmentBase.UserRole < 2) + { + return StatusCode(403); + } + competencyAssessmentService.MoveCompetencyGroupInSelfAssessment(competencyAssessmentId, groupId, direction); + return new RedirectResult(Url.Action("ViewSelectedCompetencies", new { competencyAssessmentId }) + "#group-" + groupId.ToString()); + } + [HttpPost] + [Route("/CompetencyAssessments/{competencyAssessmentId}/Competencies")] + public IActionResult ViewSelectedCompetencies(ViewSelectedCompetenciesFormData model) + { + if (model.TaskStatus == null) + { + model.TaskStatus = false; + } + competencyAssessmentService.UpdateSelectCompetenciesTaskStatus(model.ID, model.TaskStatus.Value, null); + return RedirectToAction("ManageCompetencyAssessment", new { competencyAssessmentId = model.ID }); + } + + [Route("/CompetencyAssessments/Framework/{frameworkId}/{competencyAssessmentId}/Features")] + public IActionResult CompetencyAssessmentFeatures(int competencyAssessmentId, int? frameworkId = null) + { + + var adminId = GetAdminID(); + var data = GetcompetencyAssessmentFeaturesData(); + if (!string.IsNullOrEmpty(data.CompetencyAssessmentName)) return View(data); + var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId); + if (competencyAssessmentBase == null) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 500 }); + if (competencyAssessmentBase.UserRole < 2) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 }); + var baseModel = new CompetencyAssessmentFeaturesViewModel(competencyAssessmentBase.ID, + competencyAssessmentBase.CompetencyAssessmentName, + competencyAssessmentBase.UserRole, + frameworkId); + return View(baseModel); + } + [HttpPost] + [Route("/CompetencyAssessments/Framework/{frameworkId}/{competencyAssessmentId}/Features")] + public IActionResult CompetencyAssessmentFeatures(CompetencyAssessmentFeaturesViewModel featuresViewModel) + { + if (featuresViewModel == null) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 500 }); + SetcompetencyAssessmentFeaturesData(featuresViewModel); + return RedirectToAction("CompetencyAssessmentSummary", new { competencyAssessmentId = featuresViewModel.ID,featuresViewModel.FrameworkId }); + } + + [Route("/CompetencyAssessments/Framework/{frameworkId}/{competencyAssessmentId}/Summary")] + public IActionResult CompetencyAssessmentSummary(int competencyAssessmentId, int? frameworkId = null) + { + if (competencyAssessmentService.GetSelfAssessmentStructure(competencyAssessmentId) != 0) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 }); + if (competencyAssessmentId == 0) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 }); + var data = GetcompetencyAssessmentFeaturesData(); + if (data == null) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 500 }); + SetcompetencyAssessmentFeaturesData(data); + return View(data); + } + [HttpPost] + [Route("/CompetencyAssessments/Framework/{frameworkId}/{competencyAssessmentId}/Summary")] + public IActionResult CompetencyAssessmentSummary(CompetencyAssessmentFeaturesViewModel competency) + { + var data = GetcompetencyAssessmentFeaturesData(); + if (data.ID == 0) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 }); + if (competencyAssessmentService.GetSelfAssessmentStructure(data.ID) != 0) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 }); + var features = competencyAssessmentService.UpdateCompetencyAssessmentFeaturesTaskStatus(data.ID, + data.DescriptionStatus, + data.ProviderandCategoryStatus, + data.VocabularyStatus, + data.WorkingGroupStatus, + data.AllframeworkCompetenciesStatus); + if (!features) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 500 }); + competencyAssessmentService.UpdateSelfAssessmentFromFramework(data.ID , data.FrameworkId ); + var insertSelfAssessment = competencyAssessmentService.InsertSelfAssessmentStructure(data.ID, data.FrameworkId); + if (!insertSelfAssessment) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 500 }); + multiPageFormService.ClearMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("AssessmentFeaturesDataCWF"), TempData); + TempData.Clear(); + return RedirectToAction("ManageCompetencyAssessment", new { competencyAssessmentId = competency.ID, competency.FrameworkId }); + } + + private void SetcompetencyAssessmentFeaturesData(CompetencyAssessmentFeaturesViewModel data) + { + multiPageFormService.SetMultiPageFormData( + data, + MultiPageFormDataFeature.AddCustomWebForm("AssessmentFeaturesDataCWF"), + TempData + ); + } + + private CompetencyAssessmentFeaturesViewModel GetcompetencyAssessmentFeaturesData() + { + var data = multiPageFormService.GetMultiPageFormData( + MultiPageFormDataFeature.AddCustomWebForm("AssessmentFeaturesDataCWF"), + TempData + ).GetAwaiter().GetResult(); + return data; + } } } diff --git a/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessmentsController.cs b/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessmentsController.cs index c057b43776..a07a957ee0 100644 --- a/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessmentsController.cs +++ b/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessmentsController.cs @@ -2,6 +2,7 @@ { using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Web.Services; + using GDS.MultiPageFormData; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -16,13 +17,15 @@ public partial class CompetencyAssessmentsController : Controller private readonly IFrameworkNotificationService frameworkNotificationService; private readonly ILogger logger; private readonly IConfiguration config; + private readonly IMultiPageFormService multiPageFormService; public CompetencyAssessmentsController( ICompetencyAssessmentService competencyAssessmentService, IFrameworkService frameworkService, ICommonService commonService, IFrameworkNotificationService frameworkNotificationService, ILogger logger, - IConfiguration config) + IConfiguration config, + IMultiPageFormService multiPageFormService) { this.competencyAssessmentService = competencyAssessmentService; this.frameworkService = frameworkService; @@ -30,6 +33,7 @@ public CompetencyAssessmentsController( this.frameworkNotificationService = frameworkNotificationService; this.logger = logger; this.config = config; + this.multiPageFormService = multiPageFormService; } public IActionResult Index() { diff --git a/DigitalLearningSolutions.Web/Controllers/FrameworksController/AssessmentQuestions.cs b/DigitalLearningSolutions.Web/Controllers/FrameworksController/AssessmentQuestions.cs index f4ffce91d1..519c8881bd 100644 --- a/DigitalLearningSolutions.Web/Controllers/FrameworksController/AssessmentQuestions.cs +++ b/DigitalLearningSolutions.Web/Controllers/FrameworksController/AssessmentQuestions.cs @@ -456,6 +456,7 @@ public IActionResult EditAssessmentQuestionOptions(AssessmentQuestionDetail asse return RedirectToAction("EditAssessmentQuestionOptions", "Frameworks", new { frameworkId, assessmentQuestionId, frameworkCompetencyId }); } assessmentQuestionDetail.ScoringInstructions = SanitizerHelper.SanitizeHtmlData(assessmentQuestionDetail.ScoringInstructions); + if (string.IsNullOrWhiteSpace(StringHelper.StripHtmlTags(assessmentQuestionDetail?.ScoringInstructions))) { assessmentQuestionDetail.ScoringInstructions = null; } SessionAssessmentQuestion sessionAssessmentQuestion = multiPageFormService .GetMultiPageFormData(MultiPageFormDataFeature.EditAssessmentQuestion, TempData) .GetAwaiter() diff --git a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Competencies.cs b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Competencies.cs index 0d26aee6a3..19a918296f 100644 --- a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Competencies.cs +++ b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Competencies.cs @@ -65,13 +65,14 @@ public IActionResult AddEditFrameworkCompetencyGroup(int frameworkId, Competency var adminId = GetAdminId(); var userRole = frameworkService.GetAdminUserRoleForFrameworkId(adminId, frameworkId); if (userRole < 2) return StatusCode(403); + if (string.IsNullOrWhiteSpace(StringHelper.StripHtmlTags(competencyGroupBase?.Description))) { competencyGroupBase.Description = null; } if (competencyGroupBase.ID > 0) { - frameworkService.UpdateFrameworkCompetencyGroup(frameworkCompetencyGroupId, competencyGroupBase.CompetencyGroupID, competencyGroupBase.Name, SanitizerHelper.SanitizeHtmlData + frameworkService.UpdateFrameworkCompetencyGroup(frameworkCompetencyGroupId, competencyGroupBase.CompetencyGroupID, competencyGroupBase.Name.Trim(), SanitizerHelper.SanitizeHtmlData (competencyGroupBase.Description), adminId); return new RedirectResult(Url.Action("ViewFramework", new { tabname = "Structure", frameworkId }) + "#fcgroup-" + frameworkCompetencyGroupId.ToString()); } - var newCompetencyGroupId = frameworkService.InsertCompetencyGroup(competencyGroupBase.Name, SanitizerHelper.SanitizeHtmlData(competencyGroupBase.Description), adminId, frameworkId); + var newCompetencyGroupId = frameworkService.InsertCompetencyGroup(competencyGroupBase.Name.Trim(), SanitizerHelper.SanitizeHtmlData(competencyGroupBase.Description), adminId, frameworkId); if (newCompetencyGroupId > 0) { var newFrameworkCompetencyGroupId = frameworkService.InsertFrameworkCompetencyGroup(newCompetencyGroupId, frameworkId, adminId); @@ -102,7 +103,7 @@ public IActionResult DeleteFrameworkCompetencyGroup(int frameworkId, int compete var adminId = GetAdminId(); - frameworkService.DeleteFrameworkCompetencyGroup(frameworkCompetencyGroupId, competencyGroupId, adminId); + frameworkService.DeleteFrameworkCompetencyGroup(frameworkCompetencyGroupId, competencyGroupId, frameworkId, adminId); return new RedirectResult(Url.Action("ViewFramework", new { tabname = "Structure", frameworkId, frameworkCompetencyGroupId })); } @@ -141,14 +142,14 @@ public IActionResult AddEditFrameworkCompetency(int frameworkId, FrameworkCompet frameworkCompetency.Description?.Trim(); var description = HttpUtility.HtmlDecode(HttpUtility.HtmlDecode(frameworkCompetency.Description)); var detailFramework = frameworkService.GetDetailFrameworkByFrameworkId(frameworkId, GetAdminId()); - if (string.IsNullOrWhiteSpace(description)) { frameworkCompetency.Description = null; } + if (string.IsNullOrWhiteSpace(StringHelper.StripHtmlTags(description))) { frameworkCompetency.Description = null; } if (!ModelState.IsValid) { ModelState.Remove(nameof(FrameworkCompetency.Name)); ModelState.AddModelError(nameof(FrameworkCompetency.Name), "Please enter a valid competency statement (between 3 and 500 characters)"); var competencyFlags = frameworkService.GetCompetencyFlagsByFrameworkId(frameworkId, frameworkCompetency?.CompetencyID).ToList(); if (competencyFlags != null) - competencyFlags.ForEach(f => f.Selected = selectedFlagIds.Contains(f.FlagId)); + competencyFlags.ForEach(f => f.Selected = selectedFlagIds != null ? selectedFlagIds.Contains(f.FlagId) : false); if (detailFramework == null) return StatusCode((int)HttpStatusCode.NotFound); var model = new FrameworkCompetencyViewModel() @@ -158,6 +159,7 @@ public IActionResult AddEditFrameworkCompetency(int frameworkId, FrameworkCompet FrameworkCompetency = frameworkCompetency, CompetencyFlags = competencyFlags }; + ModelState.Remove("SearchableName"); return View("Developer/Competency", model); } var adminId = GetAdminId(); @@ -167,7 +169,7 @@ public IActionResult AddEditFrameworkCompetency(int frameworkId, FrameworkCompet { - frameworkService.UpdateFrameworkCompetency(frameworkCompetencyId, frameworkCompetency.Name, frameworkCompetency.Description, adminId); + frameworkService.UpdateFrameworkCompetency(frameworkCompetencyId, frameworkCompetency.Name.Trim(), frameworkCompetency.Description, adminId); frameworkService.UpdateCompetencyFlags(frameworkId, frameworkCompetency.CompetencyID, selectedFlagIds); return new RedirectResult(Url.Action("ViewFramework", new { tabname = "Structure", frameworkId, frameworkCompetencyGroupId, frameworkCompetencyId }) + "#fc-" + frameworkCompetencyId.ToString()); } @@ -214,7 +216,7 @@ public IActionResult AddDuplicateCompetency(int frameworkId, string competencyNa private IActionResult SaveCompetency(int adminId, int frameworkId, FrameworkCompetency frameworkCompetency, int frameworkCompetencyId, int? frameworkCompetencyGroupId, int[]? selectedFlagIds) { - var newCompetencyId = frameworkService.InsertCompetency(frameworkCompetency.Name, frameworkCompetency.Description, adminId); + var newCompetencyId = frameworkService.InsertCompetency(frameworkCompetency.Name.Trim(), frameworkCompetency.Description, adminId); if (newCompetencyId > 0) { var newFrameworkCompetencyId = frameworkService.InsertFrameworkCompetency(newCompetencyId, frameworkCompetencyGroupId, adminId, frameworkId); @@ -235,7 +237,7 @@ public IActionResult DeleteFrameworkCompetency(int frameworkId, int frameworkCom { var userRole = frameworkService.GetAdminUserRoleForFrameworkId(GetAdminId(), frameworkId); if (userRole < 2) return StatusCode(403); - frameworkService.DeleteFrameworkCompetency(frameworkCompetencyId, GetAdminId()); + frameworkService.DeleteFrameworkCompetency(frameworkCompetencyId, frameworkCompetencyGroupId, frameworkId, GetAdminId()); return frameworkCompetencyGroupId != null ? new RedirectResult(Url.Action("ViewFramework", new { tabname = "Structure", frameworkId, frameworkCompetencyGroupId }) + "#fcgroup-" + frameworkCompetencyGroupId.ToString()) : new RedirectResult(Url.Action("ViewFramework", new { tabname = "Structure", frameworkId }) + "#fc-ungrouped"); } [Route("/Frameworks/{frameworkId}/Competency/{frameworkCompetencyGroupId:int=0}/{frameworkCompetencyId}/Preview/")] diff --git a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Frameworks.cs b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Frameworks.cs index b2f67f73e5..3d3a85d9cf 100644 --- a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Frameworks.cs +++ b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Frameworks.cs @@ -582,14 +582,33 @@ public IActionResult EditFrameworkFlag(CustomFlagViewModel model, int frameworkI { if (ModelState.IsValid) { + var flags = frameworkService.GetCustomFlagsByFrameworkId(frameworkId, null) + .Where(fn => fn.FlagName?.Trim().ToLower() == model.FlagName?.Trim().ToLower()).ToList(); + + bool nameExists = flags.Any(x => x.FlagName?.Trim().ToLower() == model.FlagName?.Trim().ToLower()); + bool idExists = flags.Any(x => x.FlagId == flagId); + if (actionName == "Edit") { - frameworkService.UpdateFrameworkCustomFlag(frameworkId, model.Id, model.FlagName, model.FlagGroup, model.FlagTagClass); + if (nameExists && !idExists) + { + ModelState.AddModelError(nameof(model.FlagName), "A custom flag already exists."); + return View("Developer/EditCustomFlag", model); + } + else + frameworkService.UpdateFrameworkCustomFlag(frameworkId, model.Id, model.FlagName?.Trim(), model.FlagGroup?.Trim(), model.FlagTagClass); } else { - frameworkService.AddCustomFlagToFramework(frameworkId, model.FlagName, model.FlagGroup, model.FlagTagClass); + if (nameExists) + { + ModelState.AddModelError(nameof(model.FlagName), "A custom flag already exists."); + return View("Developer/EditCustomFlag", model); + } + else + frameworkService.AddCustomFlagToFramework(frameworkId, model.FlagName?.Trim(), model.FlagGroup?.Trim(), model.FlagTagClass); } + return RedirectToAction("EditFrameworkFlags", "Frameworks", new { frameworkId }); } return View("Developer/EditCustomFlag", model); @@ -704,9 +723,9 @@ public IActionResult ViewFramework(string tabname, int frameworkId, int? framewo model.TabNavLinks = new TabsNavViewModel(FrameworkTab.Details, routeData); break; case "Structure": - model.FrameworkCompetencyGroups = frameworkService.GetFrameworkCompetencyGroups(frameworkId).ToList(); + model.FrameworkCompetencyGroups = frameworkService.GetFrameworkCompetencyGroups(frameworkId, null).ToList(); model.CompetencyFlags = frameworkService.GetCompetencyFlagsByFrameworkId(frameworkId, null, selected: true); - model.FrameworkCompetencies = frameworkService.GetFrameworkCompetenciesUngrouped(frameworkId); + model.FrameworkCompetencies = frameworkService.GetFrameworkCompetenciesUngrouped(frameworkId, null); model.TabNavLinks = new TabsNavViewModel(FrameworkTab.Structure, routeData); break; case "Comments": @@ -718,7 +737,8 @@ public IActionResult ViewFramework(string tabname, int frameworkId, int? framewo } [Route("/Framework/{frameworkId}/Structure/PrintLayout")] - public IActionResult PrintLayout(int frameworkId) { + public IActionResult PrintLayout(int frameworkId) + { var adminId = GetAdminId(); var detailFramework = frameworkService.GetFrameworkDetailByFrameworkId(frameworkId, adminId); var routeData = new Dictionary { { "frameworkId", detailFramework?.ID.ToString() } }; @@ -726,9 +746,9 @@ public IActionResult PrintLayout(int frameworkId) { { DetailFramework = detailFramework, }; - model.FrameworkCompetencyGroups = frameworkService.GetFrameworkCompetencyGroups(frameworkId).ToList(); + model.FrameworkCompetencyGroups = frameworkService.GetFrameworkCompetencyGroups(frameworkId, null).ToList(); model.CompetencyFlags = frameworkService.GetCompetencyFlagsByFrameworkId(frameworkId, null, selected: true); - model.FrameworkCompetencies = frameworkService.GetFrameworkCompetenciesUngrouped(frameworkId); + model.FrameworkCompetencies = frameworkService.GetFrameworkCompetenciesUngrouped(frameworkId, null); return View("Developer/FrameworkPrintLayout", model); } [ResponseCache(CacheProfileName = "Never")] diff --git a/DigitalLearningSolutions.Web/Controllers/FrameworksController/ImportCompetencies.cs b/DigitalLearningSolutions.Web/Controllers/FrameworksController/ImportCompetencies.cs index 752befe216..e272f6da1e 100644 --- a/DigitalLearningSolutions.Web/Controllers/FrameworksController/ImportCompetencies.cs +++ b/DigitalLearningSolutions.Web/Controllers/FrameworksController/ImportCompetencies.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Linq.Expressions; namespace DigitalLearningSolutions.Web.Controllers.FrameworksController { @@ -45,7 +46,13 @@ public IActionResult DownloadCompetencies(int frameworkId, int DownloadOption, s public IActionResult StartImport(ImportCompetenciesFormData model, int frameworkId, string tabname, bool isNotBlank) { if (!ModelState.IsValid) - return View("Developer/Import/Index", model); + { + var adminId = GetAdminId(); + var framework = frameworkService.GetFrameworkDetailByFrameworkId(frameworkId, adminId); + var viewModel = new ImportCompetenciesViewModel(framework, isNotBlank); + viewModel.ImportFile = model.ImportFile; + return View("Developer/Import/Index", viewModel); + } try { var adminUserID = User.GetAdminIdKnownNotNull(); @@ -73,7 +80,7 @@ public IActionResult StartImport(ImportCompetenciesFormData model, int framework } catch (InvalidHeadersException) { - return View("Developer/Import/ImportFailed"); + return RedirectToAction("ImportFailed", "Frameworks", new { frameworkId, tabname, isNotBlank }); } } [Route("/Framework/{frameworkId}/{tabname}/Import/Uploaded")] @@ -97,9 +104,18 @@ public IActionResult ImportCompleted() catch (InvalidHeadersException) { FileHelper.DeleteFile(webHostEnvironment, data.CompetenciesFileName); - return View("Developer/Import/ImportFailed"); + return RedirectToAction("ImportFailed", "Frameworks", new { data.FrameworkId, tabname = "Structure", data.IsNotBlank }); } } + [Route("/Framework/{frameworkId}/{tabname}/Import/Failed")] + public IActionResult ImportFailed(int frameworkId, string tabname, bool isNotBlank) + { + var adminId = GetAdminId(); + var framework = frameworkService.GetFrameworkDetailByFrameworkId(frameworkId, adminId); + var viewModel = new ImportCompetenciesViewModel(framework, isNotBlank); + return View("Developer/Import/ImportFailed", viewModel); + } + [Route("/Framework/{frameworkId}/{tabname}/Import/Ordering")] public IActionResult ApplyCompetencyOrdering() { @@ -165,15 +181,16 @@ public IActionResult AddAssessmentQuestions() public IActionResult AddAssessmentQuestions(AddAssessmentQuestionsFormData model) { var data = GetBulkUploadData(); - data.AddDefaultAssessmentQuestions = model.AddDefaultAssessmentQuestions; + if (model.AddDefaultAssessmentQuestions) { - data.DefaultQuestionIDs = model.DefaultAssessmentQuestionIDs; + data.DefaultQuestionIDs = model.DefaultAssessmentQuestionIDs ?? []; } else { data.DefaultQuestionIDs = []; } + data.AddDefaultAssessmentQuestions = (data.DefaultQuestionIDs.Count > 0 && model.AddDefaultAssessmentQuestions); data.AddCustomAssessmentQuestion = model.AddCustomAssessmentQuestion; if (model.AddCustomAssessmentQuestion) { @@ -183,9 +200,8 @@ public IActionResult AddAssessmentQuestions(AddAssessmentQuestionsFormData model { data.CustomAssessmentQuestionID = null; } - if (data.CompetenciesToUpdateCount > 0) + if (data.CompetenciesToUpdateCount > 0 && (data.DefaultQuestionIDs.Count + (data.CustomAssessmentQuestionID != null ? 1 : 0) > 0)) { - data.AddAssessmentQuestionsOption = 2; setBulkUploadData(data); return RedirectToAction("AddQuestionsToWhichCompetencies", "Frameworks", new { frameworkId = data.FrameworkId, tabname = data.TabName }); } @@ -230,6 +246,7 @@ public IActionResult AddQuestionsToWhichCompetencies(int AddAssessmentQuestionsO [Route("/Framework/{frameworkId}/{tabname}/Import/Summary")] public IActionResult ImportSummary() { + if (!TempData.Any()) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 }); var data = GetBulkUploadData(); var model = new ImportSummaryViewModel(data); return View("Developer/Import/ImportSummary", model); @@ -245,11 +262,6 @@ public IActionResult ImportSummarySubmit() var workbook = new XLWorkbook(filePath); var results = importCompetenciesFromFileService.ProcessCompetenciesFromFile(workbook, adminId, data.FrameworkId, data.FrameworkVocubulary, data.ReorderCompetenciesOption, data.AddAssessmentQuestionsOption, data.AddCustomAssessmentQuestion ? (int)data.CustomAssessmentQuestionID : 0, data.AddDefaultAssessmentQuestions ? data.DefaultQuestionIDs : []); data.ImportCompetenciesResult = results; - //TO DO apply ordering changes if required: - if (data.ReorderCompetenciesOption == 2 && data.CompetenciesToReorderCount > 0) - { - - } setBulkUploadData(data); return RedirectToAction("UploadResults", "Frameworks", new { frameworkId = data.FrameworkId, tabname = data.TabName }); } @@ -263,12 +275,25 @@ public IActionResult UploadResults() return View("Developer/Import/UploadResults", model); } [Route("CancelImport")] - public IActionResult CancelImport() + public IActionResult CancelImport(int? frameworkId) { - var data = GetBulkUploadData(); - var frameworkId = data.FrameworkId; - FileHelper.DeleteFile(webHostEnvironment, data.CompetenciesFileName); - TempData.Clear(); + try + { + var data = GetBulkUploadData(); + frameworkId = data.FrameworkId; + if (!string.IsNullOrWhiteSpace(data.CompetenciesFileName)) + { + FileHelper.DeleteFile(webHostEnvironment, data.CompetenciesFileName); + } + } + catch + { + + } + finally + { + TempData.Clear(); + } return RedirectToAction("ViewFramework", new { frameworkId, tabname = "Structure" }); } private void setupBulkUploadData(int frameworkId, int adminUserID, string competenciessFileName, string tabName, bool isNotBlank) diff --git a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Review.cs b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Review.cs index 16c2665150..8ecf417de2 100644 --- a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Review.cs +++ b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Review.cs @@ -63,6 +63,7 @@ public IActionResult LoadReview(int frameworkId, int reviewId) { var adminId = GetAdminId(); var framework = frameworkService.GetBaseFrameworkByFrameworkId(frameworkId, adminId); + if (framework.FrameworkReviewID == 0 || framework.FrameworkReviewID == null) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 }); if (framework == null) return StatusCode(404); if (framework.UserRole < 1) return StatusCode(403); var frameworkName = framework.FrameworkName; @@ -81,8 +82,27 @@ public IActionResult LoadReview(int frameworkId, int reviewId) public IActionResult SubmitFrameworkReview(int frameworkId, int reviewId, string? comment, bool signedOff) { var adminId = GetAdminId(); + var framework = frameworkService.GetBaseFrameworkByFrameworkId(frameworkId, adminId); + if (framework.FrameworkReviewID == 0 || framework.FrameworkReviewID == null) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 }); int? commentId = null; - if (!string.IsNullOrWhiteSpace(comment)) commentId = frameworkService.InsertComment(frameworkId, adminId, comment, null); + if (string.IsNullOrWhiteSpace(comment)) + { + ModelState.AddModelError("comment", "Please enter comment"); + var frameworkReview = frameworkService.GetFrameworkReview(frameworkId, adminId, reviewId); + frameworkReview.SignedOff = signedOff; + var model = new SubmitReviewViewModel() + { + FrameworkId = frameworkId, + FrameworkName = framework.FrameworkName, + FrameworkReview = frameworkReview + }; + return View("Developer/SubmitReview", model); + } + else + { + commentId = frameworkService.InsertComment(frameworkId, adminId, comment, null); + } + frameworkService.SubmitFrameworkReview(frameworkId, reviewId, signedOff, commentId); frameworkNotificationService.SendReviewOutcomeNotification(reviewId, User.GetCentreIdKnownNotNull()); return RedirectToAction("ViewFramework", "Frameworks", new { frameworkId, tabname = "Structure" }); diff --git a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Signposting.cs b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Signposting.cs index 9acf3bd6c9..d994dee7a9 100644 --- a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Signposting.cs +++ b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Signposting.cs @@ -107,6 +107,13 @@ public IActionResult ConfirmAddCompetencyLearningResourceSummary(CompetencyResou { var frameworkCompetency = frameworkService.GetFrameworkCompetencyById(model.FrameworkCompetencyId.Value); string plainTextDescription = DisplayStringHelper.RemoveMarkup(model.Description); + var competencyLearningResource = competencyLearningResourcesService.GetActiveCompetencyLearningResourcesByCompetencyIdAndReferenceId(frameworkCompetency.CompetencyID, model.ReferenceId); + if (competencyLearningResource.Any()) + { + ModelState.Clear(); + ModelState.AddModelError("LearningResourceExists", "This learning resource is already signposted to the selected competency."); + return View("Developer/AddCompetencyLearningResourceSummary", model); + } int competencyLearningResourceId = competencyLearningResourcesService.AddCompetencyLearningResource(model.ReferenceId, model.ResourceName, plainTextDescription, model.ResourceType, model.Link, model.SelectedCatalogue, model.Rating.Value, frameworkCompetency.CompetencyID, GetAdminId()); return RedirectToAction("StartSignpostingParametersSession", "Frameworks", new { model.FrameworkId, model.FrameworkCompetencyId, model.FrameworkCompetencyGroupId, competencyLearningResourceId }); } diff --git a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/Available.cs b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/Available.cs index 6e8a5a3d90..b9e16e7ac6 100644 --- a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/Available.cs +++ b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/Available.cs @@ -4,8 +4,10 @@ using DigitalLearningSolutions.Data.Models.SearchSortFilterPaginate; using DigitalLearningSolutions.Web.Attributes; using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.Services; using DigitalLearningSolutions.Web.ViewModels.LearningPortal.Available; using Microsoft.AspNetCore.Mvc; + using System; using System.Linq; public partial class LearningPortalController @@ -58,8 +60,41 @@ public IActionResult AllAvailableItems() public IActionResult EnrolOnSelfAssessment(int selfAssessmentId) { + var selfAssessment = selfAssessmentService.GetSelfAssessmentRetirementDateById(selfAssessmentId); + if(CheckRetirementDate(selfAssessment.RetirementDate)) return RedirectToAction("ConfirmRetirement", new { selfAssessmentId }); courseService.EnrolOnSelfAssessment(selfAssessmentId, User.GetUserIdKnownNotNull(), User.GetCentreIdKnownNotNull()); return RedirectToAction("SelfAssessment", new { selfAssessmentId }); } + + [Route("/LearningPortal/Retirement/{selfAssessmentId:int}/confirm")] + public IActionResult ConfirmRetirement(int selfAssessmentId) + { + var selfAssessment = selfAssessmentService.GetSelfAssessmentRetirementDateById(selfAssessmentId); + var model = new RetirementViewModel(selfAssessmentId, selfAssessment.RetirementDate, selfAssessment.Name); + return View("Available/ConfirmRetirement", model); + } + [HttpPost] + [Route("/LearningPortal/Retirement/{selfAssessmentId:int}/confirm")] + public IActionResult ConfirmRetirement(RetirementViewModel retirementViewModel) + { + if (!ModelState.IsValid && !retirementViewModel.ActionConfirmed) + { + var selfAssessment = selfAssessmentService.GetSelfAssessmentRetirementDateById(retirementViewModel.SelfAssessmentId); + var model = new RetirementViewModel(retirementViewModel.SelfAssessmentId , selfAssessment.RetirementDate, selfAssessment.Name); + return View("Available/ConfirmRetirement", model); + } + var date = selfAssessmentService.GetSelfAssessmentRetirementDateById(retirementViewModel.SelfAssessmentId); + courseService.EnrolOnSelfAssessment(retirementViewModel.SelfAssessmentId, User.GetUserIdKnownNotNull(), User.GetCentreIdKnownNotNull()); + return RedirectToAction("SelfAssessment", new { retirementViewModel.SelfAssessmentId }); + } + private bool CheckRetirementDate(DateTime? date) + { + if (date == null) + return false; + + DateTime twoWeeksbeforeRetirementdate = DateTime.Today.AddDays(14); + DateTime today = DateTime.Today; + return (date >= today && date <= twoWeeksbeforeRetirementdate); + } } } diff --git a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs index 4947c6bd75..aa3153c81d 100644 --- a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs +++ b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs @@ -28,7 +28,7 @@ using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using System.IO; - + [ServiceFilter(typeof(RequireProcessAgreementFilter))] public partial class LearningPortalController { private const string CookieName = "DLSSelfAssessmentService"; @@ -75,9 +75,56 @@ public IActionResult SelfAssessment(int selfAssessmentId) delegateUserId ).ToList(); var model = new SelfAssessmentDescriptionViewModel(selfAssessment, supervisors); + return View("SelfAssessments/SelfAssessmentDescription", model); } + [Route("/LearningPortal/SelfAssessment/{selfAssessmentId:int}/AgreeProcess")] + public IActionResult AgreeSelfAssessmentProcess(int selfAssessmentId) + { + var delegateUserId = User.GetUserIdKnownNotNull(); + var selfAssessment = selfAssessmentService.GetSelfAssessmentForCandidateById(delegateUserId, selfAssessmentId); + + if (selfAssessment == null) + { + logger.LogWarning( + $"Attempt to display self assessment process for user {delegateUserId} with no self assessment" + ); + return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 }); + } + + var processmodel = new SelfAssessmentProcessViewModel() + { + SelfAssessmentID = selfAssessmentId, + Vocabulary = selfAssessment.Vocabulary, + VocabPlural = FrameworkVocabularyHelper.VocabularyPlural(selfAssessment.Vocabulary) + }; + return View("SelfAssessments/AgreeSelfAssessmentProcess", processmodel); + } + + [HttpPost("/LearningPortal/SelfAssessment/{selfAssessmentId:int}/AgreeProcess")] + public IActionResult ProcessAgreed(SelfAssessmentProcessViewModel model) + { + if (!ModelState.IsValid) + { + return View("SelfAssessments/AgreeSelfAssessmentProcess", model); + } + var delegateUserId = User.GetUserIdKnownNotNull(); + int selfAssessmentId = model.SelfAssessmentID; + var selfAssessment = selfAssessmentService.GetSelfAssessmentForCandidateById(delegateUserId, selfAssessmentId); + if (selfAssessment == null) + { + logger.LogWarning( + $"Attempt to display self assessment description for user {delegateUserId} with no self assessment" + ); + return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 }); + } + + selfAssessmentService.MarkProgressAgreed(selfAssessmentId, delegateUserId); + return RedirectToAction("SelfAssessment", new { selfAssessmentId }); + + } + [ServiceFilter(typeof(IsCentreAuthorizedSelfAssessment))] [Route("/LearningPortal/SelfAssessment/{selfAssessmentId:int}/{competencyNumber:int}")] public IActionResult SelfAssessmentCompetency(int selfAssessmentId, int competencyNumber) @@ -1532,22 +1579,6 @@ ManageOptionalCompetenciesViewModel model ); } } - var optionalCompetency = - (selfAssessmentService.GetCandidateAssessmentOptionalCompetencies(selfAssessmentId, delegateUserId)).Where(x => !x.IncludedInSelfAssessment); - if (optionalCompetency.Any()) - { - foreach (var optinal in optionalCompetency) - { - var selfAssessmentResults = selfAssessmentService.GetSelfAssessmentResultswithSupervisorVerificationsForDelegateSelfAssessmentCompetency(delegateUserId, selfAssessmentId, optinal.Id); - if (selfAssessmentResults.Any()) - { - foreach (var item in selfAssessmentResults) - { - selfAssessmentService.RemoveReviewCandidateAssessmentOptionalCompetencies(item.Id); - } - } - } - } if (model.GroupOptionalCompetenciesChecked != null) { var optionalCompetencies = @@ -1566,7 +1597,30 @@ ManageOptionalCompetenciesViewModel model } - if (!selfAssessmentService.HasMinimumOptionalCompetencies(selfAssessmentId, delegateUserId)) + var optionalCompetency = + (selfAssessmentService.GetCandidateAssessmentOptionalCompetencies(selfAssessmentId, delegateUserId)).Where(x => !x.IncludedInSelfAssessment); + if (optionalCompetency.Any()) + { + foreach (var optinal in optionalCompetency) + { + var selfAssessmentResults = selfAssessmentService.GetSelfAssessmentResultswithSupervisorVerificationsForDelegateSelfAssessmentCompetency(delegateUserId, selfAssessmentId, optinal.Id); + if (selfAssessmentResults.Any()) + { + foreach (var item in selfAssessmentResults) + { + selfAssessmentService.RemoveReviewCandidateAssessmentOptionalCompetencies(item.Id); + } + } + } + } + + var recentResults = selfAssessmentService.GetMostRecentResults(selfAssessmentId, User.GetCandidateIdKnownNotNull()).ToList(); + + bool isVerificationPending = recentResults?.SelectMany(comp => comp.AssessmentQuestions).Where(quest => quest.Required) + .Where(quest => quest.Required) + .All(quest => !((quest.Result == null || quest.Verified == null || quest.SignedOff != true) && quest.Required)) != true; + + if (!selfAssessmentService.HasMinimumOptionalCompetencies(selfAssessmentId, delegateUserId) || isVerificationPending) { var supervisorsSignOffs = selfAssessmentService.GetSupervisorSignOffsForCandidateAssessment(selfAssessmentId, delegateUserId); diff --git a/DigitalLearningSolutions.Web/Controllers/LoginController.cs b/DigitalLearningSolutions.Web/Controllers/LoginController.cs index 20280e18a8..80984a593b 100644 --- a/DigitalLearningSolutions.Web/Controllers/LoginController.cs +++ b/DigitalLearningSolutions.Web/Controllers/LoginController.cs @@ -82,6 +82,14 @@ public async Task Index(LoginViewModel model, string timeZone = " var loginResult = loginService.AttemptLogin(model.Username!.Trim(), model.Password!); + if (loginResult.LoginAttemptResult == LoginAttemptResult.LogIntoSingleCentre || + loginResult.LoginAttemptResult == LoginAttemptResult.ChooseACentre || + loginResult.LoginAttemptResult == LoginAttemptResult.UnverifiedEmail) + { + loginService.UpdateLastAccessedForUsersTable(loginResult.UserEntity.UserAccount.Id); + } + + switch (loginResult.LoginAttemptResult) { case LoginAttemptResult.InvalidCredentials: @@ -219,11 +227,17 @@ int centreIdToLogInto IsPersistent = rememberMe, IssuedUtc = clockUtility.UtcNow, }; + var centreAccountSet = userEntity?.GetCentreAccountSet(centreIdToLogInto); - var adminAccount = userEntity!.GetCentreAccountSet(centreIdToLogInto)?.AdminAccount; + if (centreAccountSet?.DelegateAccount?.Id != null) + { + loginService.UpdateLastAccessedForDelegatesAccountsTable(centreAccountSet.DelegateAccount.Id); + } + var adminAccount = centreAccountSet?.AdminAccount; if (adminAccount?.Active == true) { + loginService.UpdateLastAccessedForAdminAccountsTable(adminAccount.Id); sessionService.StartAdminSession(adminAccount.Id); } diff --git a/DigitalLearningSolutions.Web/Controllers/SuperAdmin/Centres/CentresController.cs b/DigitalLearningSolutions.Web/Controllers/SuperAdmin/Centres/CentresController.cs index 7d55d97729..8781d525bf 100644 --- a/DigitalLearningSolutions.Web/Controllers/SuperAdmin/Centres/CentresController.cs +++ b/DigitalLearningSolutions.Web/Controllers/SuperAdmin/Centres/CentresController.cs @@ -140,7 +140,7 @@ public IActionResult Index( { var baseUrl = config.GetAppRootPath(); var supportEmail = this.configService.GetConfigValue("SupportEmail"); - baseUrl = baseUrl+"/RegisterAdmin?centreId={centreId}".Replace("{centreId}", item.Centre.CentreId.ToString()); + baseUrl = baseUrl + "/RegisterAdmin?centreId={centreId}".Replace("{centreId}", item.Centre.CentreId.ToString()); Email welcomeEmail = this.passwordResetService.GenerateEmailInviteForCentreManager(centreEntity.Centre.CentreName, centreEntity.Centre.AutoRegisterManagerEmail, baseUrl, supportEmail); centreEntity.Centre.EmailInvite = "mailto:" + string.Join(",", welcomeEmail.To) + "?subject=" + welcomeEmail.Subject + "&body=" + welcomeEmail.Body.TextBody.Replace("&", "%26"); } @@ -295,7 +295,7 @@ public IActionResult EditCentreDetails(EditCentreDetailsSuperAdminViewModel mode model.CentreName.Trim(), model.CentreTypeId, model.RegionId, - model.CentreEmail, + model.RegistrationEmail, model.IpPrefix?.Trim(), model.ShowOnMap ); diff --git a/DigitalLearningSolutions.Web/Controllers/SupervisorController/Supervisor.cs b/DigitalLearningSolutions.Web/Controllers/SupervisorController/Supervisor.cs index 21b555391c..66e3c32a3c 100644 --- a/DigitalLearningSolutions.Web/Controllers/SupervisorController/Supervisor.cs +++ b/DigitalLearningSolutions.Web/Controllers/SupervisorController/Supervisor.cs @@ -27,7 +27,7 @@ public partial class SupervisorController { - public IActionResult Index() + public async Task IndexAsync() { var adminId = GetAdminId(); var dashboardData = supervisorService.GetDashboardDataForAdminId(adminId); @@ -35,11 +35,15 @@ public IActionResult Index() var reviewRequests = supervisorService.GetSupervisorDashboardToDoItemsForRequestedReviews(adminId); var supervisorDashboardToDoItems = Enumerable.Concat(signOffRequests, reviewRequests); var bannerText = GetBannerText(); + var tableauFlag = await featureManager.IsEnabledAsync(FeatureFlags.TableauSelfAssessmentDashboards); + var tableauQueryOverride = string.Equals(Request.Query["tableaulink"], "true", StringComparison.OrdinalIgnoreCase); + var showTableauLink = tableauFlag || tableauQueryOverride; var model = new SupervisorDashboardViewModel() { DashboardData = dashboardData, SupervisorDashboardToDoItems = supervisorDashboardToDoItems, - BannerText = bannerText + BannerText = bannerText, + ShowTableauLink = showTableauLink }; return View(model); } @@ -387,7 +391,7 @@ public IActionResult ReviewDelegateSelfAssessment(int supervisorDelegateId, int foreach (var competency in competencies) { competency.CompetencyFlags = flags.Where(f => f.CompetencyId == competency.Id); - }; + } if (superviseDelegate.DelegateUserID != null) { @@ -781,12 +785,22 @@ public IActionResult EnrolSetCompetencyAssessment(int supervisorDelegateId, int return View("EnrolDelegateOnProfileAssessment", model); } + if (sessionEnrolOnCompetencyAssessment.SelfAssessmentID != selfAssessmentID) + sessionEnrolOnCompetencyAssessment = new SessionEnrolOnCompetencyAssessment(); + sessionEnrolOnCompetencyAssessment.SelfAssessmentID = selfAssessmentID; multiPageFormService.SetMultiPageFormData( sessionEnrolOnCompetencyAssessment, MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment, TempData ); + + var retirementDate = selfAssessmentService.GetSelfAssessmentById(selfAssessmentID).RetirementDate; + if (CheckRetirementDate(retirementDate)) + { + return RedirectToAction("ConfirmRetiringSelfAssessment", "Supervisor", new { supervisorDelegateId }); + } + return RedirectToAction( "EnrolDelegateCompleteBy", "Supervisor", @@ -794,6 +808,60 @@ public IActionResult EnrolSetCompetencyAssessment(int supervisorDelegateId, int ); } + [Route("/Supervisor/Staff/{supervisorDelegateId}/ProfileAssessment/Enrol/Confirm")] + public IActionResult ConfirmRetiringSelfAssessment(int supervisorDelegateId) + { + var sessionEnrolOnRoleProfile = multiPageFormService.GetMultiPageFormData( + MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment, + TempData + ).GetAwaiter().GetResult(); + + var retirementDate = selfAssessmentService.GetSelfAssessmentById((int)sessionEnrolOnRoleProfile.SelfAssessmentID).RetirementDate; + if (!CheckRetirementDate((retirementDate))) + { + return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 }); + } + var model = new RetiringSelfAssessmentViewModel() + { + SelfAssessmentID = (int)sessionEnrolOnRoleProfile.SelfAssessmentID, + SupervisorDelegateID = supervisorDelegateId, + RetirementDate = retirementDate, + ActionConfirmed = sessionEnrolOnRoleProfile.ActionConfirmed + }; + return View("ConfirmRetiringSelfAssessment", model); + } + + [HttpPost] + [Route("/Supervisor/Staff/{supervisorDelegateId}/ProfileAssessment/Enrol/Confirm")] + public IActionResult ConfirmRetiringSelfAssessment(RetiringSelfAssessmentViewModel retiringSelfAssessment) + { + var sessionEnrolOnRoleProfile = multiPageFormService.GetMultiPageFormData( + MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment, + TempData + ).GetAwaiter().GetResult(); + + sessionEnrolOnRoleProfile.SelfAssessmentID = retiringSelfAssessment.SelfAssessmentID; + sessionEnrolOnRoleProfile.ActionConfirmed = retiringSelfAssessment.ActionConfirmed; + multiPageFormService.SetMultiPageFormData( + sessionEnrolOnRoleProfile, + MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment, + TempData + ); + + if (ModelState.IsValid && retiringSelfAssessment.ActionConfirmed) + { + return RedirectToAction( + "EnrolDelegateCompleteBy", + "Supervisor", + new { supervisorDelegateId = retiringSelfAssessment.SupervisorDelegateID } + ); + } + else + { + return View("ConfirmRetiringSelfAssessment", retiringSelfAssessment); + } + } + [Route("/Supervisor/Staff/{supervisorDelegateId}/ProfileAssessment/Enrol/CompleteBy")] [ResponseCache(CacheProfileName = "Never")] [TypeFilter( @@ -811,6 +879,12 @@ public IActionResult EnrolDelegateCompleteBy(int supervisorDelegateId, int? day, MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment, TempData ); + + var retirementDate = selfAssessmentService.GetSelfAssessmentById((int)sessionEnrolOnCompetencyAssessment.SelfAssessmentID).RetirementDate; + if (CheckRetirementDate(retirementDate) && !sessionEnrolOnCompetencyAssessment.ActionConfirmed) + { + return RedirectToAction("ConfirmRetiringSelfAssessment", "Supervisor", new { supervisorDelegateId }); + } var supervisorDelegate = supervisorService.GetSupervisorDelegateDetailsById(supervisorDelegateId, GetAdminId(), 0); var competencyAssessment = supervisorService.GetCompetencyAssessmentById((int)sessionEnrolOnCompetencyAssessment.SelfAssessmentID); @@ -818,7 +892,8 @@ public IActionResult EnrolDelegateCompleteBy(int supervisorDelegateId, int? day, { SupervisorDelegateDetail = supervisorDelegate, CompetencyAssessment = competencyAssessment, - CompleteByDate = sessionEnrolOnCompetencyAssessment.CompleteByDate + CompleteByDate = sessionEnrolOnCompetencyAssessment.CompleteByDate, + ActionConfirmed = sessionEnrolOnCompetencyAssessment.ActionConfirmed }; if (day != null && month != null && year != null) { @@ -853,6 +928,12 @@ public IActionResult EnrolDelegateSetCompleteBy(int supervisorDelegateId, int da TempData ); } + + sessionEnrolOnCompetencyAssessment.CompleteByDate = new DateTime(year, month, day); + } + else + { + sessionEnrolOnCompetencyAssessment.CompleteByDate = null; } var supervisorRoles = @@ -964,6 +1045,13 @@ public IActionResult EnrolDelegateSummary(int supervisorDelegateId) MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment, TempData ); + + var retirementDate = selfAssessmentService.GetSelfAssessmentById((int)sessionEnrolOnCompetencyAssessment.SelfAssessmentID).RetirementDate; + if (CheckRetirementDate(retirementDate) && !sessionEnrolOnCompetencyAssessment.ActionConfirmed) + { + return RedirectToAction("ConfirmRetiringSelfAssessment", "Supervisor", new { supervisorDelegateId }); + } + var supervisorDelegate = supervisorService.GetSupervisorDelegateDetailsById(supervisorDelegateId, GetAdminId(), 0); var competencyAssessment = supervisorService.GetCompetencyAssessmentById((int)sessionEnrolOnCompetencyAssessment.SelfAssessmentID); @@ -991,6 +1079,7 @@ public IActionResult EnrolDelegateSummary(int supervisorDelegateId) ViewBag.completeByMonth = TempData["completeByMonth"]; ViewBag.completeByYear = TempData["completeByYear"]; ViewBag.navigatedFrom = TempData["navigatedFrom"]; + ViewBag.actionConfirmed = sessionEnrolOnCompetencyAssessment.ActionConfirmed; return View("EnrolDelegateSummary", model); } @@ -1514,5 +1603,14 @@ private static string RenderRazorViewToString(Controller controller, string view return sw.GetStringBuilder().ToString(); } } + private bool CheckRetirementDate(DateTime? date) + { + if (date == null) + return false; + + DateTime retirementOffsetDate = DateTime.Today.AddDays(14); + DateTime today = DateTime.Today; + return (date >= today && date <= retirementOffsetDate); + } } } diff --git a/DigitalLearningSolutions.Web/Controllers/SupervisorController/SupervisorController.cs b/DigitalLearningSolutions.Web/Controllers/SupervisorController/SupervisorController.cs index 476000816f..e61cae3b7b 100644 --- a/DigitalLearningSolutions.Web/Controllers/SupervisorController/SupervisorController.cs +++ b/DigitalLearningSolutions.Web/Controllers/SupervisorController/SupervisorController.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; + using Microsoft.FeatureManagement; [Authorize(Policy = CustomPolicies.UserSupervisor)] public partial class SupervisorController : Controller @@ -28,7 +29,7 @@ public partial class SupervisorController : Controller private readonly IClockUtility clockUtility; private readonly IPdfService pdfService; private readonly ICourseCategoriesService courseCategoriesService; - + private readonly IFeatureManager featureManager; public SupervisorController( ISupervisorService supervisorService, ICommonService commonService, @@ -49,7 +50,8 @@ public SupervisorController( ICandidateAssessmentDownloadFileService candidateAssessmentDownloadFileService, IClockUtility clockUtility, IPdfService pdfService, - ICourseCategoriesService courseCategoriesService + ICourseCategoriesService courseCategoriesService, + IFeatureManager featureManager ) { this.supervisorService = supervisorService; @@ -68,6 +70,7 @@ ICourseCategoriesService courseCategoriesService this.clockUtility = clockUtility; this.pdfService = pdfService; this.courseCategoriesService = courseCategoriesService; + this.featureManager = featureManager; } private int GetCentreId() diff --git a/DigitalLearningSolutions.Web/Controllers/Support/RequestSupportTicketController.cs b/DigitalLearningSolutions.Web/Controllers/Support/RequestSupportTicketController.cs index 3f58a7ec7f..a289b279aa 100644 --- a/DigitalLearningSolutions.Web/Controllers/Support/RequestSupportTicketController.cs +++ b/DigitalLearningSolutions.Web/Controllers/Support/RequestSupportTicketController.cs @@ -84,7 +84,7 @@ public IActionResult TypeofRequest(DlsSubApplication dlsSubApplication) } [HttpPost] - [Route("/{dlsSubApplication}/RequestSupport/setRequestType")] + [Route("/{dlsSubApplication}/RequestSupport/TypeofRequest")] public IActionResult setRequestType(DlsSubApplication dlsSubApplication, RequestTypeViewModel RequestTypemodel, int requestType) { var requestTypes = requestSupportTicketService.GetRequestTypes(); @@ -117,31 +117,36 @@ public IActionResult RequestSummary(DlsSubApplication dlsSubApplication, Request ).GetAwaiter().GetResult(); ; var model = new RequestSummaryViewModel(data); data.setRequestSubjectDetails(model); + if (!ModelState.IsValid) + { + ModelState.Clear(); + } return View("RequestSummary", model); } [HttpPost] - [Route("/{dlsSubApplication}/RequestSupport/SetRequestSummary")] + [Route("/{dlsSubApplication}/RequestSupport/RequestSummary")] public IActionResult SetRequestSummary(DlsSubApplication dlsSubApplication, RequestSummaryViewModel requestDetailsmodel) { - if (requestDetailsmodel.RequestSubject == null) - { - ModelState.AddModelError("RequestSubject", "Please enter request summary"); - return View("RequestSummary", requestDetailsmodel); - } - if (requestDetailsmodel.RequestDescription == null) + var data = multiPageFormService.GetMultiPageFormData( + MultiPageFormDataFeature.AddCustomWebForm("RequestSupportTicketCWF"), + TempData + ).GetAwaiter().GetResult(); + requestDetailsmodel.RequestType = data.RequestType; + + // Check if RequestDescription is null or contains any default empty tags ("


"). + // This ensures that when a user navigates to the submit page and returns to SetRequestSummary, + // removing the description completely results in an actual empty value rather than leftover HTML tags. + if (string.IsNullOrEmpty(StringHelper.StripHtmlTags(requestDetailsmodel.RequestDescription))) { ModelState.AddModelError("RequestDescription", "Please enter request description"); - return View("RequestSummary", requestDetailsmodel); } + if (!ModelState.IsValid) { return View("RequestSummary", requestDetailsmodel); } - var data = multiPageFormService.GetMultiPageFormData( - MultiPageFormDataFeature.AddCustomWebForm("RequestSupportTicketCWF"), - TempData - ).GetAwaiter().GetResult(); ; + data.setRequestSubjectDetails(requestDetailsmodel); setRequestSupportTicketData(data); return RedirectToAction("RequestAttachment", new { dlsSubApplication }); @@ -241,7 +246,7 @@ public IActionResult SupportSummary(DlsSubApplication dlsSubApplication, Support var data = multiPageFormService.GetMultiPageFormData( MultiPageFormDataFeature.AddCustomWebForm("RequestSupportTicketCWF"), TempData - ).GetAwaiter().GetResult(); + ).GetAwaiter().GetResult(); var model = new SupportSummaryViewModel(data); return View("SupportTicketSummaryPage", model); } @@ -257,7 +262,7 @@ public IActionResult SubmitSupportSummary(DlsSubApplication dlsSubApplication, S var data = multiPageFormService.GetMultiPageFormData( MultiPageFormDataFeature.AddCustomWebForm("RequestSupportTicketCWF"), TempData - ).GetAwaiter().GetResult(); + ).GetAwaiter().GetResult(); data.GroupId = configuration.GetFreshdeskCreateTicketGroupId(); data.ProductId = configuration.GetFreshdeskCreateTicketProductId(); List RequestAttachmentList = new List(); @@ -349,7 +354,7 @@ private void setRequestSupportTicketData(RequestSupportTicketData requestSupport { foreach (var item in requestAttachmentmodel.RequestAttachment) { - totalFileSize = totalFileSize + item.SizeMb??0; + totalFileSize = totalFileSize + item.SizeMb ?? 0; } } foreach (var item in requestAttachmentmodel.ImageFiles) diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/SelfAssessmentReports/SelfAssessmentReportsController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/SelfAssessmentReports/SelfAssessmentReportsController.cs index 0491429fd3..483c75ef3a 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/SelfAssessmentReports/SelfAssessmentReportsController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/SelfAssessmentReports/SelfAssessmentReportsController.cs @@ -1,20 +1,23 @@ namespace DigitalLearningSolutions.Web.Controllers.TrackingSystem.Centre.SelfAssessmentReports { + using DigitalLearningSolutions.Data.Enums; + using DigitalLearningSolutions.Data.Extensions; using DigitalLearningSolutions.Data.Services; - using DigitalLearningSolutions.Web.Helpers; - using Microsoft.AspNetCore.Authorization; - using Microsoft.AspNetCore.Mvc; - using Microsoft.FeatureManagement.Mvc; + using DigitalLearningSolutions.Data.Utilities; using DigitalLearningSolutions.Web.Attributes; + using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.Helpers.ExternalApis; using DigitalLearningSolutions.Web.Models.Enums; - using DigitalLearningSolutions.Data.Enums; - using DigitalLearningSolutions.Data.Utilities; + using DigitalLearningSolutions.Web.Services; using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Centre.Reports; - using DigitalLearningSolutions.Web.Helpers.ExternalApis; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; - using DigitalLearningSolutions.Data.Extensions; - using DigitalLearningSolutions.Web.Services; - + using Microsoft.FeatureManagement; + using Microsoft.FeatureManagement.Mvc; + using System; + using System.Linq; + using System.Threading.Tasks; [FeatureGate(FeatureFlags.RefactoredTrackingSystem)] [Authorize(Policy = CustomPolicies.UserCentreAdmin)] [SetDlsSubApplication(nameof(DlsSubApplication.TrackingSystem))] @@ -30,12 +33,16 @@ public class SelfAssessmentReportsController : Controller private readonly string workbookName; private readonly string viewName; private readonly ISelfAssessmentService selfAssessmentService; + private readonly ICentreSelfAssessmentsService centreSelfAssessmentsService; + private readonly IFeatureManager featureManager; public SelfAssessmentReportsController( ISelfAssessmentReportService selfAssessmentReportService, ITableauConnectionHelperService tableauConnectionHelper, IClockUtility clockUtility, IConfiguration config, - ISelfAssessmentService selfAssessmentService + ISelfAssessmentService selfAssessmentService, + ICentreSelfAssessmentsService centreSelfAssessmentsService, + IFeatureManager featureManager ) { this.selfAssessmentReportService = selfAssessmentReportService; @@ -46,17 +53,25 @@ ISelfAssessmentService selfAssessmentService workbookName = config.GetTableauWorkbookName(); viewName = config.GetTableauViewName(); this.selfAssessmentService = selfAssessmentService; + this.centreSelfAssessmentsService = centreSelfAssessmentsService; + this.featureManager = featureManager; } - public IActionResult Index() + [Route("/TrackingSystem/Centre/Reports/SelfAssessments")] + public async Task IndexAsync() { var centreId = User.GetCentreId(); var adminCategoryId = User.GetAdminCategoryId(); var categoryId = this.selfAssessmentService.GetSelfAssessmentCategoryId(1); - var model = new SelfAssessmentReportsViewModel(selfAssessmentReportService.GetSelfAssessmentsForReportList((int)centreId, adminCategoryId), adminCategoryId, categoryId); + var selfAssessments = centreSelfAssessmentsService.GetCentreSelfAssessments(centreId.Value); + var dSATreportIsPublish = selfAssessments.Any(x => x.SelfAssessmentId == 1); + var tableauFlag = await featureManager.IsEnabledAsync(FeatureFlags.TableauSelfAssessmentDashboards); + var tableauQueryOverride = string.Equals(Request.Query["tableaulink"], "true", StringComparison.OrdinalIgnoreCase); + var showTableauLink = tableauFlag || tableauQueryOverride; + var model = new SelfAssessmentReportsViewModel(selfAssessmentReportService.GetSelfAssessmentsForReportList((int)centreId, adminCategoryId), adminCategoryId, categoryId, dSATreportIsPublish, showTableauLink); return View(model); } [HttpGet] - [Route("DownloadDcsa")] + [Route("/TrackingSystem/Centre/Reports/DownloadDcsa")] public IActionResult DownloadDigitalCapabilityToExcel() { var centreId = User.GetCentreIdKnownNotNull(); @@ -69,7 +84,7 @@ public IActionResult DownloadDigitalCapabilityToExcel() ); } [HttpGet] - [Route("DownloadReport")] + [Route("/TrackingSystem/Centre/Reports/DownloadReport")] public IActionResult DownloadSelfAssessmentReport(int selfAssessmentId) { var centreId = User.GetCentreId(); @@ -83,17 +98,24 @@ public IActionResult DownloadSelfAssessmentReport(int selfAssessmentId) ); } [HttpGet] - [Route("TableauCompetencyDashboard")] - public IActionResult TableauCompetencyDashboard() + [Route("/{source}/Reports/TableauCompetencyDashboard")] + public async Task TableauCompetencyDashboardAsync(string source = "TrackingSystem") { var userEmail = User.GetUserPrimaryEmail(); - var jwt = tableauConnectionHelper.GetTableauJwt(userEmail); + var adminId = User.GetAdminId(); + var jwt = tableauConnectionHelper.GetTableauJwt(); + var tableauFlag = await featureManager.IsEnabledAsync(FeatureFlags.TableauSelfAssessmentDashboards); + var tableauQueryOverride = string.Equals(Request.Query["tableaulink"], "true", StringComparison.OrdinalIgnoreCase); + var showTableauLink = tableauFlag || tableauQueryOverride; + ViewBag.Source = source; + ViewBag.Email = userEmail; + ViewBag.AdminId = adminId; ViewBag.SiteName = tableauSiteName; ViewBag.TableauServerUrl = tableauUrl; ViewBag.WorkbookName = workbookName; ViewBag.ViewName = viewName; ViewBag.JwtToken = jwt; - + ViewBag.ShowTableauLink = showTableauLink; return View(); } } diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/DelegateProgressController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/DelegateProgressController.cs index b2fad6e72b..c6b70ef461 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/DelegateProgressController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/DelegateProgressController.cs @@ -275,10 +275,8 @@ public IActionResult ConfirmRemoveFromCourse( ) { var progress = progressService.GetDetailedCourseProgress(progressId); - if (progress == null) - { - return StatusCode((int)HttpStatusCode.Gone); - } + if (!courseService.DelegateHasCurrentProgress(progressId) || progress == null) + return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 }); var model = new RemoveFromCourseViewModel( progress, @@ -305,9 +303,7 @@ RemoveFromCourseViewModel model var progress = progressService.GetDetailedCourseProgress(progressId); if (!courseService.DelegateHasCurrentProgress(progressId) || progress == null) - { - return new NotFoundResult(); - } + return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 }); courseService.RemoveDelegateFromCourse( progress.DelegateId, diff --git a/DigitalLearningSolutions.Web/DigitalLearningSolutions.Web.csproj b/DigitalLearningSolutions.Web/DigitalLearningSolutions.Web.csproj index dcbccf6154..0057f8da8d 100644 --- a/DigitalLearningSolutions.Web/DigitalLearningSolutions.Web.csproj +++ b/DigitalLearningSolutions.Web/DigitalLearningSolutions.Web.csproj @@ -67,11 +67,11 @@ - - - + + + - + @@ -79,12 +79,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - +
diff --git a/DigitalLearningSolutions.Web/Helpers/CompetencyFilterHelper.cs b/DigitalLearningSolutions.Web/Helpers/CompetencyFilterHelper.cs index 26b0e181ab..6dccdc57c7 100644 --- a/DigitalLearningSolutions.Web/Helpers/CompetencyFilterHelper.cs +++ b/DigitalLearningSolutions.Web/Helpers/CompetencyFilterHelper.cs @@ -32,30 +32,46 @@ public static IEnumerable FilterCompetencies(IEnumerable private static void ApplyResponseStatusFilters(ref IEnumerable competencies, IEnumerable filters, string searchText = "") { - var filteredCompetencies = competencies; - var appliedResponseStatusFilters = filters.Where(f => IsResponseStatusFilter(f)); + var appliedResponseStatusFilters = filters.Where(IsResponseStatusFilter).ToList(); - if (appliedResponseStatusFilters.Any() || searchText.Length > 0) + if (!appliedResponseStatusFilters.Any() && string.IsNullOrWhiteSpace(searchText)) { - var wordsInSearchText = searchText.Split().Where(w => w != string.Empty); - filteredCompetencies = from c in competencies - let searchTextMatchesGroup = wordsInSearchText.All(w => c.CompetencyGroup?.Contains(w, StringComparison.CurrentCultureIgnoreCase) ?? false) - let searchTextMatchesCompetencyDescription = wordsInSearchText.All(w => c.Description?.Contains(w, StringComparison.CurrentCultureIgnoreCase) ?? false) - let searchTextMatchesCompetencyName = wordsInSearchText.All(w => c.Name?.Contains(w, StringComparison.CurrentCultureIgnoreCase) ?? false) - let responseStatusFilterMatchesAll = - (!filters.Contains((int)SelfAssessmentCompetencyFilter.RequiresSelfAssessment) || c.AssessmentQuestions.Any(q => q.ResultId == null)) - && (!filters.Contains((int)SelfAssessmentCompetencyFilter.SelfAssessed) || c.AssessmentQuestions.Any(q => q.ResultId != null && q.Requested == null && q.SignedOff == null)) - && (!filters.Contains((int)SelfAssessmentCompetencyFilter.ConfirmationRequested) || c.AssessmentQuestions.Any(q => q.Verified == null && q.Requested != null)) - && (!filters.Contains((int)SelfAssessmentCompetencyFilter.ConfirmationRejected) || c.AssessmentQuestions.Any(q => q.Verified.HasValue && q.SignedOff != true)) - && (!filters.Contains((int)SelfAssessmentCompetencyFilter.Verified) || c.AssessmentQuestions.Any(q => q.Verified.HasValue && q.SignedOff == true)) - && (!filters.Contains((int)SelfAssessmentCompetencyFilter.Optional) || c.Optional) - where (wordsInSearchText.Count() == 0 || searchTextMatchesGroup || searchTextMatchesCompetencyDescription || searchTextMatchesCompetencyName) - && (!appliedResponseStatusFilters.Any() || responseStatusFilterMatchesAll) - select c; + return; } - competencies = filteredCompetencies; - } + // Break search text into words + var wordsInSearchText = searchText? + .Split(' ', StringSplitOptions.RemoveEmptyEntries) + ?? Array.Empty(); + + bool MatchesSearch(Competency c) => + wordsInSearchText.Length == 0 + || wordsInSearchText.All(w => + (c.CompetencyGroup?.Contains(w, StringComparison.CurrentCultureIgnoreCase) ?? false) + || (c.Description?.Contains(w, StringComparison.CurrentCultureIgnoreCase) ?? false) + || (c.Name?.Contains(w, StringComparison.CurrentCultureIgnoreCase) ?? false)); + + // Define reusable filter checks + var filterChecks = new Dictionary> + { + [SelfAssessmentCompetencyFilter.RequiresSelfAssessment] = c => c.AssessmentQuestions.Any(q => q.ResultId == null), + [SelfAssessmentCompetencyFilter.SelfAssessed] = c => c.AssessmentQuestions.Any(q => q.ResultId != null && q.Requested == null && q.SignedOff == null), + [SelfAssessmentCompetencyFilter.ConfirmationRequested] = c => c.AssessmentQuestions.Any(q => q.Verified == null && q.Requested != null), + [SelfAssessmentCompetencyFilter.ConfirmationRejected] = c => c.AssessmentQuestions.Any(q => q.Verified.HasValue && q.SignedOff != true), + [SelfAssessmentCompetencyFilter.Verified] = c => c.AssessmentQuestions.Any(q => q.Verified.HasValue && q.SignedOff == true), + [SelfAssessmentCompetencyFilter.AwaitingConfirmation] = c => c.AssessmentQuestions.Any(q => q.Verified == null && q.Requested != null && q.UserIsVerifier == true), + [SelfAssessmentCompetencyFilter.PendingConfirmation] = c => c.AssessmentQuestions.Any(q => q.ResultId != null && q.Verified == null && q.Requested != null && q.UserIsVerifier == false), + [SelfAssessmentCompetencyFilter.Optional] = c => c.Optional + }; + + // Require ALL applied filters to match + bool MatchesFilters(Competency c) => + !appliedResponseStatusFilters.Any() + || appliedResponseStatusFilters.All(f => filterChecks[(SelfAssessmentCompetencyFilter)f](c)); + + // Final filtering + competencies = competencies.Where(c => MatchesSearch(c) && MatchesFilters(c)); + } private static void ApplyRequirementsFilters(ref IEnumerable competencies, IEnumerable filters) { var filteredCompetencies = competencies; diff --git a/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs b/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs index bb691af8ad..1790e61a09 100644 --- a/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs +++ b/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs @@ -8,6 +8,7 @@ public static class ConfigHelper { public const string DefaultConnectionStringName = "DefaultConnection"; + public const string ReadOnlyConnectionStringName = "ReadOnlyConnection"; public const string UnitTestConnectionStringName = "UnitTestConnection"; public static IConfigurationRoot GetAppConfig() diff --git a/DigitalLearningSolutions.Web/Helpers/DisplayStringHelper.cs b/DigitalLearningSolutions.Web/Helpers/DisplayStringHelper.cs index 5014a2d93c..9f6e6a571b 100644 --- a/DigitalLearningSolutions.Web/Helpers/DisplayStringHelper.cs +++ b/DigitalLearningSolutions.Web/Helpers/DisplayStringHelper.cs @@ -71,6 +71,21 @@ public static string GetPluralitySuffix(int number) { return number == 1 ? string.Empty : "s"; } + public static string PluraliseStringIfRequired(string input, int number) + { + if (number == 1) + { + return input; + } + else if (input.EndsWith("y")) + { + return input.Substring(0, input.Length - 1) + "ies"; + } + else + { + return input + "s"; + } + } public static string? ReplaceNonAlphaNumericSpaceChars(string? input, string replacement) { diff --git a/DigitalLearningSolutions.Web/Helpers/ExternalApis/TableauConnectionHelper.cs b/DigitalLearningSolutions.Web/Helpers/ExternalApis/TableauConnectionHelper.cs index ffe5240748..b04528cf44 100644 --- a/DigitalLearningSolutions.Web/Helpers/ExternalApis/TableauConnectionHelper.cs +++ b/DigitalLearningSolutions.Web/Helpers/ExternalApis/TableauConnectionHelper.cs @@ -9,7 +9,7 @@ public interface ITableauConnectionHelperService { - string GetTableauJwt(string email); + string GetTableauJwt(); } public class TableauConnectionHelper : ITableauConnectionHelperService { @@ -24,7 +24,7 @@ public TableauConnectionHelper(IConfiguration config) connectedAppSecretKey = config.GetTableauClientSecret(); user = config.GetTableauUser(); } - public string GetTableauJwt(string email) + public string GetTableauJwt() { var key = Encoding.UTF8.GetBytes(connectedAppSecretKey); var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256); @@ -41,8 +41,7 @@ public string GetTableauJwt(string email) { "aud", "tableau" }, { "exp", new DateTimeOffset(DateTime.UtcNow.AddMinutes(5)).ToUnixTimeSeconds() }, { "sub", user }, - { "scp", new[] { "tableau:views:embed" } }, - { "ExernalUserEmail", new [] { email } } + { "scp", new[] { "tableau:views:embed" } } }; var token = new JwtSecurityToken(header, payload); var tokenHandler = new JwtSecurityTokenHandler(); diff --git a/DigitalLearningSolutions.Web/Helpers/ProfessionalRegistrationNumberHelper.cs b/DigitalLearningSolutions.Web/Helpers/ProfessionalRegistrationNumberHelper.cs index 461d7ae46b..130c8d52b6 100644 --- a/DigitalLearningSolutions.Web/Helpers/ProfessionalRegistrationNumberHelper.cs +++ b/DigitalLearningSolutions.Web/Helpers/ProfessionalRegistrationNumberHelper.cs @@ -1,7 +1,7 @@ namespace DigitalLearningSolutions.Web.Helpers { - using System.Text.RegularExpressions; using Microsoft.AspNetCore.Mvc.ModelBinding; + using System.Text.RegularExpressions; public class ProfessionalRegistrationNumberHelper { @@ -48,13 +48,17 @@ public static void ValidateProfessionalRegistrationNumber( ); } - const string pattern = @"^[a-z\d-]+$"; + const string pattern = @"^(\d{7}|[A-Za-z]{1,2}\d{6}|\d{4,8}|P?\d{5,6}|[C|P]\d{6}|[A-Za-z]?\d{5,6}|L\d{4,6}|\d{2}-[A-Za-z\d]{4,5})$"; var rg = new Regex(pattern, RegexOptions.IgnoreCase); if (!rg.Match(prn).Success) { modelState.AddModelError( "ProfessionalRegistrationNumber", - "Invalid professional registration number format - Only alphanumeric characters (a-z, A-Z and 0-9) and hyphens (-) allowed" + "Invalid professional registration number format. " + + "Valid formats include: 7 digits (e.g., 1234567), 1–2 letters followed by 6 digits (e.g., AB123456), " + + "4–8 digits, an optional 'P' plus 5–6 digits, 'C' or 'P' plus 6 digits, " + + "an optional letter plus 5–6 digits, 'L' plus 4–6 digits, " + + "or 2 digits followed by a hyphen and 4–5 alphanumeric characters (e.g., 12-AB123)." ); } } diff --git a/DigitalLearningSolutions.Web/Helpers/StringHelper.cs b/DigitalLearningSolutions.Web/Helpers/StringHelper.cs index f5f7faf583..82809fb671 100644 --- a/DigitalLearningSolutions.Web/Helpers/StringHelper.cs +++ b/DigitalLearningSolutions.Web/Helpers/StringHelper.cs @@ -1,6 +1,7 @@ namespace DigitalLearningSolutions.Web.Helpers { using System; + using System.Text.RegularExpressions; using DigitalLearningSolutions.Data.Extensions; using Microsoft.Extensions.Configuration; @@ -16,5 +17,17 @@ public static string GetLocalRedirectUrl(IConfiguration config, string basicUrl) var applicationPath = new Uri(config.GetAppRootPath()).AbsolutePath.TrimEnd('/'); return applicationPath + basicUrl; } + public static string StripHtmlTags(string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + return string.Empty; + } + + // Remove HTML tags + string result = Regex.Replace(input, "<.*?>", string.Empty).Trim(); + result = System.Net.WebUtility.HtmlDecode(result); + return string.IsNullOrEmpty(result.Trim()) ? string.Empty : result; + } } } diff --git a/DigitalLearningSolutions.Web/Models/BulkCompetenciesData.cs b/DigitalLearningSolutions.Web/Models/BulkCompetenciesData.cs index 344c674a89..0fd476caa3 100644 --- a/DigitalLearningSolutions.Web/Models/BulkCompetenciesData.cs +++ b/DigitalLearningSolutions.Web/Models/BulkCompetenciesData.cs @@ -32,7 +32,7 @@ public BulkCompetenciesData(DetailFramework framework, int adminUserId, string c public bool AddCustomAssessmentQuestion { get; set; } = false; public List DefaultQuestionIDs { get; set; } = []; public int? CustomAssessmentQuestionID { get; set; } - public int AddAssessmentQuestionsOption { get; set; } //1 = only added, 2 = added and updated, 3 = all uploaded + public int AddAssessmentQuestionsOption { get; set; } = 2; //1 = only added, 2 = added and updated, 3 = all uploaded public int CompetenciesToProcessCount { get; set; } public int CompetenciesToAddCount { get; set; } public int CompetenciesToUpdateCount { get; set; } diff --git a/DigitalLearningSolutions.Web/Scripts/frameworks/htmleditor.ts b/DigitalLearningSolutions.Web/Scripts/frameworks/htmleditor.ts index 8577474744..3952d26df3 100644 --- a/DigitalLearningSolutions.Web/Scripts/frameworks/htmleditor.ts +++ b/DigitalLearningSolutions.Web/Scripts/frameworks/htmleditor.ts @@ -1,4 +1,4 @@ -import { Jodit } from 'jodit'; +import { Jodit } from 'jodit'; import DOMPurify from 'dompurify'; let jodited = false; @@ -70,5 +70,101 @@ if (jodited === false) { const clean = DOMPurify.sanitize(editor.editor.innerHTML); editor.editor.innerHTML = clean; }); + + document.addEventListener('DOMContentLoaded', () => { + removeWaveErrors(); + removeDevToolsIssues(); + }); + + // ** Start* for jodit editor error (display red outline, focus on summary error text click) **** + const textarea = document.querySelector('.nhsuk-textarea.html-editor.nhsuk-input--error') as HTMLTextAreaElement | null; + if (textarea) { + const editorDiv = document.querySelector('.jodit-container.jodit.jodit_theme_default.jodit-wysiwyg_mode') as HTMLDivElement | null; + editorDiv?.classList.add('jodit-container', 'jodit', 'jodit_theme_default', 'jodit-wysiwyg_mode', 'jodit-error'); + } + const summary = document.querySelector('.nhsuk-list.nhsuk-error-summary__list') as HTMLDivElement | null; + if (summary) { + summary.addEventListener('click', (e: Event) => { + if (textarea) { + const textareaId = textarea.id.toString(); + const target = e.target as HTMLElement; + if (target.tagName.toLowerCase() === 'a') { + const href = (target as HTMLAnchorElement).getAttribute('href'); + + if (href && href.includes(textareaId)) { + const editorArea = document.querySelector('.jodit-wysiwyg') as HTMLDivElement | null; + editorArea?.focus(); + editorArea?.scrollIntoView({ behavior: 'smooth', block: 'center' }); + e.preventDefault(); + } + } + } + }); + } + // ** End* for jodit editor error (display red outline, focus on summary error text click) **** + } +} + +function removeWaveErrors() { + const input = Array.from(document.querySelectorAll('input[tab-index="-1"]')) + .find((el) => el.style.width === '0px' && el.style.height === '0px' + && el.style.position === 'absolute' && el.style.visibility === 'hidden'); + + if (input) { + input.setAttribute('aria-label', 'Hidden input for accessibility'); + input.setAttribute('title', 'HiddenInput'); + } + + const observer = new MutationObserver((mutations, obs) => { + const textarea = document.querySelector('.ace_text-input') as HTMLTextAreaElement | null; + if (textarea) { + textarea.setAttribute('aria-label', 'ace_text-input'); + obs.disconnect(); + } + }); + observer.observe(document.body, { + childList: true, + subtree: true, + }); +} +function removeDevToolsIssues() { + // set role = 'list' to toolbar + const toolbarbox = document.querySelector('.jodit-toolbar__box') as HTMLElement | null; + if (toolbarbox) { + toolbarbox.setAttribute('role', 'list'); + } + // set role = 'list' to statusbar + const statusbar = document.querySelector('.jodit-xpath') as HTMLElement | null; + if (statusbar) { + statusbar.setAttribute('role', 'list'); } + document.querySelectorAll('.jodit-toolbar-button__trigger').forEach((el) => { + el.removeAttribute('role'); + }); + // observer to detect role='trigger' and remove role + const observer = new MutationObserver(() => { + document.querySelectorAll('.jodit-toolbar-button__trigger').forEach((el) => { + el.removeAttribute('role'); + }); + }); + const target = document.querySelector('.jodit-toolbar__box'); + if (target) { + observer.observe(target, { subtree: true, childList: true }); + } + + // observer to detect iframe and set title + const observer2 = new MutationObserver(() => { + const hiddenIframe = Array.from(document.querySelectorAll('iframe')).find((iframe) => { + const rect = iframe.getBoundingClientRect(); + return rect.width === 0 && rect.height === 0 && (iframe.src === 'about:blank' || iframe.getAttribute('src') === 'about:blank'); + }); + if (hiddenIframe) { + hiddenIframe.setAttribute('title', 'Hidden iframe'); + observer2.disconnect(); // Stop observing once found + } + }); + observer2.observe(document.body, { + childList: true, + subtree: true, + }); } diff --git a/DigitalLearningSolutions.Web/Scripts/trackingSystem/tableaureports.ts b/DigitalLearningSolutions.Web/Scripts/trackingSystem/tableaureports.ts new file mode 100644 index 0000000000..873f322189 --- /dev/null +++ b/DigitalLearningSolutions.Web/Scripts/trackingSystem/tableaureports.ts @@ -0,0 +1,31 @@ +async function setTableauParams(): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const viz = document.getElementById('tableau-viz') as any; + const adminIdElement = document.getElementById('hf-adminid') as HTMLInputElement | null; + const emailElement = document.getElementById('hf-email') as HTMLInputElement | null; + + if (!viz) { + // console.error('Tableau Viz element not found.'); + return; + } + + if (!adminIdElement || !emailElement) { + // console.error('Hidden input fields for adminid or email not found.'); + return; + } + + const adminId = adminIdElement.value; + const email = emailElement.value; + + // Listen for the `firstinteractive` event from the Web Component + // eslint-disable-next-line @typescript-eslint/no-unused-vars + viz.addEventListener('firstinteractive', async (event: Event) => { + await viz.workbook.changeParameterValueAsync('adminid', adminId); + await viz.workbook.changeParameterValueAsync('email', email); + }); +} + +// Run the function after the page loads +document.addEventListener('DOMContentLoaded', () => { + setTableauParams(); +}); diff --git a/DigitalLearningSolutions.Web/ServiceFilter/RequireProcessAgreementFilter .cs b/DigitalLearningSolutions.Web/ServiceFilter/RequireProcessAgreementFilter .cs new file mode 100644 index 0000000000..67e11614a0 --- /dev/null +++ b/DigitalLearningSolutions.Web/ServiceFilter/RequireProcessAgreementFilter .cs @@ -0,0 +1,74 @@ +namespace DigitalLearningSolutions.Web.ServiceFilter +{ + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using DigitalLearningSolutions.Web.Services; + using DigitalLearningSolutions.Web.Helpers; + using Microsoft.Extensions.Logging; + + public class RequireProcessAgreementFilter : IActionFilter + { + private readonly ISelfAssessmentService selfAssessmentService; + private readonly ILogger logger; + + public RequireProcessAgreementFilter( + ISelfAssessmentService selfAssessmentService, + ILogger logger + ) + { + this.selfAssessmentService = selfAssessmentService; + this.logger = logger; + } + + public void OnActionExecuted(ActionExecutedContext context) { } + + public void OnActionExecuting(ActionExecutingContext context) + { + if (context.HttpContext.Request.Path.ToString().Contains("/LearningPortal/SelfAssessment/")) + { + if (!(context.Controller is Controller controller)) + { + return; + } + + if (!context.ActionArguments.ContainsKey("selfAssessmentId")) + { + return; + } + + var selfAssessmentId = int.Parse(context.ActionArguments["selfAssessmentId"].ToString()!); + var delegateUserId = controller.User.GetUserIdKnownNotNull(); + + var selfAssessment = selfAssessmentService.GetSelfAssessmentForCandidateById(delegateUserId, selfAssessmentId); + + if (selfAssessment == null) + { + logger.LogWarning( + $"Attempt to access self assessment {selfAssessmentId} by user {delegateUserId}, but no such assessment found" + ); + context.Result = new RedirectToActionResult("StatusCode", "LearningSolutions", new { code = 403 }); + return; + } + + var actionName = context.RouteData.Values["action"]?.ToString(); + if (actionName == "AgreeSelfAssessmentProcess" || actionName == "ProcessAgreed") + { + return; + } + + if (!selfAssessment.SelfAssessmentProcessAgreed && selfAssessment.IsSupervised) + { + logger.LogInformation( + $"Redirecting user {delegateUserId} to agree process page for self assessment {selfAssessmentId}" + ); + + context.Result = new RedirectToActionResult( + "AgreeSelfAssessmentProcess", + "LearningPortal", + new { selfAssessmentId } + ); + } + } + } + } +} diff --git a/DigitalLearningSolutions.Web/Services/CentresService.cs b/DigitalLearningSolutions.Web/Services/CentresService.cs index 6f367105b0..d76e112064 100644 --- a/DigitalLearningSolutions.Web/Services/CentresService.cs +++ b/DigitalLearningSolutions.Web/Services/CentresService.cs @@ -52,7 +52,7 @@ public void UpdateCentreDetailsForSuperAdmin( string centreName, int centreTypeId, int regionId, - string? centreEmail, + string? registrationEmail, string? ipPrefix, bool showOnMap ); @@ -227,9 +227,9 @@ public void UpdateCentreManagerDetails(int centreId, string firstName, string la return centresDataService.GetAllCentres(activeOnly); } - public void UpdateCentreDetailsForSuperAdmin(int centreId, string centreName, int centreTypeId, int regionId, string? centreEmail, string? ipPrefix, bool showOnMap) + public void UpdateCentreDetailsForSuperAdmin(int centreId, string centreName, int centreTypeId, int regionId, string? registrationEmail, string? ipPrefix, bool showOnMap) { - centresDataService.UpdateCentreDetailsForSuperAdmin(centreId, centreName, centreTypeId, regionId, centreEmail, ipPrefix, showOnMap); + centresDataService.UpdateCentreDetailsForSuperAdmin(centreId, centreName, centreTypeId, regionId, registrationEmail, ipPrefix, showOnMap); } public CentreSummaryForRoleLimits GetRoleLimitsForCentre(int centreId) diff --git a/DigitalLearningSolutions.Web/Services/CompetencyAssessmentService.cs b/DigitalLearningSolutions.Web/Services/CompetencyAssessmentService.cs index 7e8cf37fdb..33d55b7fed 100644 --- a/DigitalLearningSolutions.Web/Services/CompetencyAssessmentService.cs +++ b/DigitalLearningSolutions.Web/Services/CompetencyAssessmentService.cs @@ -1,8 +1,6 @@ using DigitalLearningSolutions.Data.DataServices; -using DigitalLearningSolutions.Data.Models.Common; using DigitalLearningSolutions.Data.Models.CompetencyAssessments; using System.Collections.Generic; -using System.Threading.Tasks; namespace DigitalLearningSolutions.Web.Services { @@ -23,7 +21,17 @@ public interface ICompetencyAssessmentService IEnumerable GetNRPRoles(int? nRPSubGroupID); CompetencyAssessmentTaskStatus GetCompetencyAssessmentTaskStatus(int assessmentId, int? frameworkId); + int[] GetLinkedFrameworkIds(int assessmentId); + int? GetPrimaryLinkedFrameworkId(int assessmentId); + IEnumerable GetCompetenciesForCompetencyAssessment(int competencyAssessmentId); + IEnumerable GetLinkedFrameworksForCompetencyAssessment(int competencyAssessmentId); + + bool RemoveSelfAssessmentFramework(int assessmentId, int frameworkId, int adminId); + + int[] GetLinkedFrameworkCompetencyIds(int competencyAssessmentId, int frameworkId); + CompetencyAssessmentFeatures? GetCompetencyAssessmentFeaturesTaskStatus(int competencyAssessmentId); + int? GetSelfAssessmentStructure(int competencyAssessmentId); //UPDATE DATA bool UpdateCompetencyAssessmentName(int competencyAssessmentId, int adminId, string competencyAssessmentName); bool UpdateCompetencyRoleProfileLinks(int competencyAssessmentId, int adminId, int? professionalGroupId, int? subGroupId, int? roleId); @@ -34,10 +42,32 @@ public interface ICompetencyAssessmentService bool UpdateCompetencyAssessmentVocabulary(int assessmentId, int adminId, string vocabulary); bool UpdateVocabularyTaskStatus(int assessmentId, bool taskStatus); bool UpdateRoleProfileLinksTaskStatus(int assessmentId, bool taskStatus); + bool UpdateFrameworkLinksTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus); + bool UpdateSelectCompetenciesTaskStatus(int competencyAssessmentId, bool taskStatus, bool? previousStatus); + bool UpdateOptionalCompetenciesTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus); + bool UpdateRoleRequirementsTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus); + void MoveCompetencyInSelfAssessment(int competencyAssessmentId, + int competencyId, + string direction + ); + void MoveCompetencyGroupInSelfAssessment(int competencyAssessmentId, + int groupId, + string direction + ); + bool UpdateCompetencyAssessmentFeaturesTaskStatus(int id, bool descriptionStatus, bool providerandCategoryStatus, bool vocabularyStatus, + bool workingGroupStatus, bool AllframeworkCompetenciesStatus); + void UpdateSelfAssessmentFromFramework(int selfAssessmentId, int? frameworkId); //INSERT DATA int InsertCompetencyAssessment(int adminId, int centreId, string competencyAssessmentName, int? frameworkId); - } + bool InsertSelfAssessmentFramework(int adminId, int assessmentId, int frameworkId); + int GetCompetencyCountByFrameworkId(int competencyAssessmentId, int frameworkId); + bool InsertCompetenciesIntoAssessmentFromFramework(int[] selectedCompetencyIds, int frameworkId, int competencyAssessmentId); + bool InsertSelfAssessmentStructure(int selfAssessmentId, int? frameworkId); + //DELETE DATA + bool RemoveFrameworkCompetenciesFromAssessment(int competencyAssessmentId, int frameworkId); + bool RemoveCompetencyFromAssessment(int competencyAssessmentId, int competencyId); + } public class CompetencyAssessmentService : ICompetencyAssessmentService { private readonly ICompetencyAssessmentDataService competencyAssessmentDataService; @@ -81,7 +111,7 @@ public int InsertCompetencyAssessment(int adminId, int centreId, string competen { competencyAssessmentDataService.InsertSelfAssessmentFramework(adminId, assessmentId, framework.ID); competencyAssessmentDataService.UpdateCompetencyAssessmentDescription(adminId, assessmentId, framework.Description); - competencyAssessmentDataService.UpdateCompetencyAssessmentBranding(assessmentId, (int)framework.BrandID, (int)framework.CategoryID, adminId); + competencyAssessmentDataService.UpdateCompetencyAssessmentBranding(assessmentId, adminId, (int)framework.BrandID, (int)framework.CategoryID); competencyAssessmentDataService.UpdateCompetencyAssessmentVocabulary(assessmentId, adminId, framework.Vocabulary); } } @@ -149,5 +179,116 @@ public bool UpdateRoleProfileLinksTaskStatus(int assessmentId, bool taskStatus) { return competencyAssessmentDataService.UpdateRoleProfileLinksTaskStatus(assessmentId, taskStatus); } - } + + public int[] GetLinkedFrameworkIds(int assessmentId) + { + return competencyAssessmentDataService.GetLinkedFrameworkIds(assessmentId); + } + + public bool InsertSelfAssessmentFramework(int adminId, int assessmentId, int frameworkId) + { + return competencyAssessmentDataService.InsertSelfAssessmentFramework(adminId, assessmentId, frameworkId); + } + + public bool UpdateFrameworkLinksTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus) + { + return competencyAssessmentDataService.UpdateFrameworkLinksTaskStatus(assessmentId, taskStatus, previousStatus); + } + + public int? GetPrimaryLinkedFrameworkId(int assessmentId) + { + return competencyAssessmentDataService.GetPrimaryLinkedFrameworkId(assessmentId); + } + + public bool RemoveSelfAssessmentFramework(int assessmentId, int frameworkId, int adminId) + { + UpdateFrameworkLinksTaskStatus(assessmentId, false, true); + return competencyAssessmentDataService.RemoveSelfAssessmentFramework(assessmentId, frameworkId, adminId); + } + + public int GetCompetencyCountByFrameworkId(int competencyAssessmentId, int frameworkId) + { + return competencyAssessmentDataService.GetCompetencyCountByFrameworkId(competencyAssessmentId, frameworkId); + } + + public bool RemoveFrameworkCompetenciesFromAssessment(int competencyAssessmentId, int frameworkId) + { + UpdateSelectCompetenciesTaskStatus(competencyAssessmentId, false, true); + UpdateOptionalCompetenciesTaskStatus(competencyAssessmentId, false, true); + UpdateRoleRequirementsTaskStatus(competencyAssessmentId, false, true); + return competencyAssessmentDataService.RemoveFrameworkCompetenciesFromAssessment(competencyAssessmentId, frameworkId); + } + + public bool UpdateSelectCompetenciesTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus) + { + return competencyAssessmentDataService.UpdateSelectCompetenciesTaskStatus(assessmentId, taskStatus, previousStatus); + } + + public bool UpdateOptionalCompetenciesTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus) + { + return competencyAssessmentDataService.UpdateOptionalCompetenciesTaskStatus(assessmentId, taskStatus, previousStatus); + } + + public bool UpdateRoleRequirementsTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus) + { + return competencyAssessmentDataService.UpdateRoleRequirementsTaskStatus(assessmentId, taskStatus, previousStatus); + } + + public IEnumerable GetCompetenciesForCompetencyAssessment(int competencyAssessmentId) + { + return competencyAssessmentDataService.GetCompetenciesForCompetencyAssessment(competencyAssessmentId); + } + + public IEnumerable GetLinkedFrameworksForCompetencyAssessment(int competencyAssessmentId) + { + return competencyAssessmentDataService.GetLinkedFrameworksForCompetencyAssessment(competencyAssessmentId); + } + + public int[] GetLinkedFrameworkCompetencyIds(int competencyAssessmentId, int frameworkId) + { + return competencyAssessmentDataService.GetLinkedFrameworkCompetencyIds(competencyAssessmentId, frameworkId); + } + + public bool InsertCompetenciesIntoAssessmentFromFramework(int[] selectedCompetencyIds, int frameworkId, int competencyAssessmentId) + { + return competencyAssessmentDataService.InsertCompetenciesIntoAssessmentFromFramework(selectedCompetencyIds, frameworkId, competencyAssessmentId); + } + + public bool RemoveCompetencyFromAssessment(int competencyAssessmentId, int competencyId) + { + return competencyAssessmentDataService.RemoveCompetencyFromAssessment(competencyAssessmentId, competencyId); + } + + public void MoveCompetencyInSelfAssessment(int competencyAssessmentId, int competencyId, string direction) + { + competencyAssessmentDataService.MoveCompetencyInSelfAssessment(competencyAssessmentId, competencyId, direction); + } + + public void MoveCompetencyGroupInSelfAssessment(int competencyAssessmentId, int groupId, string direction) + { + competencyAssessmentDataService.MoveCompetencyGroupInSelfAssessment(competencyAssessmentId, groupId, direction); + } + public bool UpdateCompetencyAssessmentFeaturesTaskStatus(int id, bool descriptionStatus, bool providerandCategoryStatus, bool vocabularyStatus, + bool workingGroupStatus, bool AllframeworkCompetenciesStatus) + { + return competencyAssessmentDataService.UpdateCompetencyAssessmentFeaturesTaskStatus(id, descriptionStatus, providerandCategoryStatus, vocabularyStatus, + workingGroupStatus, AllframeworkCompetenciesStatus); + } + public CompetencyAssessmentFeatures? GetCompetencyAssessmentFeaturesTaskStatus(int competencyAssessmentId) + { + return competencyAssessmentDataService.GetCompetencyAssessmentFeaturesTaskStatus(competencyAssessmentId); + } + public bool InsertSelfAssessmentStructure(int selfAssessmentId, int? frameworkId) + { + return competencyAssessmentDataService.InsertSelfAssessmentStructure(selfAssessmentId, frameworkId); + } + public void UpdateSelfAssessmentFromFramework(int selfAssessmentId, int? frameworkId) + { + competencyAssessmentDataService.UpdateSelfAssessmentFromFramework(selfAssessmentId, frameworkId); + } + public int? GetSelfAssessmentStructure(int competencyAssessmentId) + { + return competencyAssessmentDataService.GetSelfAssessmentStructure(competencyAssessmentId); + } + } } diff --git a/DigitalLearningSolutions.Web/Services/CompetencyLearningResourcesService.cs b/DigitalLearningSolutions.Web/Services/CompetencyLearningResourcesService.cs index 3f9a8b295c..525d3e87e0 100644 --- a/DigitalLearningSolutions.Web/Services/CompetencyLearningResourcesService.cs +++ b/DigitalLearningSolutions.Web/Services/CompetencyLearningResourcesService.cs @@ -13,6 +13,7 @@ public interface ICompetencyLearningResourcesService IEnumerable GetCompetencyResourceAssessmentQuestionParameters(IEnumerable competencyLearningResourceIds); int AddCompetencyLearningResource(int resourceRefID, string originalResourceName, string description, string resourceType, string link, string catalogue, decimal rating, int competencyID, int adminId); + IEnumerable GetActiveCompetencyLearningResourcesByCompetencyIdAndReferenceId(int competencyId, int referenceId); } public class CompetencyLearningResourcesService : ICompetencyLearningResourcesService { @@ -40,5 +41,9 @@ public IEnumerable GetCompetencyR { return competencyLearningResourcesDataService.GetCompetencyResourceAssessmentQuestionParameters(competencyLearningResourceIds); } + public IEnumerable GetActiveCompetencyLearningResourcesByCompetencyIdAndReferenceId(int competencyId, int referenceId) + { + return competencyLearningResourcesDataService.GetActiveCompetencyLearningResourcesByCompetencyIdAndReferenceId(competencyId, referenceId); + } } } diff --git a/DigitalLearningSolutions.Web/Services/DelegateApprovalsService.cs b/DigitalLearningSolutions.Web/Services/DelegateApprovalsService.cs index 6dd32ebafa..1948e06dd5 100644 --- a/DigitalLearningSolutions.Web/Services/DelegateApprovalsService.cs +++ b/DigitalLearningSolutions.Web/Services/DelegateApprovalsService.cs @@ -103,7 +103,7 @@ public void ApproveAllUnapprovedDelegatesForCentre(int centreId) public void RejectDelegate(int delegateId, int centreId) { - var delegateEntity = userDataService.GetDelegateById(delegateId); + var delegateEntity = userDataService.GetDelegateById(delegateId); if (delegateEntity == null || delegateEntity.DelegateAccount.CentreId != centreId) { @@ -127,6 +127,7 @@ public void RejectDelegate(int delegateId, int centreId) else { userDataService.RemoveDelegateAccount(delegateId); + userDataService.DeleteUserCentreDetail(delegateEntity.UserAccount.Id, centreId); } SendRejectionEmail(delegateEntity); diff --git a/DigitalLearningSolutions.Web/Services/DelegateDownloadFileService.cs b/DigitalLearningSolutions.Web/Services/DelegateDownloadFileService.cs index 7274173d88..f0bba93403 100644 --- a/DigitalLearningSolutions.Web/Services/DelegateDownloadFileService.cs +++ b/DigitalLearningSolutions.Web/Services/DelegateDownloadFileService.cs @@ -39,6 +39,7 @@ public class DelegateDownloadFileService : IDelegateDownloadFileService private const string ProfessionalRegistrationNumber = "Professional Registration Number"; private const string JobGroup = "Job group"; private const string RegisteredDate = "Registered"; + private const string LastAccessed = "Last Accessed Date"; private const string RegistrationComplete = "Registration complete"; private const string Active = "Active"; private const string Approved = "Approved"; @@ -333,6 +334,7 @@ DataTable dataTable new DataColumn(ProfessionalRegistrationNumber), new DataColumn(JobGroup), new DataColumn(RegisteredDate), + new DataColumn(LastAccessed), } ); @@ -374,7 +376,7 @@ CentreRegistrationPrompts registrationPrompts ); row[JobGroup] = delegateRecord.JobGroupName; row[RegisteredDate] = delegateRecord.DateRegistered?.Date; - + row[LastAccessed] = delegateRecord.LastAccessed?.Date; var delegateAnswers = delegateRecord.GetRegistrationFieldAnswers(); foreach (var prompt in registrationPrompts.CustomPrompts) @@ -402,7 +404,7 @@ CentreRegistrationPrompts registrationPrompts private static void FormatAllDelegateWorksheetColumns(IXLWorkbook workbook, DataTable dataTable) { ClosedXmlHelper.FormatWorksheetColumn(workbook, dataTable, RegisteredDate, XLDataType.DateTime); - + ClosedXmlHelper.FormatWorksheetColumn(workbook, dataTable, LastAccessed, XLDataType.DateTime); var boolColumns = new[] { RegistrationComplete, Active, Approved, IsAdmin }; foreach (var columnName in boolColumns) { diff --git a/DigitalLearningSolutions.Web/Services/FrameworkService.cs b/DigitalLearningSolutions.Web/Services/FrameworkService.cs index 74bbde1d0f..a4bd97de16 100644 --- a/DigitalLearningSolutions.Web/Services/FrameworkService.cs +++ b/DigitalLearningSolutions.Web/Services/FrameworkService.cs @@ -5,6 +5,7 @@ using DigitalLearningSolutions.Data.Models.Frameworks.Import; using DigitalLearningSolutions.Data.Models.SelfAssessments; using System.Collections.Generic; +using DigitalLearningSolutions.Web.Helpers; using AssessmentQuestion = DigitalLearningSolutions.Data.Models.Frameworks.AssessmentQuestion; using CompetencyResourceAssessmentQuestionParameter = DigitalLearningSolutions.Data.Models.Frameworks.CompetencyResourceAssessmentQuestionParameter; @@ -45,9 +46,9 @@ public interface IFrameworkService CollaboratorNotification? GetCollaboratorNotification(int id, int invitedByAdminId); // Competencies/groups: - IEnumerable GetFrameworkCompetencyGroups(int frameworkId); + IEnumerable GetFrameworkCompetencyGroups(int frameworkId, int? assessmentId); - IEnumerable GetFrameworkCompetenciesUngrouped(int frameworkId); + IEnumerable GetFrameworkCompetenciesUngrouped(int frameworkId, int? assessmentId); CompetencyGroupBase? GetCompetencyGroupBaseById(int Id); @@ -58,6 +59,7 @@ public interface IFrameworkService int GetMaxFrameworkCompetencyGroupID(); IEnumerable GetBulkCompetenciesForFramework(int frameworkId); List GetFrameworkCompetencyOrder(int frameworkId, List frameworkCompetencyIds); + int GetFrameworkCompetencyGroupId(int frameworkId, int competencyGroupId); // Assessment questions: IEnumerable GetAllCompetencyQuestions(int adminId); @@ -119,11 +121,11 @@ bool zeroBased int InsertCompetencyGroup(string groupName, string? groupDescription, int adminId, int? frameworkId = null); - int InsertFrameworkCompetency(int competencyId, int? frameworkCompetencyGroupID, int adminId, int frameworkId, bool alwaysShowDescription = false); + int InsertFrameworkCompetency(int competencyId, int? frameworkCompetencyGroupID, int adminId, int frameworkId, bool addDefaultQuestions = true); IEnumerable GetAllCompetenciesForAdminId(string name, int adminId); - int InsertCompetency(string name, string? description, int adminId); + int InsertCompetency(string name, string? description, int adminId, bool alwaysShowDescription = false); int InsertFrameworkCompetencyGroup(int groupId, int frameworkID, int adminId); @@ -192,7 +194,7 @@ int adminId void UpdateFrameworkConfig(int frameworkId, int adminId, string? frameworkConfig); - void UpdateFrameworkCompetencyGroup( + bool UpdateFrameworkCompetencyGroup( int frameworkCompetencyGroupId, int competencyGroupId, string name, @@ -245,9 +247,9 @@ string direction void RemoveCustomFlag(int flagId); void RemoveCollaboratorFromFramework(int frameworkId, int id); - void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int competencyGroupId, int adminId); + void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int competencyGroupId, int frameworkId, int adminId); - void DeleteFrameworkCompetency(int frameworkCompetencyId, int adminId); + void DeleteFrameworkCompetency(int frameworkCompetencyId, int? frameworkCompetencyGroupId, int frameworkId, int adminId); void DeleteFrameworkDefaultQuestion( int frameworkId, @@ -259,6 +261,7 @@ bool deleteFromExisting void DeleteCompetencyAssessmentQuestion(int frameworkCompetencyId, int assessmentQuestionId, int adminId); void DeleteCompetencyLearningResource(int competencyLearningResourceId, int adminId); + void UpdateFrameworkCompetencyFrameworkCompetencyGroup(int? competencyGroupId, int frameworkCompetencyGroupId, int adminId); } public class FrameworkService : IFrameworkService { @@ -313,14 +316,14 @@ public void DeleteCompetencyLearningResource(int competencyLearningResourceId, i frameworkDataService.DeleteCompetencyLearningResource(competencyLearningResourceId, adminId); } - public void DeleteFrameworkCompetency(int frameworkCompetencyId, int adminId) + public void DeleteFrameworkCompetency(int frameworkCompetencyId, int? frameworkCompetencyGroupId, int frameworkId, int adminId) { - frameworkDataService.DeleteFrameworkCompetency(frameworkCompetencyId, adminId); + frameworkDataService.DeleteFrameworkCompetency(frameworkCompetencyId, frameworkCompetencyGroupId, frameworkId, adminId); } - public void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int competencyGroupId, int adminId) + public void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int competencyGroupId, int frameworkId, int adminId) { - frameworkDataService.DeleteFrameworkCompetencyGroup(frameworkCompetencyGroupId, competencyGroupId, adminId); + frameworkDataService.DeleteFrameworkCompetencyGroup(frameworkCompetencyGroupId, competencyGroupId, frameworkId, adminId); } public void DeleteFrameworkDefaultQuestion(int frameworkId, int assessmentQuestionId, int adminId, bool deleteFromExisting) @@ -478,9 +481,9 @@ public IEnumerable GetFrameworkByFrameworkName(string framewor return frameworkDataService.GetFrameworkByFrameworkName(frameworkName, adminId); } - public IEnumerable GetFrameworkCompetenciesUngrouped(int frameworkId) + public IEnumerable GetFrameworkCompetenciesUngrouped(int frameworkId, int? assessmentId) { - return frameworkDataService.GetFrameworkCompetenciesUngrouped(frameworkId); + return frameworkDataService.GetFrameworkCompetenciesUngrouped(frameworkId, assessmentId); } public FrameworkCompetency? GetFrameworkCompetencyById(int Id) @@ -493,9 +496,9 @@ public IEnumerable GetFrameworkCompetenciesUngrouped(int fr return frameworkDataService.GetFrameworkCompetencyForPreview(frameworkCompetencyId); } - public IEnumerable GetFrameworkCompetencyGroups(int frameworkId) + public IEnumerable GetFrameworkCompetencyGroups(int frameworkId, int? assessmentId) { - return frameworkDataService.GetFrameworkCompetencyGroups(frameworkId); + return frameworkDataService.GetFrameworkCompetencyGroups(frameworkId, assessmentId); } public string? GetFrameworkConfigForFrameworkId(int frameworkId) @@ -515,7 +518,12 @@ public FrameworkDefaultQuestionUsage GetFrameworkDefaultQuestionUsage(int framew public DetailFramework? GetFrameworkDetailByFrameworkId(int frameworkId, int adminId) { - return frameworkDataService.GetFrameworkDetailByFrameworkId(frameworkId, adminId); + var detailFramework = frameworkDataService.GetFrameworkDetailByFrameworkId(frameworkId, adminId); + if (StringHelper.StripHtmlTags(detailFramework.Description) == string.Empty) + { + detailFramework.Description = string.Empty; + } + return detailFramework; } public FrameworkReview? GetFrameworkReview(int frameworkId, int adminId, int reviewId) @@ -593,19 +601,19 @@ public int InsertComment(int frameworkId, int adminId, string comment, int? repl return frameworkDataService.InsertComment(frameworkId, adminId, comment, replyToCommentId); } - public int InsertCompetency(string name, string? description, int adminId) + public int InsertCompetency(string name, string? description, int adminId, bool alwaysShowDescription = false) { - return frameworkDataService.InsertCompetency(name, description, adminId); + return frameworkDataService.InsertCompetency(name.Trim(), description, adminId, alwaysShowDescription); } public int InsertCompetencyGroup(string groupName, string? groupDescription, int adminId, int? frameworkId) { - return frameworkDataService.InsertCompetencyGroup(groupName, groupDescription, adminId, frameworkId); + return frameworkDataService.InsertCompetencyGroup(groupName.Trim(), groupDescription, adminId, frameworkId); } - public int InsertFrameworkCompetency(int competencyId, int? frameworkCompetencyGroupID, int adminId, int frameworkId, bool alwaysShowDescription = false) + public int InsertFrameworkCompetency(int competencyId, int? frameworkCompetencyGroupID, int adminId, int frameworkId, bool addDefaultQuestions = true) { - return frameworkDataService.InsertFrameworkCompetency(competencyId, frameworkCompetencyGroupID, adminId, frameworkId, alwaysShowDescription); + return frameworkDataService.InsertFrameworkCompetency(competencyId, frameworkCompetencyGroupID, adminId, frameworkId, addDefaultQuestions); } public int InsertFrameworkCompetencyGroup(int groupId, int frameworkID, int adminId) @@ -675,12 +683,12 @@ public int UpdateCompetencyFlags(int frameworkId, int competencyId, int[] select public void UpdateFrameworkCompetency(int frameworkCompetencyId, string name, string? description, int adminId, bool? alwaysShowDescription) { - frameworkDataService.UpdateFrameworkCompetency(frameworkCompetencyId, name, description, adminId, alwaysShowDescription); + frameworkDataService.UpdateFrameworkCompetency(frameworkCompetencyId, name.Trim(), description, adminId, alwaysShowDescription); } - public void UpdateFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int competencyGroupId, string name, string? description, int adminId) + public bool UpdateFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int competencyGroupId, string name, string? description, int adminId) { - frameworkDataService.UpdateFrameworkCompetencyGroup(frameworkCompetencyGroupId, competencyGroupId, name, description, adminId); + return frameworkDataService.UpdateFrameworkCompetencyGroup(frameworkCompetencyGroupId, competencyGroupId, name.Trim(), description, adminId); } public void UpdateFrameworkConfig(int frameworkId, int adminId, string? frameworkConfig) @@ -717,5 +725,15 @@ public void UpdateReviewRequestedDate(int reviewId) { frameworkDataService.UpdateReviewRequestedDate(reviewId); } + + public int GetFrameworkCompetencyGroupId(int frameworkId, int competencyGroupId) + { + return frameworkDataService.GetFrameworkCompetencyGroupId(frameworkId, competencyGroupId); + } + + public void UpdateFrameworkCompetencyFrameworkCompetencyGroup(int? competencyGroupId, int frameworkCompetencyGroupId, int adminId) + { + frameworkDataService.UpdateFrameworkCompetencyFrameworkCompetencyGroup(competencyGroupId, frameworkCompetencyGroupId, adminId); + } } } diff --git a/DigitalLearningSolutions.Web/Services/ImportCompetenciesFromFileService.cs b/DigitalLearningSolutions.Web/Services/ImportCompetenciesFromFileService.cs index 45828f985f..a1987466e2 100644 --- a/DigitalLearningSolutions.Web/Services/ImportCompetenciesFromFileService.cs +++ b/DigitalLearningSolutions.Web/Services/ImportCompetenciesFromFileService.cs @@ -11,15 +11,13 @@ namespace DigitalLearningSolutions.Web.Services using ClosedXML.Excel; using DigitalLearningSolutions.Data.Exceptions; using DigitalLearningSolutions.Data.Helpers; - using DigitalLearningSolutions.Data.Models.Frameworks; using DigitalLearningSolutions.Data.Models.Frameworks.Import; - using DocumentFormat.OpenXml.Office2010.Excel; public interface IImportCompetenciesFromFileService { byte[] GetCompetencyFileForFramework(int frameworkId, bool isBlank, string vocabulary); public ImportCompetenciesResult PreProcessCompetenciesTable(IXLWorkbook workbook, string vocabulary, int frameworkId); - public ImportCompetenciesResult ProcessCompetenciesFromFile(IXLWorkbook workbook, int adminUserId, int frameworkId, string vocabulary, int reorderCompetenciesOption, int addAssessmentQuestionsOption, int customAssessmentQuestionID, List defaultQuestionIds); + public ImportCompetenciesResult ProcessCompetenciesFromFile(IXLWorkbook workbook, int adminId, int frameworkId, string vocabulary, int reorderCompetenciesOption, int addAssessmentQuestionsOption, int customAssessmentQuestionID, List defaultQuestionIds); } public class ImportCompetenciesFromFileService : IImportCompetenciesFromFileService { @@ -38,14 +36,23 @@ public ImportCompetenciesResult PreProcessCompetenciesTable(IXLWorkbook workbook var competencyRows = table.Rows().Skip(1).Select(row => new CompetencyTableRow(table, row)).ToList(); var newCompetencyIds = competencyRows.Select(row => row.ID ?? 0).ToList(); var existingIds = frameworkService.GetFrameworkCompetencyOrder(frameworkId, newCompetencyIds); + var existingGroups = frameworkService + .GetFrameworkCompetencyGroups(frameworkId, null).Where(x => x.FrameworkCompetencies.Any()).ToList() + .Select(row => row.Name) + .Distinct() + .ToList(); + var newGroups = competencyRows.Select(row => row.CompetencyGroup) + .Where(g => !string.IsNullOrEmpty(g)) + .Distinct().ToList(); foreach (var competencyRow in competencyRows) { - PreProcessCompetencyRow(competencyRow, newCompetencyIds, existingIds); + PreProcessCompetencyRow(competencyRow, newCompetencyIds, existingIds, existingGroups, newGroups); } return new ImportCompetenciesResult(competencyRows); } - private void PreProcessCompetencyRow(CompetencyTableRow competencyRow, List newIds, List existingIds) + private void PreProcessCompetencyRow(CompetencyTableRow competencyRow, List newIds, List existingIds, List existingGroups, List newGroups) { + if (competencyRow.ID == null) { competencyRow.RowStatus = RowStatus.CompetencyInserted; @@ -66,16 +73,29 @@ private void PreProcessCompetencyRow(CompetencyTableRow competencyRow, List { competencyRow.Reordered = true; } + else + { + var groupName = (string)(competencyRow?.CompetencyGroup); + if (!string.IsNullOrWhiteSpace(groupName)) + { + originalIndex = existingGroups.IndexOf(groupName); + newIndex = newGroups.IndexOf(groupName); + if (originalIndex != newIndex) + { + competencyRow.Reordered = true; + } + } + } } } competencyRow.Validate(); } - public ImportCompetenciesResult ProcessCompetenciesFromFile(IXLWorkbook workbook, int adminUserId, int frameworkId, string vocabulary, int reorderCompetenciesOption, int addAssessmentQuestionsOption, int customAssessmentQuestionID, List defaultQuestionIds) + public ImportCompetenciesResult ProcessCompetenciesFromFile(IXLWorkbook workbook, int adminId, int frameworkId, string vocabulary, int reorderCompetenciesOption, int addAssessmentQuestionsOption, int customAssessmentQuestionID, List defaultQuestionIds) { int maxFrameworkCompetencyId = frameworkService.GetMaxFrameworkCompetencyID(); int maxFrameworkCompetencyGroupId = frameworkService.GetMaxFrameworkCompetencyGroupID(); var table = OpenCompetenciesTable(workbook, vocabulary); - return ProcessCompetenciesTable(table, adminUserId, frameworkId, maxFrameworkCompetencyId, maxFrameworkCompetencyGroupId, addAssessmentQuestionsOption, reorderCompetenciesOption, customAssessmentQuestionID, defaultQuestionIds); + return ProcessCompetenciesTable(table, adminId, frameworkId, maxFrameworkCompetencyId, maxFrameworkCompetencyGroupId, addAssessmentQuestionsOption, reorderCompetenciesOption, customAssessmentQuestionID, defaultQuestionIds); } internal IXLTable OpenCompetenciesTable(IXLWorkbook workbook, string vocabulary) { @@ -92,7 +112,7 @@ internal IXLTable OpenCompetenciesTable(IXLWorkbook workbook, string vocabulary) } return table; } - internal ImportCompetenciesResult ProcessCompetenciesTable(IXLTable table, int adminUserId, int frameworkId, int maxFrameworkCompetencyId, int maxFrameworkCompetencyGroupId, int addAssessmentQuestionsOption, int reorderCompetenciesOption, int customAssessmentQuestionID, List defaultQuestionIds) + internal ImportCompetenciesResult ProcessCompetenciesTable(IXLTable table, int adminId, int frameworkId, int maxFrameworkCompetencyId, int maxFrameworkCompetencyGroupId, int addAssessmentQuestionsOption, int reorderCompetenciesOption, int customAssessmentQuestionID, List defaultQuestionIds) { var competenciesRows = table.Rows().Skip(1).Select(row => new CompetencyTableRow(table, row)).ToList(); int rowCount = 0; @@ -120,19 +140,19 @@ internal ImportCompetenciesResult ProcessCompetenciesTable(IXLTable table, int a .Count(); foreach (var competencyRow in competenciesRows) { - maxFrameworkCompetencyGroupId = ProcessCompetencyRow(adminUserId, frameworkId, maxFrameworkCompetencyId, maxFrameworkCompetencyGroupId, addAssessmentQuestionsOption, reorderCompetenciesOption, customAssessmentQuestionID, defaultQuestionIds, competencyRow); + maxFrameworkCompetencyGroupId = ProcessCompetencyRow(adminId, frameworkId, maxFrameworkCompetencyId, maxFrameworkCompetencyGroupId, addAssessmentQuestionsOption, reorderCompetenciesOption, customAssessmentQuestionID, defaultQuestionIds, competencyRow); } // Check for changes to competency group order and apply them if appropriate: if (reorderCompetenciesOption == 2) { var distinctCompetencyGroups = competenciesRows - .Where(row => !string.IsNullOrWhiteSpace(row.CompetencyGroup)) - .Select(row => row.CompetencyGroup) - .Distinct() - .ToList(); + .Where(row => !string.IsNullOrWhiteSpace(row.CompetencyGroup)) + .Select(row => row.CompetencyGroup) + .Distinct() + .ToList(); for (int i = 0; i < competencyGroupCount; i++) { - var existingGroups = frameworkService.GetFrameworkCompetencyGroups(frameworkId).Select(row => new { row.ID, row.Name }) + var existingGroups = frameworkService.GetFrameworkCompetencyGroups(frameworkId, null).Select(row => new { row.ID, row.Name }) .Distinct() .ToList(); var placesToMove = Math.Abs(existingGroups.FindIndex(group => group.Name == distinctCompetencyGroups[i]) - i); @@ -154,7 +174,7 @@ internal ImportCompetenciesResult ProcessCompetenciesTable(IXLTable table, int a return new ImportCompetenciesResult(competenciesRows); } private int ProcessCompetencyRow( - int adminUserId, + int adminId, int frameworkId, int maxFrameworkCompetencyId, int maxFrameworkCompetencyGroupId, @@ -175,15 +195,22 @@ CompetencyTableRow competencyRow int? frameworkCompetencyGroupId = null; if (competencyRow.CompetencyGroup != null) { - var newCompetencyGroupId = frameworkService.InsertCompetencyGroup(competencyRow.CompetencyGroup, competencyRow.GroupDescription, adminUserId); + int newCompetencyGroupId = frameworkService.InsertCompetencyGroup(competencyRow.CompetencyGroup, competencyRow.GroupDescription, adminId, frameworkId); if (newCompetencyGroupId > 0) { - frameworkCompetencyGroupId = frameworkService.InsertFrameworkCompetencyGroup(newCompetencyGroupId, frameworkId, adminUserId); + frameworkCompetencyGroupId = frameworkService.InsertFrameworkCompetencyGroup(newCompetencyGroupId, frameworkId, adminId); + frameworkService.UpdateFrameworkCompetencyFrameworkCompetencyGroup(competencyRow.ID, (int)frameworkCompetencyGroupId, adminId); if (frameworkCompetencyGroupId > maxFrameworkCompetencyGroupId) { maxFrameworkCompetencyGroupId = (int)frameworkCompetencyGroupId; competencyRow.RowStatus = RowStatus.CompetencyGroupInserted; } + else + { + frameworkCompetencyGroupId = frameworkService.GetFrameworkCompetencyGroupId(frameworkId, newCompetencyGroupId); + var isUpdated = frameworkService.UpdateFrameworkCompetencyGroup((int)frameworkCompetencyGroupId, newCompetencyGroupId, competencyRow.CompetencyGroup, competencyRow.GroupDescription, adminId); + competencyRow.RowStatus = RowStatus.CompetencyGroupUpdated; + } } } // If FrameworkCompetency ID is supplied, update the competency @@ -195,7 +222,7 @@ CompetencyTableRow competencyRow newCompetencyId = frameworkCompetency.CompetencyID; if (frameworkCompetency.Name != competencyRow.Competency || frameworkCompetency.Description != competencyRow.CompetencyDescription || frameworkCompetency.AlwaysShowDescription != competencyRow.AlwaysShowDescription) { - frameworkService.UpdateFrameworkCompetency((int)competencyRow.ID, competencyRow.Competency, competencyRow.CompetencyDescription, adminUserId, competencyRow.AlwaysShowDescription ?? false); + frameworkService.UpdateFrameworkCompetency((int)competencyRow.ID, competencyRow.Competency, competencyRow.CompetencyDescription, adminId, competencyRow.AlwaysShowDescription ?? false); competencyRow.RowStatus = (competencyRow.RowStatus == RowStatus.CompetencyGroupInserted ? RowStatus.CompetencyGroupAndCompetencyUpdated : RowStatus.CompetencyUpdated); } else @@ -207,10 +234,10 @@ CompetencyTableRow competencyRow else { //Check if competency already exists in framework competency group and add if not - newCompetencyId = frameworkService.InsertCompetency(competencyRow.Competency, competencyRow.CompetencyDescription, adminUserId); + newCompetencyId = frameworkService.InsertCompetency(competencyRow.Competency, competencyRow.CompetencyDescription, adminId, competencyRow.AlwaysShowDescription ?? false); if (newCompetencyId > 0) { - newFrameworkCompetencyId = frameworkService.InsertFrameworkCompetency(newCompetencyId, frameworkCompetencyGroupId, adminUserId, frameworkId, competencyRow.AlwaysShowDescription ?? false); //including always show desc flag + newFrameworkCompetencyId = frameworkService.InsertFrameworkCompetency(newCompetencyId, frameworkCompetencyGroupId, adminId, frameworkId, false); if (newFrameworkCompetencyId > maxFrameworkCompetencyId) { competencyRow.RowStatus = (competencyRow.RowStatus == RowStatus.CompetencyGroupInserted ? RowStatus.CompetencyGroupAndCompetencyInserted : RowStatus.CompetencyInserted); @@ -236,7 +263,7 @@ CompetencyTableRow competencyRow { foreach (var frameworkFlag in frameworkFlags) { - if (frameworkFlag.FlagName == flag) + if (frameworkFlag.FlagName?.Trim().ToLower() == flag?.Trim().ToLower()) { flagId = frameworkFlag.FlagId; break; @@ -245,7 +272,7 @@ CompetencyTableRow competencyRow } if (flagId == 0) { - flagId = frameworkService.AddCustomFlagToFramework(frameworkId, flag, "Flag", "nhsuk-tag--white"); + flagId = frameworkService.AddCustomFlagToFramework(frameworkId, flag?.Trim(), "Flag", "nhsuk-tag--white"); } flagIds.Add(flagId); } @@ -262,20 +289,23 @@ CompetencyTableRow competencyRow // Reorder competencies if required: if (reorderCompetenciesOption == 2) { - var frameworkCompetencyId = (int)competencyRow.ID; - var frameworkCompetency = frameworkService.GetFrameworkCompetencyById(frameworkCompetencyId); - var placesToMove = Math.Abs(frameworkCompetency.Ordering - competencyRow.CompetencyOrderNumber); - - if (placesToMove > 0) + var frameworkCompetencyId = competencyRow.ID ?? 0; + if (frameworkCompetencyId > 0) { - var direction = frameworkCompetency.Ordering > competencyRow.CompetencyOrderNumber ? "UP" : "DOWN"; + var frameworkCompetency = frameworkService.GetFrameworkCompetencyById(frameworkCompetencyId); + var placesToMove = Math.Abs(frameworkCompetency.Ordering - competencyRow.CompetencyOrderNumber); - for (int i = 0; i < placesToMove; i++) + if (placesToMove > 0) { - frameworkService.MoveFrameworkCompetency(frameworkCompetencyId, true, direction); - } + var direction = frameworkCompetency.Ordering > competencyRow.CompetencyOrderNumber ? "UP" : "DOWN"; - competencyRow.Reordered = true; + for (int i = 0; i < placesToMove; i++) + { + frameworkService.MoveFrameworkCompetency(frameworkCompetencyId, true, direction); + } + + competencyRow.Reordered = true; + } } } @@ -286,11 +316,11 @@ CompetencyTableRow competencyRow { foreach (var id in defaultQuestionIds) { - frameworkService.AddCompetencyAssessmentQuestion(competencyRow.ID ?? newFrameworkCompetencyId, id, adminUserId); + frameworkService.AddCompetencyAssessmentQuestion(competencyRow.ID ?? newFrameworkCompetencyId, id, adminId); } if (customAssessmentQuestionID > 0) { - frameworkService.AddCompetencyAssessmentQuestion(competencyRow.ID ?? newFrameworkCompetencyId, customAssessmentQuestionID, adminUserId); + frameworkService.AddCompetencyAssessmentQuestion(competencyRow.ID ?? newFrameworkCompetencyId, customAssessmentQuestionID, adminId); } } } diff --git a/DigitalLearningSolutions.Web/Services/LoginService.cs b/DigitalLearningSolutions.Web/Services/LoginService.cs index 35d8cc7a16..dc52aca0b5 100644 --- a/DigitalLearningSolutions.Web/Services/LoginService.cs +++ b/DigitalLearningSolutions.Web/Services/LoginService.cs @@ -5,10 +5,12 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; + using DigitalLearningSolutions.Data.DataServices; using DigitalLearningSolutions.Data.Enums; using DigitalLearningSolutions.Data.Helpers; using DigitalLearningSolutions.Data.Models; using DigitalLearningSolutions.Data.Models.User; + using DigitalLearningSolutions.Data.Utilities; using DigitalLearningSolutions.Data.ViewModels; using DigitalLearningSolutions.Web.Helpers; using Microsoft.AspNetCore.Authentication; @@ -28,6 +30,12 @@ List idsOfCentresWithUnverifiedEmails bool CentreEmailIsVerified(int userId, int centreIdIfLoggingIntoSingleCentre); + void UpdateLastAccessedForUsersTable(int Id); + + void UpdateLastAccessedForDelegatesAccountsTable(int Id); + + void UpdateLastAccessedForAdminAccountsTable(int Id); + Task HandleLoginResult( LoginResult loginResult, TicketReceivedContext context, @@ -41,11 +49,28 @@ public class LoginService : ILoginService { private readonly IUserService userService; private readonly IUserVerificationService userVerificationService; + private readonly ILoginDataService loginDataService; - public LoginService(IUserService userService, IUserVerificationService userVerificationService) + public LoginService(IUserService userService, IUserVerificationService userVerificationService, ILoginDataService loginDataService) { this.userService = userService; this.userVerificationService = userVerificationService; + this.loginDataService = loginDataService; + } + + public void UpdateLastAccessedForUsersTable(int Id) + { + loginDataService.UpdateLastAccessedForUsersTable(Id); + } + + public void UpdateLastAccessedForDelegatesAccountsTable(int Id) + { + loginDataService.UpdateLastAccessedForDelegatesAccountsTable(Id); + } + + public void UpdateLastAccessedForAdminAccountsTable(int Id) + { + loginDataService.UpdateLastAccessedForAdminAccountsTable(Id); } public LoginResult AttemptLogin(string username, string password) diff --git a/DigitalLearningSolutions.Web/Services/SelfAssessmentReportService.cs b/DigitalLearningSolutions.Web/Services/SelfAssessmentReportService.cs index 18b6c46cdc..916e42d46f 100644 --- a/DigitalLearningSolutions.Web/Services/SelfAssessmentReportService.cs +++ b/DigitalLearningSolutions.Web/Services/SelfAssessmentReportService.cs @@ -1,129 +1,495 @@ -namespace DigitalLearningSolutions.Data.Services -{ - using ClosedXML.Excel; - using DigitalLearningSolutions.Data.DataServices.SelfAssessmentDataService; - using DigitalLearningSolutions.Data.Models.Email; - using DigitalLearningSolutions.Data.Models.SelfAssessments; - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - - public interface ISelfAssessmentReportService - { - byte[] GetSelfAssessmentExcelExportForCentre(int centreId, int selfAssessmentId); - byte[] GetDigitalCapabilityExcelExportForCentre(int centreId); - IEnumerable GetSelfAssessmentsForReportList(int centreId, int? categoryId); - } - public class SelfAssessmentReportService : ISelfAssessmentReportService - { - private readonly IDCSAReportDataService dcsaReportDataService; - private readonly ISelfAssessmentReportDataService selfAssessmentReportDataService; - public SelfAssessmentReportService( - IDCSAReportDataService dcsaReportDataService, - ISelfAssessmentReportDataService selfAssessmentReportDataService - ) - { - this.dcsaReportDataService = dcsaReportDataService; - this.selfAssessmentReportDataService = selfAssessmentReportDataService; - } - private static void AddSheetToWorkbook(IXLWorkbook workbook, string sheetName, IEnumerable? dataObjects) - { - var sheet = workbook.Worksheets.Add(sheetName); - var table = sheet.Cell(1, 1).InsertTable(dataObjects); - table.Theme = XLTableTheme.TableStyleLight9; - sheet.Columns().AdjustToContents(); - } - - public IEnumerable GetSelfAssessmentsForReportList(int centreId, int? categoryId) - { - return selfAssessmentReportDataService.GetSelfAssessmentsForReportList(centreId, categoryId); - } - - public byte[] GetSelfAssessmentExcelExportForCentre(int centreId, int selfAssessmentId) - { - var selfAssessmentReportData = selfAssessmentReportDataService.GetSelfAssessmentReportDataForCentre(centreId, selfAssessmentId); - var reportData = selfAssessmentReportData.Select( - x => new - { - x.SelfAssessment, - x.Learner, - x.LearnerActive, - x.PRN, - x.JobGroup, - x.ProgrammeCourse, - x.Organisation, - x.DepartmentTeam, - x.OtherCentres, - x.DLSRole, - x.Registered, - x.Started, - x.LastAccessed, - x.OptionalProficienciesAssessed, - x.SelfAssessedAchieved, - x.ConfirmedResults, - x.SignOffRequested, - x.SignOffAchieved, - x.ReviewedDate - } - ); - using var workbook = new XLWorkbook(); - AddSheetToWorkbook(workbook, "SelfAssessmentLearners", reportData); - using var stream = new MemoryStream(); - workbook.SaveAs(stream); - return stream.ToArray(); - } - public byte[] GetDigitalCapabilityExcelExportForCentre(int centreId) - { - var delegateCompletionStatus = dcsaReportDataService.GetDelegateCompletionStatusForCentre(centreId); - var outcomeSummary = dcsaReportDataService.GetOutcomeSummaryForCentre(centreId); - var summary = delegateCompletionStatus.Select( - x => new - { - x.EnrolledMonth, - x.EnrolledYear, - x.FirstName, - x.LastName, - Email = (Guid.TryParse(x.Email, out _) ? string.Empty : x.Email), - x.CentreField1, - x.CentreField2, - x.CentreField3, - x.Status - } - ); - var details = outcomeSummary.Select( - x => new - { - x.EnrolledMonth, - x.EnrolledYear, - x.JobGroup, - x.CentreField1, - x.CentreField2, - x.CentreField3, - x.Status, - x.LearningLaunched, - x.LearningCompleted, - x.DataInformationAndContentConfidence, - x.DataInformationAndContentRelevance, - x.TeachinglearningAndSelfDevelopmentConfidence, - x.TeachinglearningAndSelfDevelopmentRelevance, - x.CommunicationCollaborationAndParticipationConfidence, - x.CommunicationCollaborationAndParticipationRelevance, - x.TechnicalProficiencyConfidence, - x.TechnicalProficiencyRelevance, - x.CreationInnovationAndResearchConfidence, - x.CreationInnovationAndResearchRelevance, - x.DigitalIdentityWellbeingSafetyAndSecurityConfidence, - x.DigitalIdentityWellbeingSafetyAndSecurityRelevance - } - ); - using var workbook = new XLWorkbook(); - AddSheetToWorkbook(workbook, "Delegate Completion Status", summary); - AddSheetToWorkbook(workbook, "Assessment Outcome Summary", outcomeSummary); - using var stream = new MemoryStream(); - workbook.SaveAs(stream); - return stream.ToArray(); - } - } - -} +namespace DigitalLearningSolutions.Data.Services +{ + using ClosedXML.Excel; + using DigitalLearningSolutions.Data.DataServices.SelfAssessmentDataService; + using DigitalLearningSolutions.Data.Models.CustomPrompts; + using DigitalLearningSolutions.Data.Models.SelfAssessments; + using DigitalLearningSolutions.Data.Models.SelfAssessments.Export; + using DigitalLearningSolutions.Web.Services; + using System; + using System.Collections.Generic; + using System.Data; + using System.IO; + using System.Linq; + + public interface ISelfAssessmentReportService + { + byte[] GetSelfAssessmentExcelExportForCentre(int centreId, int selfAssessmentId); + byte[] GetDigitalCapabilityExcelExportForCentre(int centreId); + IEnumerable GetSelfAssessmentsForReportList(int centreId, int? categoryId); + } + public class SelfAssessmentReportService : ISelfAssessmentReportService + { + private const string SelfAssessment = "SelfAssessment"; + private const string Learner = "Learner"; + private const string LearnerActive = "LearnerActive"; + private const string PRN = "PRN"; + private const string JobGroup = "JobGroup"; + private const string OtherCentres = "OtherCentres"; + private const string DLSRole = "DLSRole"; + private const string Registered = "Registered"; + private const string Started = "Started"; + private const string LastAccessed = "LastAccessed"; + private const string OptionalProficienciesAssessed = "OptionalProficienciesAssessed"; + private const string SelfAssessedAchieved = "SelfAssessedAchieved"; + private const string ConfirmedResults = "ConfirmedResults"; + private const string SignOffRequested = "SignOffRequested"; + private const string SignOffAchieved = "SignOffAchieved"; + private const string ReviewedDate = "ReviewedDate"; + private const string EnrolledMonth = "EnrolledMonth"; + private const string EnrolledYear = "EnrolledYear"; + private const string FirstName = "FirstName"; + private const string LastName = "LastName"; + private const string Email = "Email"; + private const string Status = "Status"; + private const string LearningLaunched = "LearningLaunched"; + private const string LearningCompleted = "LearningCompleted"; + private const string DataInformationAndContentConfidence = "DataInformationAndContentConfidence"; + private const string DataInformationAndContentRelevance = "DataInformationAndContentRelevance"; + private const string TeachinglearningAndSelfDevelopmentConfidence = "TeachinglearningAndSelfDevelopmentConfidence"; + private const string TeachinglearningAndSelfDevelopmentRelevance = "TeachinglearningAndSelfDevelopmentRelevance"; + private const string CommunicationCollaborationAndParticipationConfidence = "CommunicationCollaborationAndParticipationConfidence"; + private const string CommunicationCollaborationAndParticipationRelevance = "CommunicationCollaborationAndParticipationRelevance"; + private const string TechnicalProficiencyConfidence = "TechnicalProficiencyConfidence"; + private const string TechnicalProficiencyRelevance = "TechnicalProficiencyRelevance"; + private const string CreationInnovationAndResearchConfidence = "CreationInnovationAndResearchConfidence"; + private const string CreationInnovationAndResearchRelevance = "CreationInnovationAndResearchRelevance"; + private const string DigitalIdentityWellbeingSafetyAndSecurityConfidence = "DigitalIdentityWellbeingSafetyAndSecurityConfidence"; + private const string DigitalIdentityWellbeingSafetyAndSecurityRelevance = "DigitalIdentityWellbeingSafetyAndSecurityRelevance"; + + + + private readonly IDCSAReportDataService dcsaReportDataService; + private readonly ISelfAssessmentReportDataService selfAssessmentReportDataService; + private readonly ICentreRegistrationPromptsService registrationPromptsService; + public SelfAssessmentReportService( + IDCSAReportDataService dcsaReportDataService, + ISelfAssessmentReportDataService selfAssessmentReportDataService, + ICentreRegistrationPromptsService registrationPromptsService + ) + { + this.dcsaReportDataService = dcsaReportDataService; + this.selfAssessmentReportDataService = selfAssessmentReportDataService; + this.registrationPromptsService = registrationPromptsService; + } + private static void AddSheetToWorkbook(IXLWorkbook workbook, string sheetName, IEnumerable? dataObjects) + { + var sheet = workbook.Worksheets.Add(sheetName); + var table = sheet.Cell(1, 1).InsertTable(dataObjects); + table.Theme = XLTableTheme.TableStyleLight9; + sheet.Columns().AdjustToContents(); + } + + public IEnumerable GetSelfAssessmentsForReportList(int centreId, int? categoryId) + { + return selfAssessmentReportDataService.GetSelfAssessmentsForReportList(centreId, categoryId); + } + + public byte[] GetSelfAssessmentExcelExportForCentre(int centreId, int selfAssessmentId) + { + using var workbook = new XLWorkbook(); + var selfAssessmentReportData = selfAssessmentReportDataService.GetSelfAssessmentReportDataForCentre(centreId, selfAssessmentId); + PopulateSelfAssessmentSheetForCentre(workbook, centreId, selfAssessmentReportData); + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + return stream.ToArray(); + } + public byte[] GetDigitalCapabilityExcelExportForCentre(int centreId) + { + + using var workbook = new XLWorkbook(); + GetDelegateCompletionStatusForCentre(workbook, centreId); + GetOutcomeSummaryForCentre(workbook, centreId); + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + return stream.ToArray(); + } + + private void GetOutcomeSummaryForCentre(XLWorkbook workbook, int centreId) + { + var outcomeSummary = dcsaReportDataService.GetOutcomeSummaryForCentre(centreId); + + var sheet = workbook.Worksheets.Add("Assessment Outcome Summary"); + // Set sheet to have outlining expand buttons at the top of the expanded section. + sheet.Outline.SummaryVLocation = XLOutlineSummaryVLocation.Top; + var customRegistrationPrompts = + registrationPromptsService.GetCentreRegistrationPromptsByCentreId(centreId); + var dataTable = new DataTable(); + + var details = outcomeSummary.Select( + x => new + { + x.EnrolledMonth, + x.EnrolledYear, + x.JobGroup, + x.RegistrationAnswer1, + x.RegistrationAnswer2, + x.RegistrationAnswer3, + x.RegistrationAnswer4, + x.RegistrationAnswer5, + x.RegistrationAnswer6, + x.Status, + x.LearningLaunched, + x.LearningCompleted, + x.DataInformationAndContentConfidence, + x.DataInformationAndContentRelevance, + x.TeachinglearningAndSelfDevelopmentConfidence, + x.TeachinglearningAndSelfDevelopmentRelevance, + x.CommunicationCollaborationAndParticipationConfidence, + x.CommunicationCollaborationAndParticipationRelevance, + x.TechnicalProficiencyConfidence, + x.TechnicalProficiencyRelevance, + x.CreationInnovationAndResearchConfidence, + x.CreationInnovationAndResearchRelevance, + x.DigitalIdentityWellbeingSafetyAndSecurityConfidence, + x.DigitalIdentityWellbeingSafetyAndSecurityRelevance + } + ); + + // set the common header table for the excel sheet + SetUpCommonTableColumnsForOutcomeSummary(customRegistrationPrompts, dataTable); + + // insert the header table into the sheet starting at the first position + var headerTable = sheet.Cell(1, 1).InsertTable(dataTable); + + foreach (var report in outcomeSummary) + { + //iterate and add every record from the query to the datatable + AddOutcomeSummaryReportToSheet(sheet, dataTable, customRegistrationPrompts, report); + } + + var insertedDataRange = sheet.Cell(GetNextEmptyRowNumber(sheet), 1).InsertData(dataTable.Rows); + if (dataTable.Rows.Count > 0) + { + sheet.Rows(insertedDataRange.FirstRow().RowNumber(), insertedDataRange.LastRow().RowNumber()) + .Group(true); + } + + //format the sheet rows and content + sheet.Rows().Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + headerTable.Theme = XLTableTheme.TableStyleLight9; + + sheet.Columns().AdjustToContents(); + + + } + + private void AddOutcomeSummaryReportToSheet(IXLWorksheet sheet, DataTable dataTable, CentreRegistrationPrompts customRegistrationPrompts, DCSAOutcomeSummary report) + { + var row = dataTable.NewRow(); + row[EnrolledMonth] = report.EnrolledMonth; + row[EnrolledYear] = report.EnrolledYear; + row[JobGroup] = report.JobGroup; + + // map the individual registration fields with the centre registration custom prompts + foreach (var prompt in customRegistrationPrompts.CustomPrompts) + { + if (dataTable.Columns.Contains($"{prompt.PromptText} (Prompt {prompt.RegistrationField.Id})")) + { + row[$"{prompt.PromptText} (Prompt {prompt.RegistrationField.Id})"] = + report.CentreRegistrationPrompts[prompt.RegistrationField.Id - 1]; + } + else + { + row[prompt.PromptText] = + report.CentreRegistrationPrompts[prompt.RegistrationField.Id - 1]; + } + } + row[Status] = report.Status; + row[LearningLaunched] = report.LearningLaunched; + row[LearningCompleted] = report.LearningCompleted; + row[DataInformationAndContentConfidence] = report.DataInformationAndContentConfidence; + row[DataInformationAndContentRelevance] = report.DataInformationAndContentRelevance; + row[TeachinglearningAndSelfDevelopmentConfidence] = report.TeachinglearningAndSelfDevelopmentConfidence; + row[TeachinglearningAndSelfDevelopmentRelevance] = report.TeachinglearningAndSelfDevelopmentRelevance; + row[CommunicationCollaborationAndParticipationConfidence] = report.CommunicationCollaborationAndParticipationConfidence; + row[CommunicationCollaborationAndParticipationRelevance] = report.CommunicationCollaborationAndParticipationRelevance; + row[TechnicalProficiencyConfidence] = report.TechnicalProficiencyConfidence; + row[TechnicalProficiencyRelevance] = report.TechnicalProficiencyRelevance; + row[CreationInnovationAndResearchConfidence] = report.CreationInnovationAndResearchConfidence; + row[CreationInnovationAndResearchRelevance] = report.CreationInnovationAndResearchRelevance; + row[DigitalIdentityWellbeingSafetyAndSecurityConfidence] = report.DigitalIdentityWellbeingSafetyAndSecurityConfidence; + row[DigitalIdentityWellbeingSafetyAndSecurityRelevance] = report.DigitalIdentityWellbeingSafetyAndSecurityRelevance; + dataTable.Rows.Add(row); + } + + private void SetUpCommonTableColumnsForOutcomeSummary(CentreRegistrationPrompts customRegistrationPrompts, DataTable dataTable) + { + dataTable.Columns.AddRange( + new[] { + new DataColumn(EnrolledMonth), new DataColumn(EnrolledYear), new DataColumn(JobGroup) + } + ); + + foreach (var prompt in customRegistrationPrompts.CustomPrompts) + { + dataTable.Columns.Add( + !dataTable.Columns.Contains(prompt.PromptText) + ? prompt.PromptText + : $"{prompt.PromptText} (Prompt {prompt.RegistrationField.Id})" + ); + } + + dataTable.Columns.AddRange( + new[] + { + new DataColumn(Status), new DataColumn(LearningLaunched),new DataColumn(LearningCompleted),new DataColumn(DataInformationAndContentConfidence), + new DataColumn(DataInformationAndContentRelevance), new DataColumn(TeachinglearningAndSelfDevelopmentConfidence), + new DataColumn(TeachinglearningAndSelfDevelopmentRelevance),new DataColumn(CommunicationCollaborationAndParticipationConfidence), + new DataColumn(CommunicationCollaborationAndParticipationRelevance), + new DataColumn(TechnicalProficiencyConfidence), + new DataColumn(TechnicalProficiencyRelevance), + new DataColumn(CreationInnovationAndResearchConfidence), + new DataColumn(CreationInnovationAndResearchRelevance), + new DataColumn(DigitalIdentityWellbeingSafetyAndSecurityConfidence), + new DataColumn(DigitalIdentityWellbeingSafetyAndSecurityRelevance) + } + ); + } + + private void GetDelegateCompletionStatusForCentre(XLWorkbook workbook, int centreId) + { + + var delegateCompletionStatus = dcsaReportDataService.GetDelegateCompletionStatusForCentre(centreId); + + var sheet = workbook.Worksheets.Add("Delegate Completion Status"); + // Set sheet to have outlining expand buttons at the top of the expanded section. + sheet.Outline.SummaryVLocation = XLOutlineSummaryVLocation.Top; + var customRegistrationPrompts = + registrationPromptsService.GetCentreRegistrationPromptsByCentreId(centreId); + var dataTable = new DataTable(); + + // did this to sequqence the element into a new form based on the order below + var summary = delegateCompletionStatus.Select( + x => new + { + x.EnrolledMonth, + x.EnrolledYear, + x.FirstName, + x.LastName, + Email = (Guid.TryParse(x.Email, out _) ? string.Empty : x.Email), + x.RegistrationAnswer1, + x.RegistrationAnswer2, + x.RegistrationAnswer3, + x.RegistrationAnswer4, + x.RegistrationAnswer5, + x.RegistrationAnswer6, + x.Status + } + + ); + + // set the common header table for the excel sheet + SetUpCommonTableColumnsForDelegateCompletion(customRegistrationPrompts, dataTable); + + // insert the header table into the sheet starting at the first position + var headerTable = sheet.Cell(1, 1).InsertTable(dataTable); + + foreach (var report in delegateCompletionStatus) + { + //iterate and add every record from the query to the datatable + AddDelegateCompletionReportToSheet(sheet, dataTable, customRegistrationPrompts, report); + } + + var insertedDataRange = sheet.Cell(GetNextEmptyRowNumber(sheet), 1).InsertData(dataTable.Rows); + if (dataTable.Rows.Count > 0) + { + sheet.Rows(insertedDataRange.FirstRow().RowNumber(), insertedDataRange.LastRow().RowNumber()) + .Group(true); + } + + //format the sheet rows and content + sheet.Rows().Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + headerTable.Theme = XLTableTheme.TableStyleLight9; + + sheet.Columns().AdjustToContents(); + + + } + + private void PopulateSelfAssessmentSheetForCentre(IXLWorkbook workbook, int centreId, IEnumerable selfAssessmentReportData) + { + var sheet = workbook.Worksheets.Add("SelfAssessmentLearners"); + // Set sheet to have outlining expand buttons at the top of the expanded section. + sheet.Outline.SummaryVLocation = XLOutlineSummaryVLocation.Top; + var customRegistrationPrompts = + registrationPromptsService.GetCentreRegistrationPromptsByCentreId(centreId); + var dataTable = new DataTable(); + + // did this to sequqence the element into a new form based on the order below + var reportData = selfAssessmentReportData.Select( + x => new + { + x.SelfAssessment, + x.Learner, + x.LearnerActive, + x.PRN, + x.JobGroup, + x.OtherCentres, + x.DLSRole, + x.Registered, + x.Started, + x.LastAccessed, + x.OptionalProficienciesAssessed, + x.SelfAssessedAchieved, + x.ConfirmedResults, + x.SignOffRequested, + x.SignOffAchieved, + x.ReviewedDate + } + ); + + // set the common header table for the excel sheet + SetUpCommonTableColumnsForSelfAssessment(customRegistrationPrompts, dataTable); + + // insert the header table into the sheet starting at the first position + var headerTable = sheet.Cell(1, 1).InsertTable(dataTable); + + foreach (var report in selfAssessmentReportData) + { + //iterate and add every record from the query to the datatable + AddSelfAssessmentReportToSheet(sheet, dataTable, customRegistrationPrompts, report); + } + + var insertedDataRange = sheet.Cell(GetNextEmptyRowNumber(sheet), 1).InsertData(dataTable.Rows); + if (dataTable.Rows.Count > 0) + { + sheet.Rows(insertedDataRange.FirstRow().RowNumber(), insertedDataRange.LastRow().RowNumber()) + .Group(true); + } + + //format the sheet rows and content + sheet.Rows().Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + headerTable.Theme = XLTableTheme.TableStyleLight9; + + sheet.Columns().AdjustToContents(); + } + + private static void SetUpCommonTableColumnsForSelfAssessment(CentreRegistrationPrompts centreRegistrationPrompts, DataTable dataTable) + { + dataTable.Columns.AddRange( + new[] {new DataColumn(SelfAssessment), new DataColumn(Learner), new DataColumn(LearnerActive), new DataColumn(PRN), + new DataColumn(JobGroup)} + ); + foreach (var prompt in centreRegistrationPrompts.CustomPrompts) + { + dataTable.Columns.Add( + !dataTable.Columns.Contains(prompt.PromptText) + ? prompt.PromptText + : $"{prompt.PromptText} (Prompt {prompt.RegistrationField.Id})" + ); + } + + dataTable.Columns.AddRange( + new[] + { + new DataColumn(OtherCentres), + new DataColumn(DLSRole), + new DataColumn(Registered), + new DataColumn(Started), + new DataColumn(LastAccessed), + new DataColumn(OptionalProficienciesAssessed), + new DataColumn(SelfAssessedAchieved), + new DataColumn(ConfirmedResults), + new DataColumn(SignOffRequested), + new DataColumn(SignOffAchieved), + new DataColumn(ReviewedDate) + } + ); + } + + private static void SetUpCommonTableColumnsForDelegateCompletion(CentreRegistrationPrompts centreRegistrationPrompts, DataTable dataTable) + { + dataTable.Columns.AddRange( + new[] {new DataColumn(EnrolledMonth), new DataColumn(EnrolledYear), new DataColumn(FirstName), new DataColumn(LastName), + new DataColumn(Email)} + ); + foreach (var prompt in centreRegistrationPrompts.CustomPrompts) + { + dataTable.Columns.Add( + !dataTable.Columns.Contains(prompt.PromptText) + ? prompt.PromptText + : $"{prompt.PromptText} (Prompt {prompt.RegistrationField.Id})" + ); + } + + dataTable.Columns.AddRange( + new[] + { + new DataColumn(Status) + } + ); + } + + private static void AddSelfAssessmentReportToSheet(IXLWorksheet sheet, DataTable dataTable, CentreRegistrationPrompts centreRegistrationPrompts, + SelfAssessmentReportData report) + { + var row = dataTable.NewRow(); + row[SelfAssessment] = report.SelfAssessment; + row[Learner] = report.Learner; + row[LearnerActive] = report.LearnerActive; + row[PRN] = report.PRN; + row[JobGroup] = report.JobGroup; + + // map the individual registration fields with the centre registration custom prompts + foreach (var prompt in centreRegistrationPrompts.CustomPrompts) + { + if (dataTable.Columns.Contains($"{prompt.PromptText} (Prompt {prompt.RegistrationField.Id})")) + { + row[$"{prompt.PromptText} (Prompt {prompt.RegistrationField.Id})"] = + report.CentreRegistrationPrompts[prompt.RegistrationField.Id - 1]; + } + else + { + row[prompt.PromptText] = + report.CentreRegistrationPrompts[prompt.RegistrationField.Id - 1]; + } + } + row[OtherCentres] = report.OtherCentres; + row[DLSRole] = report.DLSRole; + row[Registered] = report.Registered?.ToString("dd/MM/yyyy"); + row[Started] = report.Started?.ToString("dd/MM/yyyy"); + row[LastAccessed] = report.LastAccessed?.ToString("dd/MM/yyyy"); + row[OptionalProficienciesAssessed] = report.OptionalProficienciesAssessed; + row[SelfAssessedAchieved] = report.SelfAssessedAchieved; + row[ConfirmedResults] = report.ConfirmedResults; + row[SignOffRequested] = report.SignOffRequested?.ToString("dd/MM/yyyy"); + row[SignOffAchieved] = report.SignOffAchieved ? "Yes" : "No"; + row[ReviewedDate] = report.ReviewedDate?.ToString("dd/MM/yyyy"); + dataTable.Rows.Add(row); + } + + private static void AddDelegateCompletionReportToSheet(IXLWorksheet sheet, DataTable dataTable, CentreRegistrationPrompts centreRegistrationPrompts, + DCSADelegateCompletionStatus report) + { + var row = dataTable.NewRow(); + row[EnrolledMonth] = report.EnrolledMonth; + row[EnrolledYear] = report.EnrolledYear; + row[FirstName] = report.FirstName; + row[LastName] = report.LastName; + row[Email] = report.Email; + + // map the individual registration fields with the centre registration custom prompts + foreach (var prompt in centreRegistrationPrompts.CustomPrompts) + { + if (dataTable.Columns.Contains($"{prompt.PromptText} (Prompt {prompt.RegistrationField.Id})")) + { + row[$"{prompt.PromptText} (Prompt {prompt.RegistrationField.Id})"] = + report.CentreRegistrationPrompts[prompt.RegistrationField.Id - 1]; + } + else + { + row[prompt.PromptText] = + report.CentreRegistrationPrompts[prompt.RegistrationField.Id - 1]; + } + } + row[Status] = report.Status; + dataTable.Rows.Add(row); + } + + private static int GetNextEmptyRowNumber(IXLWorksheet sheet) + { + return sheet.LastRowUsed().RowNumber() + 1; + } + } +} diff --git a/DigitalLearningSolutions.Web/Services/SelfAssessmentService.cs b/DigitalLearningSolutions.Web/Services/SelfAssessmentService.cs index c9a50014fa..27ae5e3930 100644 --- a/DigitalLearningSolutions.Web/Services/SelfAssessmentService.cs +++ b/DigitalLearningSolutions.Web/Services/SelfAssessmentService.cs @@ -14,6 +14,7 @@ public interface ISelfAssessmentService { //Self Assessments string? GetSelfAssessmentNameById(int selfAssessmentId); + SelfAssessment? GetSelfAssessmentById(int selfAssessmentId); // Candidate Assessments IEnumerable GetSelfAssessmentsForCandidate(int delegateUserId, int centreId, int? adminIdCategoryID); IEnumerable GetSelfAssessmentsForCandidate(int delegateUserId, int centreId); @@ -32,6 +33,7 @@ public interface ISelfAssessmentService void SetCompleteByDate(int selfAssessmentId, int delegateUserId, DateTime? completeByDate); + void MarkProgressAgreed(int selfAssessmentId, int delegateUserId); bool CanDelegateAccessSelfAssessment(int delegateUserId, int selfAssessmentId, int centreId); // Competencies @@ -162,6 +164,7 @@ public IEnumerable GetSelfAssessmentResultswithSupervisorV int competencyId ); void RemoveReviewCandidateAssessmentOptionalCompetencies(int id); + SelfAssessment GetSelfAssessmentRetirementDateById(int selfAssessmentId); } public class SelfAssessmentService : ISelfAssessmentService @@ -216,6 +219,11 @@ public void SetCompleteByDate(int selfAssessmentId, int delegateUserId, DateTime selfAssessmentDataService.SetCompleteByDate(selfAssessmentId, delegateUserId, completeByDate); } + public void MarkProgressAgreed(int selfAssessmentId, int delegateUserId) + { + selfAssessmentDataService.MarkProgressAgreed(selfAssessmentId, delegateUserId); + } + public IEnumerable GetCandidateAssessmentResultsById(int candidateAssessmentId, int adminId, int? selfAssessmentResultId = null) { return selfAssessmentDataService.GetCandidateAssessmentResultsById(candidateAssessmentId, adminId, selfAssessmentResultId); @@ -466,6 +474,11 @@ public void RemoveEnrolment(int selfAssessmentId, int delegateUserId) return selfAssessmentDataService.GetSelfAssessmentNameById(selfAssessmentId); } + public SelfAssessment? GetSelfAssessmentById(int selfAssessmentId) + { + return selfAssessmentDataService.GetSelfAssessmentById(selfAssessmentId); + } + public (SelfAssessmentDelegatesData, int?) GetSelfAssessmentDelegatesPerPage(string searchString, int offSet, int itemsPerPage, string sortBy, string sortDirection, int? selfAssessmentId, int centreId, bool? isDelegateActive, bool? removed, bool? submitted, bool? signedOff, int? adminCategoryId) { @@ -610,5 +623,9 @@ public void RemoveReviewCandidateAssessmentOptionalCompetencies(int id) { selfAssessmentDataService.RemoveReviewCandidateAssessmentOptionalCompetencies(id); } + public SelfAssessment GetSelfAssessmentRetirementDateById(int selfAssessmentId) + { + return selfAssessmentDataService.GetSelfAssessmentRetirementDateById(selfAssessmentId); + } } } diff --git a/DigitalLearningSolutions.Web/Startup.cs b/DigitalLearningSolutions.Web/Startup.cs index 9443817b2d..9b820a8045 100644 --- a/DigitalLearningSolutions.Web/Startup.cs +++ b/DigitalLearningSolutions.Web/Startup.cs @@ -1,13 +1,5 @@ namespace DigitalLearningSolutions.Web { - using System.Collections.Generic; - using System.Data; - using System.IO; - using System.Linq; - using System.Security.Claims; - using System.Threading.Tasks; - using System.Transactions; - using System.Web; using AspNetCoreRateLimit; using DigitalLearningSolutions.Data.ApiClients; using DigitalLearningSolutions.Data.DataServices; @@ -39,6 +31,7 @@ namespace DigitalLearningSolutions.Web using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.HttpOverrides; + using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; @@ -46,12 +39,19 @@ namespace DigitalLearningSolutions.Web using Microsoft.Extensions.Hosting; using Microsoft.FeatureManagement; using Microsoft.IdentityModel.Protocols.OpenIdConnect; - using Microsoft.AspNetCore.Identity; + using Serilog; + using System; + using System.Collections.Generic; + using System.Data; + using System.IO; + using System.Linq; + using System.Security.Claims; + using System.Threading.Tasks; + using System.Transactions; + using System.Web; using static DigitalLearningSolutions.Data.DataServices.ICentreApplicationsDataService; using static DigitalLearningSolutions.Web.Services.ICentreApplicationsService; using static DigitalLearningSolutions.Web.Services.ICentreSelfAssessmentsService; - using System; - using Serilog; public class Startup { @@ -65,7 +65,6 @@ public Startup(IConfiguration config, IHostEnvironment env) this.config = config; this.env = env; } - public void ConfigureServices(IServiceCollection services) { ConfigureIpRateLimiting(services); @@ -194,6 +193,8 @@ public void ConfigureServices(IServiceCollection services) // Register database connection for Dapper. services.AddScoped(_ => new SqlConnection(defaultConnectionString)); + // Register factory for read-only replica connections + services.AddScoped(); Dapper.SqlMapper.Settings.CommandTimeout = 60; MultiPageFormService.InitConnection(new SqlConnection(defaultConnectionString)); @@ -510,6 +511,7 @@ private static void RegisterDataServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -525,7 +527,6 @@ private static void RegisterDataServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -534,6 +535,7 @@ private static void RegisterDataServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } private static void RegisterHelpers(IServiceCollection services) @@ -580,6 +582,7 @@ private static void RegisterWebServiceFilters(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); } diff --git a/DigitalLearningSolutions.Web/Styles/index.scss b/DigitalLearningSolutions.Web/Styles/index.scss index 987c59e75b..c61029a83f 100644 --- a/DigitalLearningSolutions.Web/Styles/index.scss +++ b/DigitalLearningSolutions.Web/Styles/index.scss @@ -62,11 +62,7 @@ ul > li > ul > li { padding-top: 0; } -.nhsuk-button { - @include mq($until: tablet) { - margin-top: nhsuk-spacing(3); - } -} + input:invalid { border: $nhsuk-border-width-form-element-error solid $nhsuk-error-color; @@ -78,13 +74,7 @@ input[type=file] { } } -input[type=file].nhsuk-input--error { - height: 44px; - @include govuk-media-query($until: tablet) { - height: 40px; - } -} h1#page-heading { margin-bottom: 16px; @@ -98,9 +88,6 @@ h1#page-heading { display: flex; } -.nhsuk-main-wrapper { - flex: 1; -} .responsive-iframe-wrapper { height: 100%; @@ -121,10 +108,6 @@ h1#page-heading { padding-bottom: $iframe-padding-bottom * 1.5; } -.nhsuk-heading-xl.heading-margin-2 { - margin-bottom: nhsuk-spacing(2); -} - .responsive-iframe { width: 100%; height: 100%; @@ -147,25 +130,6 @@ h1#page-heading { margin-bottom: 0; } -.basic-summary-list__row:last-child .nhsuk-summary-list__key { - border: none; -} - -.basic-summary-list__row:last-child .nhsuk-summary-list__value { - border: none; - margin-bottom: 0; -} - -.nhsuk-summary-list__value { - @include govuk-media-query($until: tablet) { - word-break: break-word; - @include word-break-ie-fix; - } -} - -.nhsuk-tag { - text-align: center; -} .right-align-tag-column { padding: 0; @@ -211,10 +175,6 @@ ul.no-bullets { margin-bottom: 0; } -.nhsuk-details__text > .nhsuk-button { - margin-bottom: 0; -} - @mixin hidden-submit-ie-fix { // IE11 hack @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { @@ -335,10 +295,6 @@ ul.no-bullets { } } -.nhsuk-error-message.error-message--margin-bottom-1 { - margin-bottom: nhsuk-spacing(1); -} - .display-none { display: none; } diff --git a/DigitalLearningSolutions.Web/Styles/jodit.scss b/DigitalLearningSolutions.Web/Styles/jodit.scss index 8e0ed889dd..416099e8f7 100644 --- a/DigitalLearningSolutions.Web/Styles/jodit.scss +++ b/DigitalLearningSolutions.Web/Styles/jodit.scss @@ -1 +1,9 @@ @use "jodit/build/jodit.min"; + +.jodit-error { + border: 2px solid red !important; + border-radius: 4px; +} +.jodit-placeholder { + display: none !important; +} diff --git a/DigitalLearningSolutions.Web/Styles/layout.scss b/DigitalLearningSolutions.Web/Styles/layout.scss index f815adafe4..62fe159b39 100644 --- a/DigitalLearningSolutions.Web/Styles/layout.scss +++ b/DigitalLearningSolutions.Web/Styles/layout.scss @@ -14,84 +14,14 @@ body { } #pagewrapper { - display: flex; - flex-direction: column; height: 100%; min-height: 100vh; } -#maincontentwrapper { - flex: 1 0 auto; - align-self: center; - width: 100%; - margin: 0; -} - footer { flex-shrink: 0; } -.nhsuk-header__navigation-item.selected { - a { - font-weight: bold; - } -} - -.nhsuk-header__logo { - @include mq($until: large-desktop) { - max-width: unset; - - .nhsuk-header__link--service { - align-items: center; - display: flex; - -ms-flex-align: center; - margin-bottom: 0; - width: auto; - } - - .nhsuk-header__service-name { - padding-left: nhsuk-spacing(3); - max-width: unset; - } - } -} -.nhsuk-navigation-container { - @include mq($until: tablet) { - margin-top: unset; - } -} - -body:not(.js-enabled) { - .nhsuk-header__menu { - display: none; - } - - #close-menu { - display: none; - } - - .nhsuk-header__navigation { - display: block; - } -} - -:not(.nhsuk-header--transactional) div.nhsuk-header__container { - @media (max-width: 40.0525em) { - margin: 0; - } -} - -.nhsuk-header--transactional { - .nhsuk-header__link--service { - width: auto; - height: auto; - - @include mq($from: large-desktop) { - display: flex; - } - } -} - .centre-brand-logo { float: right; max-width: 280px; @@ -102,14 +32,6 @@ body:not(.js-enabled) { } } -nav .nhsuk-width-container { - margin: 0 auto; -} - -nav, .nhsuk-header__navigation, #header-navigation { - border-bottom: 0; -} - .visual-separator { height: 8px; width: 100%; @@ -307,19 +229,7 @@ nav, .nhsuk-header__navigation, #header-navigation { clip: auto; } -.nhsuk-button--danger { - background-color: $color_nhsuk-red; - box-shadow: 0 4px 0 shade($color_nhsuk-red, 50%); - margin-bottom: 16px !important; - &:hover { - background-color: shade($color_nhsuk-red, 20%); - } - - &:active { - background-color: shade($color_nhsuk-red, 50%); - } -} .first-row td { border-top: 2px solid #d8dde0; @@ -330,12 +240,6 @@ nav, .nhsuk-header__navigation, #header-navigation { white-space: nowrap; } -.nhsuk-header__link--service { - @include mq($from: large-desktop) { - align-items: unset; - } -} - .header-beta { color: #c8e4ff; font-family: FrutigerLTW01-55Roman, Arial, sans-serif; @@ -347,15 +251,8 @@ nav, .nhsuk-header__navigation, #header-navigation { } } -.nhsuk-width-container, .nhsuk-header__navigation-list { - max-width: 1144px !important; - padding-left: $nhsuk-gutter !important; - padding-right: $nhsuk-gutter !important; -} -.nhsuk-width-container { - margin: auto !important; -} + @media only screen and (max-width: 767px) { .section-card-result { diff --git a/DigitalLearningSolutions.Web/Styles/learningPortal/selfassessmentcertificate.scss b/DigitalLearningSolutions.Web/Styles/learningPortal/selfassessmentcertificate.scss index ea0b873f76..166e37f81e 100644 --- a/DigitalLearningSolutions.Web/Styles/learningPortal/selfassessmentcertificate.scss +++ b/DigitalLearningSolutions.Web/Styles/learningPortal/selfassessmentcertificate.scss @@ -99,13 +99,12 @@ width: 100%; } -.pg-2 { +.certificate .pg-2 { background-color: white; width: 100%; position: relative; display: flex; flex-direction: column; - width: 100%; max-width: 210mm; min-height: 297mm; margin: 0; @@ -113,8 +112,9 @@ background: white; } -.pg-2 .body { +.certificate .pg-2 .body { padding: 50px; + flex: 1; } .activity { @@ -126,6 +126,7 @@ .activity p, ul { width: 100%; + word-break: break-word; } .activity ul { @@ -157,12 +158,6 @@ .certificate .pg-1 { width: 100%; } - - - - .pg-2 { - width: 100%; - } } .certificate h1 { @@ -378,18 +373,21 @@ .certificate .nhsuk-u-margin-bottom-2 { margin-bottom: 8px !important; } - } +} + @media screen and (max-width: 767px) { .certificate { - width: 95%; + width: 100%; justify-content: space-around; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; -webkit-print-color-adjust: exact; } + .certificate .pg-1 { - width: 95%; + width: 100%; } + .pg-2 { - width: 95%; + width: 100%; } } diff --git a/DigitalLearningSolutions.Web/Styles/nhsuk.scss b/DigitalLearningSolutions.Web/Styles/nhsuk.scss index c2b4c7239b..51b728e407 100644 --- a/DigitalLearningSolutions.Web/Styles/nhsuk.scss +++ b/DigitalLearningSolutions.Web/Styles/nhsuk.scss @@ -7,3 +7,151 @@ .nhsuk-hint { white-space: initial; } + + +@mixin word-break-ie-fix { + // IE11 hack + @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + word-break: break-all; + } +} + +/* Items below moved from layout.scss or index.scss and deemed to be part of NHSE frontend */ + +body:not(.js-enabled) { + .nhsuk-header__menu { + display: none; + } + + #close-menu { + display: none; + } + + .nhsuk-header__navigation { + display: block; + } +} + +/* nhsuk-header__navigation-item paired with menu li items that become bold when you're on that page */ +/*
  • */ +.nhsuk-header__navigation-item.selected { + a { + font-weight: bold; + } +} + +/* stops blue menu overlapping white header, noticable because it's white, not as noticable in blue headers */ +.nhsuk-header--white .nhsuk-navigation-container { + @include mq($until: tablet) { + margin-top: unset; + } +} + +/* .nhsuk-header--transactional brought from live design-system, not in current implentation */ +.nhsuk-header--transactional { + + .nhsuk-header__link { + display: block; + height: 32px; + width: 80px + } + + .nhsuk-logo { + height: 32px; + width: 80px + } +} + +.nhsuk-main-wrapper { + flex: 1; +} + +.feedback-bar .nhsuk-width-container { + margin-bottom: 0; +} + +nav, .nhsuk-header__navigation, #header-navigation { + border-bottom: 0; +} + +.nhsuk-heading-xl.heading-margin-2 { + margin-bottom: nhsuk-spacing(2); +} + +.nhsuk-button { + @include mq($until: tablet) { + margin-top: nhsuk-spacing(3); + } +} + +.nhsuk-button--danger { + background-color: $color_nhsuk-red; + box-shadow: 0 4px 0 shade($color_nhsuk-red, 50%); + margin-bottom: 16px !important; + + &:hover { + background-color: shade($color_nhsuk-red, 20%); + } + + &:active { + background-color: shade($color_nhsuk-red, 50%); + } +} + +.nhsuk-details__text > .nhsuk-button { + margin-bottom: 0; +} + +input[type=file].nhsuk-input--error { + height: 44px; + + @include govuk-media-query($until: tablet) { + height: 40px; + } +} + +.nhsuk-error-message.error-message--margin-bottom-1 { + margin-bottom: nhsuk-spacing(1); +} + +.basic-summary-list__row:last-child .nhsuk-summary-list__key { + border: none; +} + +.basic-summary-list__row:last-child .nhsuk-summary-list__value { + border: none; + margin-bottom: 0; +} + +.nhsuk-summary-list__value { + @include govuk-media-query($until: tablet) { + word-break: break-word; + @include word-break-ie-fix; + } +} + +.nhsuk-tag { + text-align: center; +} + +.nhsuk-header__logo { + display: flex; + justify-content: space-between; +} + +@media (max-width: 600px) { + .nhsuk-header__transactional--logo { + flex-direction: column; + align-items: flex-start; + gap: 4px; + } +} + +/* + styles/index.scss has the following nhsuk dependancies - FGC 2/6/25 + .nhsuk-u-font-weight-normal; + .nhsuk-u-font-weight-bold; + .nhsuk-u-font-size-16; + .nhsuk-u-font-size-19; +*/ + diff --git a/DigitalLearningSolutions.Web/Styles/trackingSystem/delegates/courseDelegates.scss b/DigitalLearningSolutions.Web/Styles/trackingSystem/delegates/courseDelegates.scss index 3f796924e8..47fb8f20d5 100644 --- a/DigitalLearningSolutions.Web/Styles/trackingSystem/delegates/courseDelegates.scss +++ b/DigitalLearningSolutions.Web/Styles/trackingSystem/delegates/courseDelegates.scss @@ -26,3 +26,31 @@ } } } + +.nhsuk-expander[open] details.nhsuk-details[open] .nhsuk-details__summary-text::before { + display: block; + width: 0; + height: 0; + border-style: solid; + border-color: transparent; + clip-path: polygon(0% 0%, 50% 100%, 100% 0%); + border-width: 12.124px 7px 0 7px; + border-top-color: inherit; +} + +.nhsuk-details[open] .nhsuk-details .nhsuk-details__summary-text::before { + bottom: 0; + content: ""; + left: 0; + margin: auto; + position: absolute; + top: 0; + display: block; + width: 0; + height: 0; + border-style: solid; + border-color: transparent; + clip-path: polygon(0% 0%, 100% 50%, 0% 100%); + border-width: 7px 0 7px 12.124px; + border-left-color: inherit; +} diff --git a/DigitalLearningSolutions.Web/ViewComponents/DlsFooterBannerTextViewComponent.cs b/DigitalLearningSolutions.Web/ViewComponents/DlsFooterBannerTextViewComponent.cs new file mode 100644 index 0000000000..bf484f426e --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewComponents/DlsFooterBannerTextViewComponent.cs @@ -0,0 +1,34 @@ +using System.Security.Claims; +using DigitalLearningSolutions.Data.DataServices; +using DigitalLearningSolutions.Web.Helpers; +using Microsoft.AspNetCore.Mvc; +using DigitalLearningSolutions.Web.ViewModels.Common.ViewComponents; +using DigitalLearningSolutions.Web.Services; +using System; +using System.Threading.Tasks; + +namespace DigitalLearningSolutions.Web.ViewComponents +{ + public class DlsFooterBannerTextViewComponent : ViewComponent + { + private readonly ICentresService centresService; + + public DlsFooterBannerTextViewComponent(ICentresService centresService) + { + this.centresService = centresService; + } + + public IViewComponentResult Invoke() + { + var centreId = ((ClaimsPrincipal)User).GetCustomClaimAsInt(CustomClaimTypes.UserCentreId); + if (centreId == null) + { + return View(new DlsFooterBannerTextViewModel(null)); + } + + var bannerText = centresService.GetBannerText(Convert.ToInt32(centreId)); + var model = new DlsFooterBannerTextViewModel(bannerText); + return View(model); + } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/Common/ViewComponents/DlsFooterBannerTextViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/Common/ViewComponents/DlsFooterBannerTextViewModel.cs new file mode 100644 index 0000000000..3a0fdcdd92 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/Common/ViewComponents/DlsFooterBannerTextViewModel.cs @@ -0,0 +1,13 @@ +namespace DigitalLearningSolutions.Web.ViewModels.Common.ViewComponents +{ + public class DlsFooterBannerTextViewModel + { + public readonly string? BannerText; + + public DlsFooterBannerTextViewModel(string bannerText) + { + BannerText = bannerText; + } + + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/AddCompetenciesSelectFrameworkFormData.cs b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/AddCompetenciesSelectFrameworkFormData.cs new file mode 100644 index 0000000000..1dc5b60ed9 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/AddCompetenciesSelectFrameworkFormData.cs @@ -0,0 +1,10 @@ +namespace DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +{ + using System.ComponentModel.DataAnnotations; + public class AddCompetenciesSelectFrameworkFormData + { + public int ID { get; set; } + [Required(ErrorMessage = "Select a linked framework")] + public int? FrameworkId { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/AddCompetenciesSelectFrameworkViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/AddCompetenciesSelectFrameworkViewModel.cs new file mode 100644 index 0000000000..98a09de671 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/AddCompetenciesSelectFrameworkViewModel.cs @@ -0,0 +1,28 @@ +namespace DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +{ + using DigitalLearningSolutions.Data.Models.CompetencyAssessments; + using DigitalLearningSolutions.Data.Models.Frameworks; + using DigitalLearningSolutions.Web.Helpers; + using System.Collections.Generic; + + public class AddCompetenciesSelectFrameworkViewModel : AddCompetenciesSelectFrameworkFormData + { + public AddCompetenciesSelectFrameworkViewModel( + CompetencyAssessmentBase competencyAssessmentBase, + IEnumerable linkedFrameworks + ) + { + ID = competencyAssessmentBase.ID; + CompetencyAssessmentName = competencyAssessmentBase.CompetencyAssessmentName; + LinkedFrameworks = linkedFrameworks; + UserRole = competencyAssessmentBase.UserRole; + VocabularySingular = FrameworkVocabularyHelper.VocabularySingular(competencyAssessmentBase.Vocabulary); + VocabularyPlural = FrameworkVocabularyHelper.VocabularyPlural(competencyAssessmentBase.Vocabulary); + } + public string? CompetencyAssessmentName { get; set; } + public int UserRole { get; set; } + public string VocabularySingular { get; set; } + public string VocabularyPlural { get; set; } + public IEnumerable LinkedFrameworks { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/AddCompetenciesViewFormData.cs b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/AddCompetenciesViewFormData.cs new file mode 100644 index 0000000000..0e6d2ed887 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/AddCompetenciesViewFormData.cs @@ -0,0 +1,9 @@ +namespace DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +{ + public class AddCompetenciesFormData + { + public int ID { get; set; } + public int[] SelectedCompetencyIds { get; set; } + public int FrameworkId { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/AddCompetenciesViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/AddCompetenciesViewModel.cs new file mode 100644 index 0000000000..38b9e87765 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/AddCompetenciesViewModel.cs @@ -0,0 +1,33 @@ +namespace DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +{ + using DigitalLearningSolutions.Data.Models.CompetencyAssessments; + using DigitalLearningSolutions.Data.Models.Frameworks; + using DigitalLearningSolutions.Web.Helpers; + using System.Collections.Generic; + public class AddCompetenciesViewModel + { + public AddCompetenciesViewModel(CompetencyAssessmentBase competencyAssessmentBase, IEnumerable groupedCompetencies, IEnumerable ungroupedCompetencies, int frameworkId, string? frameworkName, int[] selectedFrameworkCompetencies) + { + ID = competencyAssessmentBase.ID; + CompetencyAssessmentName = competencyAssessmentBase.CompetencyAssessmentName; + UserRole = competencyAssessmentBase.UserRole; + VocabularySingular = FrameworkVocabularyHelper.VocabularySingular(competencyAssessmentBase.Vocabulary); + VocabularyPlural = FrameworkVocabularyHelper.VocabularyPlural(competencyAssessmentBase.Vocabulary); + GroupedCompetencies = groupedCompetencies; + UngroupedCompetencies = ungroupedCompetencies; + FrameworkId = frameworkId; + FrameworkName = frameworkName; + SelectedCompetencyIds = selectedFrameworkCompetencies; + } + public int ID { get; set; } + public string CompetencyAssessmentName { get; set; } + public int UserRole { get; set; } + public string VocabularySingular { get; set; } + public string VocabularyPlural { get; set; } + public IEnumerable GroupedCompetencies { get; set; } + public IEnumerable UngroupedCompetencies { get; set; } + public int[] SelectedCompetencyIds { get; set; } + public int FrameworkId { get; set; } + public string? FrameworkName { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/CompetencyAssessmentFeaturesViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/CompetencyAssessmentFeaturesViewModel.cs new file mode 100644 index 0000000000..b4279f5c27 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/CompetencyAssessmentFeaturesViewModel.cs @@ -0,0 +1,52 @@ +using DigitalLearningSolutions.Data.Models.CompetencyAssessments; +using System.Collections.Generic; + +namespace DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +{ + public class CompetencyAssessmentFeaturesViewModel + { + public CompetencyAssessmentFeaturesViewModel() + { } + public CompetencyAssessmentFeaturesViewModel(int id, string competencyAssessmentName, int userRole, int? frameworkId) + { + ID = id; + CompetencyAssessmentName = competencyAssessmentName; + UserRole = userRole; + FrameworkId = frameworkId; + } + + public CompetencyAssessmentFeaturesViewModel(CompetencyAssessmentFeaturesViewModel features) + { + ID = features.ID; + CompetencyAssessmentName = features.CompetencyAssessmentName; + DescriptionStatus = features.DescriptionStatus; + ProviderandCategoryStatus = features.ProviderandCategoryStatus; + VocabularyStatus = features.VocabularyStatus; + WorkingGroupStatus = features.WorkingGroupStatus; + AllframeworkCompetenciesStatus = features.AllframeworkCompetenciesStatus; + FrameworkId = features.FrameworkId.Value; + } + public int ID { get; set; } + public string CompetencyAssessmentName { get; set; } = string.Empty; + public int UserRole { get; set; } + public bool DescriptionStatus { get; set; } + public bool ProviderandCategoryStatus { get; set; } + public bool VocabularyStatus { get; set; } + public bool WorkingGroupStatus { get; set; } + public bool AllframeworkCompetenciesStatus { get; set; } + public int? FrameworkId { get; set; } + public IEnumerable SelectedFeatures + { + get + { + var features = new List(); + if (DescriptionStatus) features.Add("Description"); + if (ProviderandCategoryStatus) features.Add("Provider and category"); + if (VocabularyStatus) features.Add("Vocabulary"); + if (WorkingGroupStatus) features.Add("Working group"); + if (AllframeworkCompetenciesStatus) features.Add("All framework competencies"); + return features; + } + } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/ConfirmRemoveFrameworkSourceViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/ConfirmRemoveFrameworkSourceViewModel.cs new file mode 100644 index 0000000000..1a4e78cc79 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/ConfirmRemoveFrameworkSourceViewModel.cs @@ -0,0 +1,29 @@ +using DigitalLearningSolutions.Data.Models.CompetencyAssessments; +using DigitalLearningSolutions.Data.Models.Frameworks; +using DigitalLearningSolutions.Web.Attributes; + +namespace DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +{ + public class ConfirmRemoveFrameworkSourceViewModel + { + public ConfirmRemoveFrameworkSourceViewModel() { } + public ConfirmRemoveFrameworkSourceViewModel(CompetencyAssessmentBase competencyAssessmentBase, DetailFramework framework, int competencyCount) + { + CompetencyAssessmentId = competencyAssessmentBase.ID; + CompetencyCount = competencyCount; + AssessmentName = competencyAssessmentBase.CompetencyAssessmentName; + FrameworkName = framework.FrameworkName; + FrameworkId = framework.ID; + Vocabulary = competencyAssessmentBase.Vocabulary; + } + public int CompetencyAssessmentId { get; set; } + public string? AssessmentName { get; set; } + public string? FrameworkName { get; set; } + public int FrameworkId { get; set; } + public int CompetencyCount { get; set; } + public string? Vocabulary { get; set; } + [BooleanMustBeTrue(ErrorMessage = "You must confirm that you wish to remove this framework")] + public bool Confirm { get; set; } + + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/ManageCompetencyAssessmentViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/ManageCompetencyAssessmentViewModel.cs index a4a3f41115..05c0a7a846 100644 --- a/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/ManageCompetencyAssessmentViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/ManageCompetencyAssessmentViewModel.cs @@ -1,4 +1,5 @@ using DigitalLearningSolutions.Data.Models.CompetencyAssessments; +using DigitalLearningSolutions.Web.Helpers; namespace DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments { @@ -13,12 +14,14 @@ CompetencyAssessmentTaskStatus competencyAssessmentTaskStatus PublishStatusID = competencyAssessmentBase.PublishStatusID; UserRole = competencyAssessmentBase.UserRole; CompetencyAssessmentTaskStatus = competencyAssessmentTaskStatus; - Vocabulary = competencyAssessmentBase.Vocabulary; + VocabularySingular = FrameworkVocabularyHelper.VocabularySingular(competencyAssessmentBase.Vocabulary); + VocabularyPlural = FrameworkVocabularyHelper.VocabularyPlural(competencyAssessmentBase.Vocabulary); } public string CompetencyAssessmentName { get; set; } public int PublishStatusID { get; set; } public int UserRole { get; set; } - public string? Vocabulary { get; set; } + public string VocabularySingular { get; set; } + public string VocabularyPlural { get; set; } public CompetencyAssessmentTaskStatus CompetencyAssessmentTaskStatus { get; set; } } } diff --git a/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/SelectFrameworkSourcesFormData.cs b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/SelectFrameworkSourcesFormData.cs new file mode 100644 index 0000000000..6f7de8cfdd --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/SelectFrameworkSourcesFormData.cs @@ -0,0 +1,12 @@ +namespace DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +{ + using System.ComponentModel.DataAnnotations; + public class SelectFrameworkSourcesFormData + { + [Required(ErrorMessage = "Select a framework")] + public int FrameworkId { get; set; } + public int CompetencyAssessmentId { get; set; } + public bool? TaskStatus { get; set; } + public string? ActionName { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/SelectFrameworkSourcesViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/SelectFrameworkSourcesViewModel.cs new file mode 100644 index 0000000000..8d1adea226 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/SelectFrameworkSourcesViewModel.cs @@ -0,0 +1,40 @@ +using DigitalLearningSolutions.Data.Models.CompetencyAssessments; +using DigitalLearningSolutions.Data.Models.Frameworks; +using System.Collections.Generic; +using System.Linq; + +namespace DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +{ + public class SelectFrameworkSourcesViewModel : SelectFrameworkSourcesFormData + { + public SelectFrameworkSourcesViewModel() { } + public SelectFrameworkSourcesViewModel(CompetencyAssessmentBase competencyAssessmentBase, IEnumerable frameworks, int[] additionalFrameworksIds, int? primaryFramework, bool? taskStatus, string actionName) + { + ID = competencyAssessmentBase.ID; + CompetencyAssessmentName = competencyAssessmentBase.CompetencyAssessmentName; + UserRole = competencyAssessmentBase.UserRole; + TaskStatus = taskStatus; + PrimaryFramework = frameworks.FirstOrDefault(f => f.ID == primaryFramework); + var excludedIds = new HashSet(additionalFrameworksIds); + if (primaryFramework.HasValue) + { + excludedIds.Add(primaryFramework.Value); + } + Frameworks = [.. frameworks + .Where(f => !excludedIds.Contains(f.ID)) + .OrderBy(f => f.FrameworkName)]; + AdditionalFrameworks = [.. additionalFrameworksIds.Select(id => frameworks.First(f => f.ID == id))]; + ActionName = actionName; + } + public IEnumerable Frameworks { get; set; } + public IEnumerable AdditionalFrameworks { get; set; } + public BrandedFramework? PrimaryFramework { get; set; } + public IEnumerable Roles { get; set; } + public int ID { get; set; } + public string CompetencyAssessmentName { get; set; } + public int UserRole { get; set; } + public string? GroupName { get; set; } + public string? SubGroupName { get; set; } + public string? RoleName { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/ViewSelectedCompetenciesFormData.cs b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/ViewSelectedCompetenciesFormData.cs new file mode 100644 index 0000000000..467a9845e1 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/ViewSelectedCompetenciesFormData.cs @@ -0,0 +1,8 @@ +namespace DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +{ + public class ViewSelectedCompetenciesFormData + { + public int ID { get; set; } + public bool? TaskStatus { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/ViewSelectedCompetenciesViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/ViewSelectedCompetenciesViewModel.cs new file mode 100644 index 0000000000..dc03de2eba --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/CompetencyAssessments/ViewSelectedCompetenciesViewModel.cs @@ -0,0 +1,46 @@ +using DigitalLearningSolutions.Data.Models.CompetencyAssessments; +using DigitalLearningSolutions.Web.Helpers; +using System.Collections.Generic; +using System.Linq; + +namespace DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +{ + + public class ViewSelectedCompetenciesViewModel : ViewSelectedCompetenciesFormData + { + public ViewSelectedCompetenciesViewModel() { } + public ViewSelectedCompetenciesViewModel(CompetencyAssessmentBase competencyAssessmentBase, IEnumerable competencies, IEnumerable linkedFrameworks, bool? taskStatus) + { + ID = competencyAssessmentBase.ID; + TaskStatus = taskStatus; + CompetencyAssessmentName = competencyAssessmentBase.CompetencyAssessmentName; + UserRole = competencyAssessmentBase.UserRole; + Competencies = competencies; + VocabularySingular = FrameworkVocabularyHelper.VocabularySingular(competencyAssessmentBase.Vocabulary); + VocabularyPlural = FrameworkVocabularyHelper.VocabularyPlural(competencyAssessmentBase.Vocabulary); + var competencyCounts = competencies + .GroupBy(c => c.FrameworkId) + .ToDictionary(g => g.Key, g => g.Count()); + // Populate LinkedFrameworks with the relevant count + foreach (var framework in linkedFrameworks) + { + if (competencyCounts.TryGetValue(framework.ID, out var count)) + { + framework.AssessmentFrameworkCompetencyCount = count; + } + else + { + framework.AssessmentFrameworkCompetencyCount = 0; + } + } + LinkedFrameworks = linkedFrameworks; + } + + public string CompetencyAssessmentName { get; set; } + public int UserRole { get; set; } + public string VocabularySingular { get; set; } + public string VocabularyPlural { get; set; } + public IEnumerable Competencies { get; set; } + public IEnumerable LinkedFrameworks { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/Frameworks/Import/AddQuestionsToWhichCompetenciesViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/Frameworks/Import/AddQuestionsToWhichCompetenciesViewModel.cs index fa86ccc30a..ce55e56f6c 100644 --- a/DigitalLearningSolutions.Web/ViewModels/Frameworks/Import/AddQuestionsToWhichCompetenciesViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/Frameworks/Import/AddQuestionsToWhichCompetenciesViewModel.cs @@ -1,5 +1,6 @@ using DigitalLearningSolutions.Web.Helpers; using System.Collections.Generic; +using System.Linq; using System.Runtime.Versioning; namespace DigitalLearningSolutions.Web.ViewModels.Frameworks.Import @@ -33,7 +34,7 @@ public AddQuestionsToWhichCompetenciesViewModel public string FrameworkVocabularySingular { get; set; } public string FrameworkVocabularyPlural { get; set; } public int TotalQuestions { get; set; } - public int AddAssessmentQuestionsOption { get; set; } = 1; //1 = only added, 2 = added and updated, 3 = all uploaded + public int AddAssessmentQuestionsOption { get; set; } //1 = only added, 2 = added and updated, 3 = all uploaded public int CompetenciesToProcessCount { get; set; } public int CompetenciesToAddCount { get; set; } public int CompetenciesToUpdateCount { get; set; } diff --git a/DigitalLearningSolutions.Web/ViewModels/Frameworks/Import/ImportCompetenciesFormData.cs b/DigitalLearningSolutions.Web/ViewModels/Frameworks/Import/ImportCompetenciesFormData.cs index 98604cc8ee..327ea8c2af 100644 --- a/DigitalLearningSolutions.Web/ViewModels/Frameworks/Import/ImportCompetenciesFormData.cs +++ b/DigitalLearningSolutions.Web/ViewModels/Frameworks/Import/ImportCompetenciesFormData.cs @@ -6,8 +6,8 @@ public class ImportCompetenciesFormData { - [Required(ErrorMessage = "Import competencies file is required")] - [AllowedExtensions(new[] { ".xlsx" }, "Import competencies file must be in xlsx format")] + [Required(ErrorMessage = "Import file is required")] + [AllowedExtensions([".xlsx"], "Import file must be in xlsx format")] [MaxFileSize(5 * 1024 * 1024, "Maximum allowed file size is 5MB")] public IFormFile? ImportFile { get; set; } } diff --git a/DigitalLearningSolutions.Web/ViewModels/LearningPortal/Available/RetirementViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/LearningPortal/Available/RetirementViewModel.cs new file mode 100644 index 0000000000..bfbba45cb6 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/LearningPortal/Available/RetirementViewModel.cs @@ -0,0 +1,25 @@ +using DigitalLearningSolutions.Web.Attributes; +using System; + +namespace DigitalLearningSolutions.Web.ViewModels.LearningPortal.Available +{ + + public class RetirementViewModel + { + public RetirementViewModel() + { + + } + public RetirementViewModel(int selfAssessmentId, DateTime? retirementDate, string name) + { + SelfAssessmentId = selfAssessmentId; + RetirementDate = retirementDate; + Name = name; + } + public string Name { get; set; } = string.Empty; + public int SelfAssessmentId { get; set; } + public DateTime? RetirementDate { get; set; } + [BooleanMustBeTrue(ErrorMessage = "Please tick the checkbox to confirm you wish to perform this action")] + public bool ActionConfirmed { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentDescriptionViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentDescriptionViewModel.cs index eb11408029..e4f7c1d11b 100644 --- a/DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentDescriptionViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentDescriptionViewModel.cs @@ -18,6 +18,7 @@ public class SelfAssessmentDescriptionViewModel public readonly string VocabPlural; public readonly string? Vocabulary; public readonly bool NonReportable; + public bool SelfAssessmentProcessAgreed { get; set; } public SelfAssessmentDescriptionViewModel( CurrentSelfAssessment selfAssessment, @@ -37,6 +38,7 @@ List supervisors Vocabulary = selfAssessment.Vocabulary; VocabPlural = FrameworkVocabularyHelper.VocabularyPlural(selfAssessment.Vocabulary); NonReportable = selfAssessment.NonReportable; + SelfAssessmentProcessAgreed = selfAssessment.SelfAssessmentProcessAgreed; } public List Supervisors { get; set; } diff --git a/DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentProcessViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentProcessViewModel.cs new file mode 100644 index 0000000000..0a63eda050 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentProcessViewModel.cs @@ -0,0 +1,18 @@ +namespace DigitalLearningSolutions.Web.ViewModels.LearningPortal.SelfAssessments +{ + using System.Collections.Generic; + using DigitalLearningSolutions.Data.Models.SelfAssessments; + using DigitalLearningSolutions.Web.Attributes; + using DigitalLearningSolutions.Web.Helpers; + + public class SelfAssessmentProcessViewModel + { + public int SelfAssessmentID { get; set; } + [BooleanMustBeTrue(ErrorMessage = "Please tick the checkbox to confirm that you understand and agree to the self-assessment process")] + public bool ActionConfirmed { get; set; } + + public string? VocabPlural { get; set; } + public string? Vocabulary { get; set; } + + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Administrators/SearchableAdminAccountsViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Administrators/SearchableAdminAccountsViewModel.cs index 0afcfe7498..88f5ea2889 100644 --- a/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Administrators/SearchableAdminAccountsViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Administrators/SearchableAdminAccountsViewModel.cs @@ -5,7 +5,7 @@ using DigitalLearningSolutions.Data.Models.User; using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Web.ViewModels.Common.SearchablePage; - + using DateHelper = Helpers.DateHelper; public class SearchableAdminAccountsViewModel : BaseFilterableViewModel { public readonly bool CanShowDeleteAdminButton; @@ -28,7 +28,10 @@ ReturnPageQuery returnPageQuery IsLocked = admin.UserAccount?.FailedLoginCount >= AuthHelper.FailedLoginThreshold; IsAdminActive = admin.AdminAccount.Active; IsUserActive = admin.UserAccount.Active; - + if (admin.LastAccessed.HasValue) + { + LastAccessed = admin.LastAccessed.Value.ToString(DateHelper.StandardDateFormat); + } CanShowDeactivateAdminButton = IsAdminActive && admin.AdminIdReferenceCount > 0; CanShowDeleteAdminButton = admin.AdminIdReferenceCount == 0; @@ -47,6 +50,7 @@ ReturnPageQuery returnPageQuery public bool IsLocked { get; set; } public bool IsAdminActive { get; set; } public bool IsUserActive { get; set; } + public string? LastAccessed { get; set; } public ReturnPageQuery ReturnPageQuery { get; set; } } } diff --git a/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Centres/EditCentreDetailsSuperAdminViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Centres/EditCentreDetailsSuperAdminViewModel.cs index 8218658e96..9000f85285 100644 --- a/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Centres/EditCentreDetailsSuperAdminViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Centres/EditCentreDetailsSuperAdminViewModel.cs @@ -19,6 +19,7 @@ public EditCentreDetailsSuperAdminViewModel(Centre centre) IpPrefix = centre.IpPrefix?.Trim(); ShowOnMap = centre.ShowOnMap; RegionId = centre.RegionId; + RegistrationEmail = centre.RegistrationEmail; } public int CentreId { get; set; } @@ -37,5 +38,10 @@ public EditCentreDetailsSuperAdminViewModel(Centre centre) [RegularExpression(@"^[\d.,\s]+$", ErrorMessage = "IP Prefix can contain only digits, stops, commas and spaces")] public string? IpPrefix { get; set; } public bool ShowOnMap { get; set; } + + [MaxLength(250, ErrorMessage = "Email must be 250 characters or fewer")] + [EmailAddress(ErrorMessage = "Enter an email in the correct format, like name@example.com")] + [NoWhitespace(ErrorMessage = "Email must not contain any whitespace characters")] + public string? RegistrationEmail { get; set; } } } diff --git a/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Delegates/SearchableDelegatesViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Delegates/SearchableDelegatesViewModel.cs index 8ea0ed22e5..a615461e73 100644 --- a/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Delegates/SearchableDelegatesViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Delegates/SearchableDelegatesViewModel.cs @@ -28,6 +28,7 @@ ReturnPageQuery returnPageQuery LearningHubID = delegates.LearningHubAuthId; AccountClaimed = delegates.RegistrationConfirmationHash; DateRegistered = delegates.DateRegistered?.ToString(Data.Helpers.DateHelper.StandardDateFormat); + LastAccessed = delegates.LastAccessed?.ToString(Data.Helpers.DateHelper.StandardDateFormat); SelRegistered = delegates.SelfReg; IsDelegateActive = delegates.Active; IsCentreEmailVerified = delegates.CentreEmailVerified == null ? false : true; @@ -52,6 +53,7 @@ ReturnPageQuery returnPageQuery public bool IsLocked { get; set; } public string AccountClaimed { get; set; } public string? DateRegistered { get; set; } + public string? LastAccessed { get; set; } public bool SelRegistered { get; set; } public bool IsDelegateActive { get; set; } public bool IsUserActive { get; set; } diff --git a/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Users/SearchableUserAccountViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Users/SearchableUserAccountViewModel.cs index 5b1e7e63ac..263e008165 100644 --- a/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Users/SearchableUserAccountViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Users/SearchableUserAccountViewModel.cs @@ -27,6 +27,10 @@ ReturnPageQuery returnPageQuery ProfessionalRegistrationNumber = user.UserAccount.ProfessionalRegistrationNumber; LearningHubAuthId = user.UserAccount.LearningHubAuthId; ReturnPageQuery = returnPageQuery; + if (user.UserAccount.LastAccessed.HasValue) + { + LastAccessed = user.UserAccount.LastAccessed.Value.ToString(DateHelper.StandardDateFormat); + } } public int Id { get; set; } @@ -51,6 +55,7 @@ ReturnPageQuery returnPageQuery public int? LearningHubAuthId { get; set; } + public string? LastAccessed { get; set; } public ReturnPageQuery ReturnPageQuery { get; set; } } } diff --git a/DigitalLearningSolutions.Web/ViewModels/Supervisor/EnrolDelegateSetCompletByDateViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/Supervisor/EnrolDelegateSetCompletByDateViewModel.cs index e8582ca012..e5bca1fe3f 100644 --- a/DigitalLearningSolutions.Web/ViewModels/Supervisor/EnrolDelegateSetCompletByDateViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/Supervisor/EnrolDelegateSetCompletByDateViewModel.cs @@ -11,5 +11,6 @@ public class EnrolDelegateSetCompletByDateViewModel public CompetencyAssessment CompetencyAssessment { get; set; } public DateTime? CompleteByDate { get; set; } public OldDateValidator.ValidationResult? CompleteByValidationResult { get; set; } + public bool ActionConfirmed { get; set; } } } diff --git a/DigitalLearningSolutions.Web/ViewModels/Supervisor/RetiringSelfAssessmentViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/Supervisor/RetiringSelfAssessmentViewModel.cs new file mode 100644 index 0000000000..06bab1bc50 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/Supervisor/RetiringSelfAssessmentViewModel.cs @@ -0,0 +1,14 @@ +namespace DigitalLearningSolutions.Web.ViewModels.Supervisor +{ + using DigitalLearningSolutions.Web.Attributes; + using System; + + public class RetiringSelfAssessmentViewModel + { + public int SelfAssessmentID { get; set; } + public int SupervisorDelegateID { get; set; } + public DateTime? RetirementDate { get; set; } + [BooleanMustBeTrue(ErrorMessage = "Please tick the checkbox to confirm you wish to perform this action")] + public bool ActionConfirmed { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/Supervisor/SupervisorDashboardViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/Supervisor/SupervisorDashboardViewModel.cs index 301e96fe77..067803fd78 100644 --- a/DigitalLearningSolutions.Web/ViewModels/Supervisor/SupervisorDashboardViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/Supervisor/SupervisorDashboardViewModel.cs @@ -8,5 +8,6 @@ public class SupervisorDashboardViewModel public string? BannerText; public DashboardData DashboardData { get; set; } public IEnumerable SupervisorDashboardToDoItems { get; set; } + public bool ShowTableauLink { get; set; } = false; } } diff --git a/DigitalLearningSolutions.Web/ViewModels/Support/RequestSupportTicket/RequestSummaryViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/Support/RequestSupportTicket/RequestSummaryViewModel.cs index 0d7cd9a65f..54b5598e23 100644 --- a/DigitalLearningSolutions.Web/ViewModels/Support/RequestSupportTicket/RequestSummaryViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/Support/RequestSupportTicket/RequestSummaryViewModel.cs @@ -18,9 +18,13 @@ public RequestSummaryViewModel(RequestSupportTicketData data) RequestDescription = data.RequestDescription; } [MaxLength(250, ErrorMessage = "Summary must be 250 characters or fewer")] + [Required(ErrorMessage = "Please enter request summary")] public string? RequestSubject { get; set; } + public string? RequestDescription { get; set; } + public int? RequestTypeId { get; set; } + public string? RequestType { get; set; } } } diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/Administrator/SearchableAdminViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/Administrator/SearchableAdminViewModel.cs index 7e4ab30b47..c3da68c395 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/Administrator/SearchableAdminViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/Administrator/SearchableAdminViewModel.cs @@ -5,6 +5,8 @@ using DigitalLearningSolutions.Data.Models.User; using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Web.ViewModels.Common.SearchablePage; + using System; + using DateHelper = Helpers.DateHelper; public class SearchableAdminViewModel : BaseFilterableViewModel { @@ -23,6 +25,10 @@ ReturnPageQuery returnPageQuery EmailAddress = admin.EmailForCentreNotifications; IsLocked = admin.UserAccount.FailedLoginCount >= AuthHelper.FailedLoginThreshold; IsActive = admin.AdminAccount.Active; + if (admin.LastAccessed.HasValue) + { + LastAccessed = admin.LastAccessed.Value.ToString(DateHelper.StandardDateFormat); + } CanShowDeactivateAdminButton = UserPermissionsHelper.LoggedInAdminCanDeactivateUser(admin.AdminAccount, loggedInAdminAccount); @@ -46,6 +52,8 @@ ReturnPageQuery returnPageQuery public bool IsActive { get; set; } + public string? LastAccessed { get; set; } + public ReturnPageQuery ReturnPageQuery { get; set; } } } diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/Reports/SelfAssessmentReportsViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/Reports/SelfAssessmentReportsViewModel.cs index 6947aad14b..dc4da36717 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/Reports/SelfAssessmentReportsViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/Reports/SelfAssessmentReportsViewModel.cs @@ -6,15 +6,19 @@ public class SelfAssessmentReportsViewModel { public SelfAssessmentReportsViewModel( - IEnumerable selfAssessmentSelects, int? adminCategoryId, int categoryId + IEnumerable selfAssessmentSelects, int? adminCategoryId, int categoryId, bool dSATreportIsPublish, bool showTableauLink ) { SelfAssessmentSelects = selfAssessmentSelects; AdminCategoryId = adminCategoryId; CategoryId = categoryId; + DSATreportIsPublish = dSATreportIsPublish; + ShowTableauLink = showTableauLink; } public IEnumerable SelfAssessmentSelects { get; set; } public int? AdminCategoryId { get; set; } public int CategoryId { get; set; } + public bool DSATreportIsPublish { get; set; } + public bool ShowTableauLink { get; set; } = false; } } diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/AllDelegates/AllDelegatesViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/AllDelegates/AllDelegatesViewModel.cs index 63a027e4d4..f6a8141dcc 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/AllDelegates/AllDelegatesViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/AllDelegates/AllDelegatesViewModel.cs @@ -44,6 +44,7 @@ IEnumerable availableFilters public override IEnumerable<(string, string)> SortOptions { get; } = new[] { DelegateSortByOptions.Name, + DelegateSortByOptions.LastAccessed, DelegateSortByOptions.RegistrationDate, }; diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/Shared/DelegateInfoViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/Shared/DelegateInfoViewModel.cs index 504fd4b454..814e488ac9 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/Shared/DelegateInfoViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/Shared/DelegateInfoViewModel.cs @@ -41,6 +41,10 @@ IEnumerable delegateRegistrationPrompts { RegistrationDate = delegateUser.DateRegistered.Value.ToString(DateHelper.StandardDateFormat); } + if (delegateUser.LastAccessed.HasValue) + { + LastAccessed = delegateUser.LastAccessed.Value.ToString(DateHelper.StandardDateFormat); + } DelegateRegistrationPrompts = delegateRegistrationPrompts; RegistrationConfirmationHash = delegateUser.RegistrationConfirmationHash; @@ -61,6 +65,7 @@ IEnumerable delegateRegistrationPrompts public int JobGroupId { get; set; } public string? JobGroup { get; set; } public string? RegistrationDate { get; set; } + public string? LastAccessed { get; set; } public string ProfessionalRegistrationNumber { get; set; } public string? RegistrationConfirmationHash { get; set; } diff --git a/DigitalLearningSolutions.Web/Views/ApplicationSelector/Index.cshtml b/DigitalLearningSolutions.Web/Views/ApplicationSelector/Index.cshtml index 11c835815f..1b4bb670fb 100644 --- a/DigitalLearningSolutions.Web/Views/ApplicationSelector/Index.cshtml +++ b/DigitalLearningSolutions.Web/Views/ApplicationSelector/Index.cshtml @@ -17,20 +17,17 @@ {
    -
    +

    Learning Portal

    Access to your current, available and completed learning courses.

    -
    -
    -
    @@ -41,7 +38,7 @@ {
    -
    +

    Tracking System @@ -51,15 +48,12 @@

    Manage and distribute learning to your organisation and access reports.

    -
    -
    -
    @@ -69,7 +63,7 @@
    -
    +

    Legacy Tracking System

    @@ -77,18 +71,15 @@ Access the old Tracking System if you can't find the functionality you need in the new one. Please raise a ticket to tell us about any missing functionality, too.

    -
    -
    -
    +
    @@ -98,20 +89,16 @@ {
    -
    +

    Content Management System

    Import and manage learning content that's delivered through the Digital Learning Solutions platform.

    -
    -
    -
    @@ -122,20 +109,16 @@ {
    -
    +

    Supervise

    -

    Assign and review staff competency assessments and arrange supervision sessions.

    -
    -
    -

    Assign and review staff profile assessments and arrange supervision sessions.

    +
    @@ -146,20 +129,16 @@ {
    -
    +

    Content Creator

    Create interactive elearning and assessments.

    -
    -
    -
    @@ -170,20 +149,16 @@ {
    -
    +

    Frameworks

    -

    Create and distribute competency frameworks and assessments.

    -
    -
    -

    Create and distribute competency frameworks and role profiles.

    +
    @@ -194,7 +169,7 @@ {
    -
    +

    Super Admin @@ -204,15 +179,11 @@

    Manage content and settings across the whole system.

    -
    -
    -
    diff --git a/DigitalLearningSolutions.Web/Views/CompetencyAssessments/AddCompetencies.cshtml b/DigitalLearningSolutions.Web/Views/CompetencyAssessments/AddCompetencies.cshtml new file mode 100644 index 0000000000..8616052934 --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/CompetencyAssessments/AddCompetencies.cshtml @@ -0,0 +1,119 @@ +@using DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +@model AddCompetenciesViewModel; +@{ + ViewData["Title"] = "Add Competencies to Self Assessment"; + ViewData["Application"] = "Framework Service"; +} + +@section NavMenuItems { + +} + +@section NavBreadcrumbs { + +} + +
    + +
    + +

    + Add @Model.VocabularyPlural.ToLower() to assessment from @Model.FrameworkName +

    +
    + + @if (Model.GroupedCompetencies.Count() > 0) + { + @foreach (var competencyGroup in Model.GroupedCompetencies) + { + @if (competencyGroup.FrameworkCompetencies.Count() > 1) + { +
    +

    + @competencyGroup.Name +

    + @if (competencyGroup.FrameworkCompetencies.Count() > 1) + { + + } +
    + @foreach (var competency in competencyGroup.FrameworkCompetencies) + { +
    + + +
    + } +
    +
    + } + } + } + + @if (Model.UngroupedCompetencies.Any()) + { +

    + Ungrouped @Model.VocabularyPlural.ToLower() +

    + @if (Model.UngroupedCompetencies.Count() > 1) + { + + } + @foreach (var competency in Model.UngroupedCompetencies) + { + +
    + + +
    + + } + } + +
    +
    + +
    +@section scripts { + +} diff --git a/DigitalLearningSolutions.Web/Views/CompetencyAssessments/AddCompetenciesSelectFramework.cshtml b/DigitalLearningSolutions.Web/Views/CompetencyAssessments/AddCompetenciesSelectFramework.cshtml new file mode 100644 index 0000000000..1bb44950b3 --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/CompetencyAssessments/AddCompetenciesSelectFramework.cshtml @@ -0,0 +1,54 @@ +@using DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +@model AddCompetenciesSelectFrameworkViewModel; +@{ + ViewData["Title"] = "Select Framework Source"; + ViewData["Application"] = "Framework Service"; +} + +@section NavMenuItems { + +} + +@section NavBreadcrumbs { + +} +

    Select Framework Source

    +
    + @if (!ViewData.ModelState.IsValid) + { + + } +
    + +
    + +

    + Select the framework source you would like to use to add @Model.VocabularyPlural.ToLower() to this competency assessment. +

    +
    +
    + @foreach (var framework in Model.LinkedFrameworks) + { +
    + + +
    + } +
    +
    + +
    + + + diff --git a/DigitalLearningSolutions.Web/Views/CompetencyAssessments/CompetencyAssessmentFeatures.cshtml b/DigitalLearningSolutions.Web/Views/CompetencyAssessments/CompetencyAssessmentFeatures.cshtml new file mode 100644 index 0000000000..1e5ef6a629 --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/CompetencyAssessments/CompetencyAssessmentFeatures.cshtml @@ -0,0 +1,64 @@ +@using DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +@model CompetencyAssessmentFeaturesViewModel +@{ + ViewData["Title"] = "Competency assessment features "; + ViewData["Application"] = "Framework Service"; + ViewData["HeaderPathName"] = "Framework Service"; +} + +@section NavMenuItems { + +} + @section NavBreadcrumbs { + +} + +

    Create a new competency assessment

    +

    Which features of the framework do you want to copy into the competency assessment?

    +

    The framework itself will be used as the source for this self-assessment.

    +

    Select any additional features you want to copy into the competency assessment. You can edit all features later if needed.

    +
    + @if (!ViewData.ModelState.IsValid) + { + + } + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DigitalLearningSolutions.Web/Views/CompetencyAssessments/CompetencyAssessmentSummary.cshtml b/DigitalLearningSolutions.Web/Views/CompetencyAssessments/CompetencyAssessmentSummary.cshtml new file mode 100644 index 0000000000..be9f1d0e0d --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/CompetencyAssessments/CompetencyAssessmentSummary.cshtml @@ -0,0 +1,81 @@ +@using DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +@model CompetencyAssessmentFeaturesViewModel +@{ + ViewData["Title"] = "Competency assessment features summary"; + ViewData["Application"] = "Framework Service"; + ViewData["HeaderPathName"] = "Framework Service"; +} + +@section NavMenuItems { + +} +@section NavBreadcrumbs { + +} + + +

    Check your answer before creating competency assessment

    +
    + @if (!ViewData.ModelState.IsValid) + { + + } +
    +
    + +
    +
    + Assessment name +
    +
    + @Model.CompetencyAssessmentName +
    +
    + + Change assessment name + +
    +
    +
    +
    + Features to copy from framework +
    +
    + @if (Model.SelectedFeatures.Any()) + { +
      + @foreach (var feature in Model.SelectedFeatures) + { +
    • @feature
    • + } +
    + } + else + { + No features selected + } +
    +
    + + Change features + +
    +
    +
    + + + + +
    + + diff --git a/DigitalLearningSolutions.Web/Views/CompetencyAssessments/ConfirmRemoveFrameworkSource.cshtml b/DigitalLearningSolutions.Web/Views/CompetencyAssessments/ConfirmRemoveFrameworkSource.cshtml new file mode 100644 index 0000000000..a20a786820 --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/CompetencyAssessments/ConfirmRemoveFrameworkSource.cshtml @@ -0,0 +1,51 @@ +@using DigitalLearningSolutions.Web.Helpers +@using DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments +@model ConfirmRemoveFrameworkSourceViewModel; +@{ + var errorHasOccurred = !ViewData.ModelState.IsValid; + ViewData["Title"] = "Competency Assessments - Remove source framework"; +} +
    +
    + @if (errorHasOccurred) + { + + } + +

    Remove framework source from @Model.AssessmentName

    +
    +
    + + + +
    +
    +
    + + + + + + +

    + This competency assessment has @Model.CompetencyCount @DisplayStringHelper.PluraliseStringIfRequired(@Model.Vocabulary.ToLower(), Model.CompetencyCount) + associated with it from the framework @Model.FrameworkName. Removing this + framework source will remove the @DisplayStringHelper.PluraliseStringIfRequired(@Model.Vocabulary.ToLower(), Model.CompetencyCount) from the assessment. +

    + + + + + + +
    +
    diff --git a/DigitalLearningSolutions.Web/Views/CompetencyAssessments/EditRoleProfileLinks.cshtml b/DigitalLearningSolutions.Web/Views/CompetencyAssessments/EditRoleProfileLinks.cshtml index b5faad6214..96e78ce8d9 100644 --- a/DigitalLearningSolutions.Web/Views/CompetencyAssessments/EditRoleProfileLinks.cshtml +++ b/DigitalLearningSolutions.Web/Views/CompetencyAssessments/EditRoleProfileLinks.cshtml @@ -119,6 +119,7 @@ + @@ -170,6 +171,7 @@ else if (Model.ActionName == "EditSubGroup" && Model.ProfessionalGroupId != null + @@ -220,6 +222,7 @@ else if (Model.ActionName == "EditRole" && Model.SubGroupId != null) + diff --git a/DigitalLearningSolutions.Web/Views/CompetencyAssessments/ManageCompetencyAssessment.cshtml b/DigitalLearningSolutions.Web/Views/CompetencyAssessments/ManageCompetencyAssessment.cshtml index 3e4f9f34be..f13c584c33 100644 --- a/DigitalLearningSolutions.Web/Views/CompetencyAssessments/ManageCompetencyAssessment.cshtml +++ b/DigitalLearningSolutions.Web/Views/CompetencyAssessments/ManageCompetencyAssessment.cshtml @@ -57,7 +57,7 @@