diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 9b44eff4c..55a7cd812 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,10 +3,10 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "10.0.7", + "version": "10.0.9", "commands": [ "dotnet-ef" ] } } -} +} \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2ea52b1ac..f3306fc90 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,3 +12,12 @@ updates: all-updates: patterns: - "*" + cooldown: + default-days: 7 + - package-ecosystem: "github-actions" + directories: + - "/" + schedule: + interval: "weekly" + cooldown: + default-days: 7 diff --git a/.github/workflows/check-ruleset.yaml b/.github/workflows/check-ruleset.yaml index fed6ee571..17abc9f09 100644 --- a/.github/workflows/check-ruleset.yaml +++ b/.github/workflows/check-ruleset.yaml @@ -1,5 +1,5 @@ # GitHub Actions Workflow: Check Repository Ruleset -# +# # This workflow ensures that the repository's branch protection rules # match the expected ruleset defined in the template repository. # @@ -28,39 +28,18 @@ name: Check Repository Ruleset on: push: - branches: [main] + branches: [dev] pull_request: +permissions: + contents: read jobs: - check-ruleset: - if: github.actor != 'dependabot[bot]' - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Create GitHub App token - id: app-token - uses: actions/create-github-app-token@v3 - with: - app-id: ${{ vars.ORG_ACTIONS_BOT_APP_ID }} - private-key: ${{ secrets.ORG_ACTIONS_BOT_PRIVATE_KEY }} - owner: UoMResearchIT - - - name: Fetch Standard RSE Ruleset - id: expected - uses: UoMResearchIT/actions/get-repo-ruleset@v0.3 - with: - repository: UoMResearchIT/RSE-Repository-Template - token: ${{ steps.app-token.outputs.token }} - - - name: Fetch This Repository's Ruleset - id: current - uses: UoMResearchIT/actions/get-repo-ruleset@v0.3 - - - name: Compare Rulesets - uses: UoMResearchIT/actions/check-ruleset-containment@v0.3 - with: - current: ${{ steps.current.outputs.filename }} - expected: ${{ steps.expected.outputs.filename }} + check: + if: github.repository_owner == 'UoMResearchIT' + name: Check Ruleset + uses: UoMResearchIT/actions/.github/workflows/check-ruleset.yaml@c1106820e385a14b9cfa3c4682b1a1ac11354968 # main + with: + bot-id: ${{ vars.ORG_ACTIONS_BOT_APP_ID }} + ignore: '["require_code_owner_review"]' + secrets: + bot-key: ${{ secrets.ORG_ACTIONS_BOT_PRIVATE_KEY }} diff --git a/.github/workflows/docker-images-build-health-check.yml b/.github/workflows/docker-images-build-health-check.yml index b5f3bafa3..5a4dc9562 100644 --- a/.github/workflows/docker-images-build-health-check.yml +++ b/.github/workflows/docker-images-build-health-check.yml @@ -2,10 +2,11 @@ name: Docker Images Build and Health Check on: push: - branches: - - dev + branches: [dev] pull_request: +permissions: + contents: read jobs: build: runs-on: ubuntu-latest @@ -22,17 +23,19 @@ jobs: SUPERUSER_EMAIL: captain.marvel@manchester.ac.uk steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - name: Create state directory for bind mount run: mkdir -p capx-state - name: Build Docker images run: docker compose build - + - name: Start container and wait for health checks run: docker compose up --wait --wait-timeout 120 diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 3155d35d8..d7d6ef584 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -5,9 +5,8 @@ name: C# .NET on: push: - branches: ["dev"] + branches: [dev] pull_request: - branches: ["dev"] jobs: format-check: @@ -16,26 +15,31 @@ jobs: defaults: run: working-directory: ./PPMTool + permissions: + contents: read steps: - name: Create GitHub App token id: app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ vars.ORG_ACTIONS_BOT_APP_ID }} + client-id: ${{ vars.ORG_ACTIONS_BOT_APP_ID }} private-key: ${{ secrets.ORG_ACTIONS_BOT_PRIVATE_KEY }} owner: UoMResearchIT + # NB: No current submodules; the .gitmodules file is empty + repositories: CapX + permission-contents: read - - uses: actions/checkout@v5 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: 'true' token: ${{ steps.app-token.outputs.token }} persist-credentials: false - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 with: dotnet-version: 10.0.x - name: Format - run: dotnet format PPMTool.sln --exclude ../Blazor-ApexCharts/src/Blazor-ApexCharts.csproj --verify-no-changes --no-restore --verbosity detailed \ No newline at end of file + run: dotnet format PPMTool.sln --verify-no-changes --no-restore --verbosity detailed diff --git a/.github/workflows/license-copyright-add.yml b/.github/workflows/license-copyright-add.yml index f5698a16a..eb38abbb9 100644 --- a/.github/workflows/license-copyright-add.yml +++ b/.github/workflows/license-copyright-add.yml @@ -4,12 +4,11 @@ name: Add Licence and Copyright Headers -on: - pull_request +on: pull_request jobs: add-headers: permissions: pull-requests: write contents: write - uses: UoMResearchIT/actions/.github/workflows/apply-reuse.yml@main + uses: UoMResearchIT/actions/.github/workflows/apply-reuse.yml@c1106820e385a14b9cfa3c4682b1a1ac11354968 # main diff --git a/.github/workflows/naming-conventions.yml b/.github/workflows/naming-conventions.yml index 9c20433bd..6919e72a7 100644 --- a/.github/workflows/naming-conventions.yml +++ b/.github/workflows/naming-conventions.yml @@ -5,6 +5,7 @@ on: pull_request: branches: - '**' +permissions: {} jobs: check-repo-name-conventions: @@ -14,26 +15,31 @@ jobs: steps: - name: Check repository name run: | - REPO_NAME=${{ github.repository }} REPO_NAME=${REPO_NAME##*/} if [[ ! "$REPO_NAME" =~ ^([A-Z][A-Za-z0-9]*)(-[A-Z][A-Za-z0-9]*)*$ ]]; then echo "Repository name '$REPO_NAME' does not follow the naming convention established in the RSE Agile Method Handbook." exit 1 fi - + env: + REPO_NAME: ${{ github.repository }} + check-branch-name-conventions: if: github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Check out repository - uses: actions/checkout@v5 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - name: Check branch name run: | - BRANCH_NAME=${{ github.event.pull_request.head.ref }} - if [[ ! "$BRANCH_NAME" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then + if [[ ! "$BRANCH_NAME" =~ ^(copilot\/)?[a-z0-9]+(-[a-z0-9]+)*$ ]]; then echo "Branch name '$BRANCH_NAME' does not follow the naming convention established in the RSE Agile Method Handbook." exit 1 fi - + env: + BRANCH_NAME: ${{ github.event.pull_request.head.ref }} diff --git a/.github/workflows/run-zizmor.yml b/.github/workflows/run-zizmor.yml new file mode 100644 index 000000000..220629574 --- /dev/null +++ b/.github/workflows/run-zizmor.yml @@ -0,0 +1,37 @@ +# Copyright (c) 2026 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: GitHub Actions Security Analysis with zizmor ๐ŸŒˆ + +on: + push: + branches: [dev] + pull_request: + +permissions: {} + +jobs: + zizmor: + name: Scan with Zizmor + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Run zizmor ๐ŸŒˆ + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + with: + advanced-security: false diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 000000000..7c592ea8a --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2026 University of Manchester +# +# SPDX-License-Identifier: apache-2.0 + +# Everything +* @aharwood2 @PhilBradbury @sjmf @philiptharrison @gdonval \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 564f91eda..371db9c9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,10 +22,10 @@ By contributing to this repository, you agree that your contributions will be li This project is open source and welcomes contributions from the wider community. However, its development and release process follows a structured governance model. -- The software is owned and licensed by **The University of Manchester**. -- Strategic direction, release planning, and final decisions on what is included in the codebase are managed by the **dRTP Operational Management Community of Practice**. +- The software was originally developed by **The University of Manchester** and is licensed under an open-source licence. +- Strategic direction, release planning, and final decisions on what is included in the codebase are managed by the **dRTP Operational Management Community of Practice** funded by the UKRI Digital Research Infrastructure Programme via the DisCouRSE Network Plus flexible fund. -This means that while the repository is open to contributions, inclusion in official releases is determined collectively by this group. +This means that while the repository is open to contributions, inclusion in official releases is determined collectively by this group. There is no funded development team to address issues but members of the **dRTP Operational Management Community of Practice** will progress priority items on a best-efforts basis. ## How Contributions Work @@ -48,31 +48,27 @@ If you wish to contribute code: All pull requests: -- Must pass any configured checks -- Must be reviewed and approved by members of the Community of Practice before merging -- May be discussed or refined collaboratively +- Must pass any configured checks. +- Must be reviewed and approved by members of the **dRTP Operational Management Community of Practice** before merging. +- May be discussed or refined collaboratively. ## Decision Making and Releases The **dRTP Operational Management Community of Practice**: -- Reviews proposed changes -- Prioritises development work -- Determines what changes are accepted -- Decides when releases are made +- Reviews proposed changes from the perspective of deployed instances. +- Prioritises development work based on urgency and impact. +- Determines what changes are accepted based on a proportionate review process. +- Decides when releases are made and how they are grouped. -This is similar to governance models used by large open source ecosystems (e.g., .NET), where: - -- The project is open and community-driven -- A defined group maintains responsibility for quality, direction, and release cadence +This is similar to governance models used by large open-source ecosystems (e.g., .NET), where the project is open and community-driven but defined group maintains responsibility for quality, direction, and release cadence. This review and ratification process will happen at least once a month. ## Expectations -Please note: - -- Not all contributions will be accepted -- Feature requests may be deferred or declined based on priorities -- Timelines for review and release are determined by the governing community +- Not all contributions will be accepted. +- Feature requests may be deferred based on priorities or declined. +- Timelines for review and release are determined by the **dRTP Operational Management Community of Practice**. +- Backwards compatibility should be preserved unless the **dRTP Operational Management Community of Practice** decides otherwise. ## Code of Conduct diff --git a/PPMTool.Data/Context/PPMToolContext.cs b/PPMTool.Data/Context/PPMToolContext.cs index 50fd502a6..a98326022 100644 --- a/PPMTool.Data/Context/PPMToolContext.cs +++ b/PPMTool.Data/Context/PPMToolContext.cs @@ -70,6 +70,22 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); + + // Npgsql v6+ maps DateTime -> timestamptz and rejects Kind=Unspecified. + // The codebase uses DateTime.Today/DateTime.Now throughout, which produce + // Kind=Unspecified. Map all DateTime columns to timestamp without time zone + // so Npgsql treats them as local/unspecified rather than requiring UTC. + if (Database.ProviderName == "Npgsql.EntityFrameworkCore.PostgreSQL") + { + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + foreach (var property in entityType.GetProperties() + .Where(p => p.ClrType == typeof(DateTime) || p.ClrType == typeof(DateTime?))) + { + property.SetColumnType("timestamp without time zone"); + } + } + } } /// diff --git a/PPMTool.Data/Entities/BaseBookableItem.cs b/PPMTool.Data/Entities/BaseBookableItem.cs new file mode 100644 index 000000000..409471135 --- /dev/null +++ b/PPMTool.Data/Entities/BaseBookableItem.cs @@ -0,0 +1,25 @@ +๏ปฟ// SPDX-FileCopyrightText: 2026 University of Manchester +// +// SPDX-License-Identifier: apache-2.0 + +using PPMTool.Data.Interfaces; + +namespace PPMTool.Data.Entities +{ + /// + /// An abstract base class for timesheet codes or tasks + /// + public abstract class BaseBookableItem : ILoggableObject + { + /// + /// Whether this item is active and can be booked to + /// + public bool IsActive { get; set; } = true; + + /// + /// Pass the interface on to the concrete classes + /// + /// + public abstract string GetSensibleObjectName(); + } +} diff --git a/PPMTool.Data/Entities/InnateCode.cs b/PPMTool.Data/Entities/InnateCode.cs index ac840d5ec..78c63d092 100644 --- a/PPMTool.Data/Entities/InnateCode.cs +++ b/PPMTool.Data/Entities/InnateCode.cs @@ -3,11 +3,10 @@ // SPDX-License-Identifier: apache-2.0 using System.ComponentModel.DataAnnotations; -using PPMTool.Data.Interfaces; namespace PPMTool.Data.Entities { - public class InnateCode : ILoggableObject + public class InnateCode : BaseBookableItem { public int InnateCodeId { get; set; } @@ -23,11 +22,6 @@ public class InnateCode : ILoggableObject [Required] public string ActivityName { get; set; } = null!; - /// - /// Whether this code is active and can be booked to - /// - public bool IsActive { get; set; } = true; - /// /// Whether this code contains sensitive information that should be restricted to line manager and the person /// @@ -38,6 +32,16 @@ public class InnateCode : ILoggableObject /// public virtual ICollection Tasks { get; set; } = new List(); + /// + /// Method to deactivate all tasks too -- assuming they are deep loaded + /// + public void DeactivateAllTasks() + { + foreach (var task in Tasks) + { + task.IsActive = false; + } + } /// /// Joins the activity code and name together with a hyphen. @@ -52,7 +56,7 @@ public string GetCodeAsString() /// Required implementation to identify this object in the logs /// /// - public string GetSensibleObjectName() + public override string GetSensibleObjectName() { return GetCodeAsString(); } diff --git a/PPMTool.Data/Entities/InnateCodeTask.cs b/PPMTool.Data/Entities/InnateCodeTask.cs index 36b1c43dc..7e2c10c0d 100644 --- a/PPMTool.Data/Entities/InnateCodeTask.cs +++ b/PPMTool.Data/Entities/InnateCodeTask.cs @@ -4,11 +4,10 @@ using System.ComponentModel.DataAnnotations; using PPMTool.Data.Enums; -using PPMTool.Data.Interfaces; namespace PPMTool.Data.Entities { - public class InnateCodeTask : ILoggableObject + public class InnateCodeTask : BaseBookableItem { public int InnateCodeTaskId { get; set; } @@ -26,8 +25,9 @@ public class InnateCodeTask : ILoggableObject /// [Required] public virtual InnateCode InnateCode { get; set; } = null!; + public int InnateCodeId { get; set; } - public string GetSensibleObjectName() + public override string GetSensibleObjectName() { return $"{InnateCode?.GetSensibleObjectName()} | {TaskName}"; } diff --git a/PPMTool.Data/Entities/Person.cs b/PPMTool.Data/Entities/Person.cs index 7823693e6..63d049c5b 100644 --- a/PPMTool.Data/Entities/Person.cs +++ b/PPMTool.Data/Entities/Person.cs @@ -308,5 +308,23 @@ public double GetWorkloadModelTotalOnDate(DateTime date) .OrderBy(x => x.ChangeDate) .LastOrDefault(); } + + /// + /// Shadow property to store a comma-separated list of the person's skill tag names. + /// Used for filtering on the Manage People page. + /// + [NotMapped] + public string SkillTags + { + get => string.Join(", ", OwnedSkills.Select(skill => skill.SkillTag.Name).OrderBy(name => name)); + } + + /// + /// Shadow property to store the current grade of the person based on the person's current WLM and the date. + /// Used for filtering by Grade on the Manage People page currently. + /// + /// + [NotMapped] + public int? CurrentGrade { get => GetGradeOnDate(DateTime.Today); } } } diff --git a/PPMTool.Data/Entities/Project.cs b/PPMTool.Data/Entities/Project.cs index c1fbe84d9..88589a963 100644 --- a/PPMTool.Data/Entities/Project.cs +++ b/PPMTool.Data/Entities/Project.cs @@ -136,6 +136,14 @@ public class Project : BaseTask, ILoggableObject /// public virtual ICollection FundingSources { get; set; } = new List(); + /// + /// Shadow property to allow easy filtering on FundsReceived in datagrids. + /// Requires additional logic in the service layer to populate this property when retrieving projects from the database. + /// + /// + [NotMapped] + public double FundsReceived { get; set; } + /// /// Constructor also adds default status messages /// @@ -325,6 +333,7 @@ public bool HasResourcesWithNoFundingSourceOnRunningTask() /// Whether to update the subtask costs and save to database /// If necessary a set of financial references /// The percentage of top slice to apply from the settings + /// When no financial references are available in the DB with which to update the costs public void UpdateProjectMetaData(bool recomputeSubTaskCosts, IEnumerable financialReferences, float indirectsPercentage) { // Check conditions for cost update diff --git a/PPMTool.Data/Entities/Setting.cs b/PPMTool.Data/Entities/Setting.cs index fe2a46d40..d1ac2fbf9 100644 --- a/PPMTool.Data/Entities/Setting.cs +++ b/PPMTool.Data/Entities/Setting.cs @@ -23,7 +23,8 @@ public class Setting : ILoggableObject public SettingType SettingType { get; set; } /// - /// Shadow property to get the name of the setting type as a string for use in the UI. This is not mapped to the database as it is derived from the SettingType enum. + /// Shadow property to get the name of the setting type as a string for use in the UI. + /// This is not mapped to the database as it is derived from the SettingType enum. /// [NotMapped] public string SettingTypeAsText => SettingType.ToString(); diff --git a/PPMTool.Data/Entities/TimesheetEntry.cs b/PPMTool.Data/Entities/TimesheetEntry.cs index 9afd8997f..704d69287 100644 --- a/PPMTool.Data/Entities/TimesheetEntry.cs +++ b/PPMTool.Data/Entities/TimesheetEntry.cs @@ -35,6 +35,7 @@ public class TimesheetEntry : ILoggableObject /// [Required] public virtual InnateCodeTask InnateCodeTask { get; set; } = null!; + public int InnateCodeTaskId { get; set; } /// /// Represents the number of hours spent on the task on Monday. @@ -72,19 +73,20 @@ public class TimesheetEntry : ILoggableObject public double SundayHours { get; set; } /// - /// Total hours for the week for this entry + /// Total hours for the week for this entry. This must be explicitly updated with the method. /// [NotMapped] public double TotalHours { get; private set; } /// - /// Represents whether this task is part of the user's template + /// Represents whether this task is part of the user's template. This must be explicitly set with the method. /// [NotMapped] - public bool IsInTemplate { get; set; } + public bool IsInTemplate { get; private set; } /// - /// Method to sum up the hours in the entry + /// Method to sum up the hours in the entry and update the TotalHours property. + /// This should be called whenever the hours for any day are modified. /// /// public void UpdateTotalHours() @@ -92,6 +94,15 @@ public void UpdateTotalHours() TotalHours = MondayHours + TuesdayHours + WednesdayHours + ThursdayHours + FridayHours + SaturdayHours + SundayHours; } + /// + /// Sets the IsInTemplate property to indicate whether this task is part of the user's template. + /// + /// + public void SetIsInTemplate(bool isInTemplate) + { + IsInTemplate = isInTemplate; + } + /// /// Returns a useful string to identify the entity /// diff --git a/PPMTool.Data/Enums/SettingType.cs b/PPMTool.Data/Enums/SettingType.cs index 4e898b8c1..52f02547e 100644 --- a/PPMTool.Data/Enums/SettingType.cs +++ b/PPMTool.Data/Enums/SettingType.cs @@ -75,5 +75,9 @@ public enum SettingType [Description("Optional logo that is displayed in the header in dark mode.")] [DefaultSettingValue("")] OrganisationLogoDark = 15, + + [Description("URL to documentation that provides help for the timesheet functionality.")] + [DefaultSettingValue("")] + TimesheetHelpUrl = 16 } } diff --git a/PPMTool.Data/Helpers/AssignmentHelper.cs b/PPMTool.Data/Helpers/AssignmentHelper.cs index dc9a2628b..fda63fc10 100644 --- a/PPMTool.Data/Helpers/AssignmentHelper.cs +++ b/PPMTool.Data/Helpers/AssignmentHelper.cs @@ -134,8 +134,8 @@ public static IEnumerable GetAssignmentChunks( ProjectId = project.RTP, ProjectName = project.Name, LeadRSE = project.ProjectManager?.Name ?? "Unknown", - UpperOrgUnit = project.School.Faculty.Name, - LowerOrgUnit = project.School.Name, + UpperOrgUnit = project.School?.Faculty?.Name ?? "Unknown", + LowerOrgUnit = project.School?.Name ?? "Unknown", PI = project.PI, TaskName = task.Name, StartDate = adjustedTaskStart, diff --git a/PPMTool.Data/PPMTool.Data.csproj b/PPMTool.Data/PPMTool.Data.csproj index 2b1b80df8..6bdf65a08 100644 --- a/PPMTool.Data/PPMTool.Data.csproj +++ b/PPMTool.Data/PPMTool.Data.csproj @@ -13,23 +13,23 @@ SPDX-License-Identifier: apache-2.0 - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/PPMTool.Migrations.PostgreSql/Migrations/20260527120344_FixDateTimeTimezone.Designer.cs b/PPMTool.Migrations.PostgreSql/Migrations/20260527120344_FixDateTimeTimezone.Designer.cs new file mode 100644 index 000000000..da6d38457 --- /dev/null +++ b/PPMTool.Migrations.PostgreSql/Migrations/20260527120344_FixDateTimeTimezone.Designer.cs @@ -0,0 +1,1462 @@ +๏ปฟ// SPDX-FileCopyrightText: 2026 University of Manchester +// +// SPDX-License-Identifier: apache-2.0 + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PPMTool.Data.Context; + +#nullable disable + +namespace PPMTool.Migrations.PostgreSql.Migrations +{ + [DbContext(typeof(PPMToolContext))] + [Migration("20260527120344_FixDateTimeTimezone")] + partial class FixDateTimeTimezone + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("PPMTool.Data.Entities.Absence", b => + { + b.Property("AbsenceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AbsenceId")); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("PersonId") + .HasColumnType("integer"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AbsenceId"); + + b.HasIndex("PersonId"); + + b.ToTable("Absence"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.ApiKey", b => + { + b.Property("ApiKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ApiKeyId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerUserId") + .HasColumnType("integer"); + + b.HasKey("ApiKeyId"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Competency", b => + { + b.Property("CompetencyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CompetencyId")); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedDate") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Grade") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LegacyId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("Objective") + .IsRequired() + .HasColumnType("text"); + + b.Property("Revision") + .HasColumnType("integer"); + + b.Property("RevisionDate") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("CompetencyId"); + + b.ToTable("Competencies"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.CompetencyAssessment", b => + { + b.Property("CompetencyAssessmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CompetencyAssessmentId")); + + b.Property("CompetencyDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompetencyId") + .HasColumnType("integer"); + + b.Property("CompetencyObjective") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompetencyRevision") + .HasColumnType("integer"); + + b.Property("DateCreated") + .IsRequired() + .HasColumnType("text"); + + b.Property("Evidence") + .HasColumnType("text"); + + b.Property("PersonId") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("CompetencyAssessmentId"); + + b.HasIndex("CompetencyId"); + + b.HasIndex("PersonId"); + + b.ToTable("CompetencyAssessments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Faculty", b => + { + b.Property("FacultyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("FacultyId")); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("FacultyId"); + + b.ToTable("Faculties"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Feature", b => + { + b.Property("FeatureId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("FeatureId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("FeatureType") + .HasColumnType("integer"); + + b.Property("MustAlwaysBeEnabled") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("FeatureId"); + + b.ToTable("Features"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FinancialReference", b => + { + b.Property("FinancialReferenceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("FinancialReferenceId")); + + b.Property("FinancialYear") + .HasColumnType("integer"); + + b.Property("Grade41Costs") + .HasColumnType("real"); + + b.Property("Grade51Costs") + .HasColumnType("real"); + + b.Property("Grade55Costs") + .HasColumnType("real"); + + b.Property("Grade65Costs") + .HasColumnType("real"); + + b.Property("Grade71Costs") + .HasColumnType("real"); + + b.Property("Grade75Costs") + .HasColumnType("real"); + + b.Property("RecoveryTarget") + .HasColumnType("real"); + + b.HasKey("FinancialReferenceId"); + + b.ToTable("FinancialReferences"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FundingSource", b => + { + b.Property("FundingSourceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("FundingSourceId")); + + b.Property("AccountCode") + .HasColumnType("text"); + + b.Property("AmountAvailable") + .HasColumnType("double precision"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("FundingSourceType") + .HasColumnType("integer"); + + b.Property("HasAccountCode") + .HasColumnType("boolean"); + + b.Property("ProjectId") + .HasColumnType("integer"); + + b.HasKey("FundingSourceId"); + + b.HasIndex("ProjectId"); + + b.ToTable("FundingSources"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCode", b => + { + b.Property("InnateCodeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InnateCodeId")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsSensitive") + .HasColumnType("boolean"); + + b.HasKey("InnateCodeId"); + + b.ToTable("InnateCodes"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCodeTask", b => + { + b.Property("InnateCodeTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InnateCodeTaskId")); + + b.Property("Duty") + .HasColumnType("integer"); + + b.Property("InnateCodeId") + .HasColumnType("integer"); + + b.Property("TaskName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("InnateCodeTaskId"); + + b.HasIndex("InnateCodeId"); + + b.ToTable("InnateCodeTasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Invoice", b => + { + b.Property("InvoiceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InvoiceId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceReference") + .HasColumnType("text"); + + b.Property("InvoiceUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("KeyDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectId") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("double precision"); + + b.HasKey("InvoiceId"); + + b.HasIndex("ProjectId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Note", b => + { + b.Property("NoteId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("NoteId")); + + b.Property("AuthorUserId") + .HasColumnType("integer"); + + b.Property("CompletedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("EditedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("EditorUserId") + .HasColumnType("integer"); + + b.Property("HtmlContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsFinanceInfo") + .HasColumnType("boolean"); + + b.Property("ProjectId") + .HasColumnType("integer"); + + b.HasKey("NoteId"); + + b.HasIndex("AuthorUserId"); + + b.HasIndex("EditorUserId"); + + b.HasIndex("ProjectId"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.OwnedSkill", b => + { + b.Property("OwnedSkillId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("OwnedSkillId")); + + b.Property("FavouriteSkill") + .HasColumnType("boolean"); + + b.Property("LastUsed") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerPersonId") + .HasColumnType("integer"); + + b.Property("Proficiency") + .HasColumnType("integer"); + + b.Property("SkillTagId") + .HasColumnType("integer"); + + b.HasKey("OwnedSkillId"); + + b.HasIndex("OwnerPersonId"); + + b.HasIndex("SkillTagId"); + + b.ToTable("OwnedSkills"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Payment", b => + { + b.Property("PaymentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PaymentId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceId") + .HasColumnType("integer"); + + b.Property("KeyDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectId") + .HasColumnType("integer"); + + b.Property("SourceFundingSourceId") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("double precision"); + + b.HasKey("PaymentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SourceFundingSourceId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Person", b => + { + b.Property("PersonId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersonId")); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("FTE") + .HasColumnType("double precision"); + + b.Property("LineManagerPersonId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TimesheetTemplateData") + .HasColumnType("text"); + + b.HasKey("PersonId"); + + b.HasIndex("LineManagerPersonId"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Project", b => + { + b.Property("ProjectId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ProjectId")); + + b.Property("ActualCost") + .HasColumnType("double precision"); + + b.Property("ActualIndirectCost") + .HasColumnType("double precision"); + + b.Property("ActualLeadershipCosts") + .HasColumnType("double precision"); + + b.Property("ActualWorkHours") + .HasColumnType("double precision"); + + b.Property("ActualsLastUpdated") + .HasColumnType("text"); + + b.Property("Budget") + .HasColumnType("double precision"); + + b.Property("BudgetedIndirects") + .HasColumnType("double precision"); + + b.Property("CostModel") + .HasColumnType("integer"); + + b.Property("DayRate") + .HasColumnType("double precision"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("InnateActivityInnateCodeId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PI") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlannedCost") + .HasColumnType("double precision"); + + b.Property("PlannedIndirectCost") + .HasColumnType("double precision"); + + b.Property("PlannedLeadershipCosts") + .HasColumnType("double precision"); + + b.Property("PlannedWorkHours") + .HasColumnType("double precision"); + + b.Property("ProjectManagerPersonId") + .HasColumnType("integer"); + + b.Property("ProjectStatus") + .HasColumnType("integer"); + + b.Property("RTP") + .HasColumnType("integer"); + + b.Property("RequestDocLink") + .IsRequired() + .HasColumnType("text"); + + b.Property("SchoolId") + .HasColumnType("integer"); + + b.Property("ScrumProjectLink") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("ProjectId"); + + b.HasIndex("InnateActivityInnateCodeId"); + + b.HasIndex("ProjectManagerPersonId"); + + b.HasIndex("SchoolId"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Resource", b => + { + b.Property("ResourceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ResourceId")); + + b.Property("ActualCost") + .HasColumnType("double precision"); + + b.Property("ActualIndirectCost") + .HasColumnType("double precision"); + + b.Property("ActualWorkHours") + .HasColumnType("double precision"); + + b.Property("AssignmentFTE") + .HasColumnType("double precision"); + + b.Property("BilledFTE") + .HasColumnType("double precision"); + + b.Property("DayRate") + .HasColumnType("double precision"); + + b.Property("FundedFromFundingSourceId") + .HasColumnType("integer"); + + b.Property("IsProvisional") + .HasColumnType("boolean"); + + b.Property("PersonId") + .HasColumnType("integer"); + + b.Property("PlannedCost") + .HasColumnType("double precision"); + + b.Property("PlannedIndirectCost") + .HasColumnType("double precision"); + + b.Property("PlannedWorkHours") + .HasColumnType("double precision"); + + b.Property("SubTaskId") + .HasColumnType("integer"); + + b.Property("UseProjectDayRate") + .HasColumnType("boolean"); + + b.HasKey("ResourceId"); + + b.HasIndex("FundedFromFundingSourceId"); + + b.HasIndex("PersonId"); + + b.HasIndex("SubTaskId"); + + b.ToTable("Resources"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.School", b => + { + b.Property("SchoolId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SchoolId")); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("FacultyId") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("SchoolId"); + + b.HasIndex("FacultyId"); + + b.ToTable("Schools"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Setting", b => + { + b.Property("SettingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SettingId")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("SettingType") + .HasColumnType("integer"); + + b.Property("SettingValue") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("SettingId"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SkillTag", b => + { + b.Property("SkillTagId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SkillTagId")); + + b.Property("ControlledName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HasValidWikiLink") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rareness") + .HasColumnType("integer"); + + b.Property("RarenessCount") + .HasColumnType("integer"); + + b.HasKey("SkillTagId"); + + b.ToTable("SkillTags"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SubTask", b => + { + b.Property("SubTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SubTaskId")); + + b.Property("ActualCost") + .HasColumnType("double precision"); + + b.Property("ActualIndirectCost") + .HasColumnType("double precision"); + + b.Property("ActualWorkHours") + .HasColumnType("double precision"); + + b.Property("Demand") + .HasColumnType("double precision"); + + b.Property("DurationBillableDays") + .HasColumnType("integer"); + + b.Property("DurationDays") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("HasFixedEndDate") + .HasColumnType("boolean"); + + b.Property("HasFixedStart") + .HasColumnType("boolean"); + + b.Property("IsLeadershipTask") + .HasColumnType("boolean"); + + b.Property("Lag") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OriginalDemand") + .HasColumnType("double precision"); + + b.Property("OwningProjectProjectId") + .HasColumnType("integer"); + + b.Property("PlannedCost") + .HasColumnType("double precision"); + + b.Property("PlannedIndirectCost") + .HasColumnType("double precision"); + + b.Property("PlannedWorkHours") + .HasColumnType("double precision"); + + b.Property("PredecessorSubTaskId") + .HasColumnType("integer"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TaskType") + .HasColumnType("integer"); + + b.Property("UnmetDemand") + .HasColumnType("double precision"); + + b.HasKey("SubTaskId"); + + b.HasIndex("OwningProjectProjectId"); + + b.HasIndex("PredecessorSubTaskId"); + + b.ToTable("SubTasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Timesheet", b => + { + b.Property("TimesheetId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TimesheetId")); + + b.Property("CreatedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DateStatusChanged") + .HasColumnType("timestamp without time zone"); + + b.Property("Info") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("integer"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusChangedByPersonId") + .HasColumnType("integer"); + + b.HasKey("TimesheetId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("StatusChangedByPersonId"); + + b.ToTable("Timesheets"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.TimesheetEntry", b => + { + b.Property("TimesheetEntryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TimesheetEntryId")); + + b.Property("FridayHours") + .HasColumnType("double precision"); + + b.Property("InnateCodeTaskId") + .HasColumnType("integer"); + + b.Property("MondayHours") + .HasColumnType("double precision"); + + b.Property("SaturdayHours") + .HasColumnType("double precision"); + + b.Property("SundayHours") + .HasColumnType("double precision"); + + b.Property("ThursdayHours") + .HasColumnType("double precision"); + + b.Property("TimesheetId") + .HasColumnType("integer"); + + b.Property("TuesdayHours") + .HasColumnType("double precision"); + + b.Property("WednesdayHours") + .HasColumnType("double precision"); + + b.HasKey("TimesheetEntryId"); + + b.HasIndex("InnateCodeTaskId"); + + b.HasIndex("TimesheetId"); + + b.ToTable("TimesheetEntries"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.User", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("UserId")); + + b.Property("CASUserName") + .IsRequired() + .HasColumnType("text"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastLoggedIn") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PersonId") + .HasColumnType("integer"); + + b.Property("RoleType") + .HasColumnType("integer"); + + b.HasKey("UserId"); + + b.HasIndex("PersonId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.WorkloadModelChange", b => + { + b.Property("WorkloadModelChangeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WorkloadModelChangeId")); + + b.Property("ArchitectureFTE") + .HasColumnType("double precision"); + + b.Property("BusinessAsUsualFTE") + .HasColumnType("double precision"); + + b.Property("ChangeDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Grade") + .HasColumnType("integer"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PersonId") + .HasColumnType("integer"); + + b.Property("PersonalDevelopmentFTE") + .HasColumnType("double precision"); + + b.Property("ProjectAndServiceManagementFTE") + .HasColumnType("double precision"); + + b.Property("ProjectManagementFTE") + .HasColumnType("double precision"); + + b.Property("ProjectWorkFTE") + .HasColumnType("double precision"); + + b.Property("ServiceManagementFTE") + .HasColumnType("double precision"); + + b.Property("StaffManagementFTE") + .HasColumnType("double precision"); + + b.HasKey("WorkloadModelChangeId"); + + b.HasIndex("PersonId"); + + b.ToTable("WorkloadModelChanges"); + }); + + modelBuilder.Entity("PersonProject", b => + { + b.Property("FollowedProjectsProjectId") + .HasColumnType("integer"); + + b.Property("FollowersPersonId") + .HasColumnType("integer"); + + b.HasKey("FollowedProjectsProjectId", "FollowersPersonId"); + + b.HasIndex("FollowersPersonId"); + + b.ToTable("PersonProject"); + }); + + modelBuilder.Entity("SkillTagSubTask", b => + { + b.Property("SkillsRequiredSkillTagId") + .HasColumnType("integer"); + + b.Property("TasksNeedingThisSkillSubTaskId") + .HasColumnType("integer"); + + b.HasKey("SkillsRequiredSkillTagId", "TasksNeedingThisSkillSubTaskId"); + + b.HasIndex("TasksNeedingThisSkillSubTaskId"); + + b.ToTable("SkillTagSubTask"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Absence", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany("Absences") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.ApiKey", b => + { + b.HasOne("PPMTool.Data.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.CompetencyAssessment", b => + { + b.HasOne("PPMTool.Data.Entities.Competency", null) + .WithMany("Assessments") + .HasForeignKey("CompetencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Person", null) + .WithMany("Assessments") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FundingSource", b => + { + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany("FundingSources") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCodeTask", b => + { + b.HasOne("PPMTool.Data.Entities.InnateCode", "InnateCode") + .WithMany("Tasks") + .HasForeignKey("InnateCodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InnateCode"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Invoice", b => + { + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany("Invoices") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Note", b => + { + b.HasOne("PPMTool.Data.Entities.User", "Author") + .WithMany("AuthoredNotes") + .HasForeignKey("AuthorUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.User", "Editor") + .WithMany("EditedNotes") + .HasForeignKey("EditorUserId"); + + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Editor"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.OwnedSkill", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Owner") + .WithMany("OwnedSkills") + .HasForeignKey("OwnerPersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SkillTag", "SkillTag") + .WithMany("OwnedSkills") + .HasForeignKey("SkillTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("SkillTag"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Payment", b => + { + b.HasOne("PPMTool.Data.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId"); + + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany("Payments") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.FundingSource", "Source") + .WithMany("PaymentsFromSource") + .HasForeignKey("SourceFundingSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Invoice"); + + b.Navigation("Project"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Person", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "LineManager") + .WithMany("PeopleManaged") + .HasForeignKey("LineManagerPersonId"); + + b.Navigation("LineManager"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Project", b => + { + b.HasOne("PPMTool.Data.Entities.InnateCode", "InnateActivity") + .WithMany() + .HasForeignKey("InnateActivityInnateCodeId"); + + b.HasOne("PPMTool.Data.Entities.Person", "ProjectManager") + .WithMany("ManagedProjects") + .HasForeignKey("ProjectManagerPersonId"); + + b.HasOne("PPMTool.Data.Entities.School", "School") + .WithMany() + .HasForeignKey("SchoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InnateActivity"); + + b.Navigation("ProjectManager"); + + b.Navigation("School"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Resource", b => + { + b.HasOne("PPMTool.Data.Entities.FundingSource", "FundedFrom") + .WithMany("ResourcesFunded") + .HasForeignKey("FundedFromFundingSourceId"); + + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SubTask", "SubTask") + .WithMany("AssignedResources") + .HasForeignKey("SubTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FundedFrom"); + + b.Navigation("Person"); + + b.Navigation("SubTask"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.School", b => + { + b.HasOne("PPMTool.Data.Entities.Faculty", "Faculty") + .WithMany("Schools") + .HasForeignKey("FacultyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Faculty"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SubTask", b => + { + b.HasOne("PPMTool.Data.Entities.Project", "OwningProject") + .WithMany("SubTasks") + .HasForeignKey("OwningProjectProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SubTask", "Predecessor") + .WithMany("Successors") + .HasForeignKey("PredecessorSubTaskId"); + + b.Navigation("OwningProject"); + + b.Navigation("Predecessor"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Timesheet", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Owner") + .WithMany("Timesheets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Person", "StatusChangedBy") + .WithMany("TimesheetsChanged") + .HasForeignKey("StatusChangedByPersonId"); + + b.Navigation("Owner"); + + b.Navigation("StatusChangedBy"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.TimesheetEntry", b => + { + b.HasOne("PPMTool.Data.Entities.InnateCodeTask", "InnateCodeTask") + .WithMany() + .HasForeignKey("InnateCodeTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Timesheet", "Timesheet") + .WithMany("TimesheetEntries") + .HasForeignKey("TimesheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InnateCodeTask"); + + b.Navigation("Timesheet"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.User", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany() + .HasForeignKey("PersonId"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.WorkloadModelChange", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany("WorkloadModelChanges") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("PersonProject", b => + { + b.HasOne("PPMTool.Data.Entities.Project", null) + .WithMany() + .HasForeignKey("FollowedProjectsProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Person", null) + .WithMany() + .HasForeignKey("FollowersPersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SkillTagSubTask", b => + { + b.HasOne("PPMTool.Data.Entities.SkillTag", null) + .WithMany() + .HasForeignKey("SkillsRequiredSkillTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SubTask", null) + .WithMany() + .HasForeignKey("TasksNeedingThisSkillSubTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Competency", b => + { + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Faculty", b => + { + b.Navigation("Schools"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FundingSource", b => + { + b.Navigation("PaymentsFromSource"); + + b.Navigation("ResourcesFunded"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCode", b => + { + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Person", b => + { + b.Navigation("Absences"); + + b.Navigation("Assessments"); + + b.Navigation("ManagedProjects"); + + b.Navigation("OwnedSkills"); + + b.Navigation("PeopleManaged"); + + b.Navigation("Timesheets"); + + b.Navigation("TimesheetsChanged"); + + b.Navigation("WorkloadModelChanges"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Project", b => + { + b.Navigation("FundingSources"); + + b.Navigation("Invoices"); + + b.Navigation("Payments"); + + b.Navigation("SubTasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SkillTag", b => + { + b.Navigation("OwnedSkills"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SubTask", b => + { + b.Navigation("AssignedResources"); + + b.Navigation("Successors"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Timesheet", b => + { + b.Navigation("TimesheetEntries"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.User", b => + { + b.Navigation("AuthoredNotes"); + + b.Navigation("EditedNotes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PPMTool.Migrations.PostgreSql/Migrations/20260527120344_FixDateTimeTimezone.cs b/PPMTool.Migrations.PostgreSql/Migrations/20260527120344_FixDateTimeTimezone.cs new file mode 100644 index 000000000..6b89ae898 --- /dev/null +++ b/PPMTool.Migrations.PostgreSql/Migrations/20260527120344_FixDateTimeTimezone.cs @@ -0,0 +1,220 @@ +๏ปฟ// SPDX-FileCopyrightText: 2026 University of Manchester +// +// SPDX-License-Identifier: apache-2.0 + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PPMTool.Migrations.PostgreSql.Migrations +{ + /// + public partial class FixDateTimeTimezone : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Scaffolded AlterColumn omits the USING clause, which PostgreSQL requires when + // casting timestamptz -> timestamp. Use raw SQL with AT TIME ZONE 'UTC' to + // preserve the existing wall-clock dates exactly (timestamptz is stored as UTC). + var columns = new[] + { + ("WorkloadModelChanges", "ChangeDate"), + ("Timesheets", "StartDate"), + ("Timesheets", "DateStatusChanged"), + ("Timesheets", "CreatedDate"), + ("SubTasks", "StartDate"), + ("SubTasks", "EndDate"), + ("Projects", "StartDate"), + ("Projects", "EndDate"), + ("People", "StartDate"), + ("People", "EndDate"), + ("Payments", "KeyDate"), + ("OwnedSkills", "LastUsed"), + ("Notes", "EditedDate"), + ("Notes", "DueDate"), + ("Notes", "CreatedDate"), + ("Notes", "CompletedDate"), + ("Invoices", "KeyDate"), + ("ApiKeys", "ExpiresAt"), + ("Absence", "StartDate"), + ("Absence", "EndDate"), + }; + + foreach (var (table, column) in columns) + { + migrationBuilder.Sql( + $"ALTER TABLE \"{table}\" ALTER COLUMN \"{column}\" TYPE timestamp without time zone USING \"{column}\" AT TIME ZONE 'UTC';"); + } + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ChangeDate", + table: "WorkloadModelChanges", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "StartDate", + table: "Timesheets", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "DateStatusChanged", + table: "Timesheets", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "CreatedDate", + table: "Timesheets", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "StartDate", + table: "SubTasks", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "EndDate", + table: "SubTasks", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "StartDate", + table: "Projects", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "EndDate", + table: "Projects", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "StartDate", + table: "People", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "EndDate", + table: "People", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "KeyDate", + table: "Payments", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "LastUsed", + table: "OwnedSkills", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "EditedDate", + table: "Notes", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "DueDate", + table: "Notes", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedDate", + table: "Notes", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "CompletedDate", + table: "Notes", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "KeyDate", + table: "Invoices", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "ExpiresAt", + table: "ApiKeys", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "StartDate", + table: "Absence", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "EndDate", + table: "Absence", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone", + oldNullable: true); + } + } +} diff --git a/PPMTool.Migrations.PostgreSql/Migrations/20260615153709_AddActiveFlagToTimesheetTask.Designer.cs b/PPMTool.Migrations.PostgreSql/Migrations/20260615153709_AddActiveFlagToTimesheetTask.Designer.cs new file mode 100644 index 000000000..bdd2df5d0 --- /dev/null +++ b/PPMTool.Migrations.PostgreSql/Migrations/20260615153709_AddActiveFlagToTimesheetTask.Designer.cs @@ -0,0 +1,1465 @@ +๏ปฟ// SPDX-FileCopyrightText: 2026 University of Manchester +// +// SPDX-License-Identifier: apache-2.0 + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PPMTool.Data.Context; + +#nullable disable + +namespace PPMTool.Migrations.PostgreSql.Migrations +{ + [DbContext(typeof(PPMToolContext))] + [Migration("20260615153709_AddActiveFlagToTimesheetTask")] + partial class AddActiveFlagToTimesheetTask + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("PPMTool.Data.Entities.Absence", b => + { + b.Property("AbsenceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AbsenceId")); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("PersonId") + .HasColumnType("integer"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AbsenceId"); + + b.HasIndex("PersonId"); + + b.ToTable("Absence"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.ApiKey", b => + { + b.Property("ApiKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ApiKeyId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerUserId") + .HasColumnType("integer"); + + b.HasKey("ApiKeyId"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Competency", b => + { + b.Property("CompetencyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CompetencyId")); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedDate") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Grade") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LegacyId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("Objective") + .IsRequired() + .HasColumnType("text"); + + b.Property("Revision") + .HasColumnType("integer"); + + b.Property("RevisionDate") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("CompetencyId"); + + b.ToTable("Competencies"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.CompetencyAssessment", b => + { + b.Property("CompetencyAssessmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CompetencyAssessmentId")); + + b.Property("CompetencyDescription") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompetencyId") + .HasColumnType("integer"); + + b.Property("CompetencyObjective") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompetencyRevision") + .HasColumnType("integer"); + + b.Property("DateCreated") + .IsRequired() + .HasColumnType("text"); + + b.Property("Evidence") + .HasColumnType("text"); + + b.Property("PersonId") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("CompetencyAssessmentId"); + + b.HasIndex("CompetencyId"); + + b.HasIndex("PersonId"); + + b.ToTable("CompetencyAssessments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Faculty", b => + { + b.Property("FacultyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("FacultyId")); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("FacultyId"); + + b.ToTable("Faculties"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Feature", b => + { + b.Property("FeatureId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("FeatureId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("FeatureType") + .HasColumnType("integer"); + + b.Property("MustAlwaysBeEnabled") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("FeatureId"); + + b.ToTable("Features"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FinancialReference", b => + { + b.Property("FinancialReferenceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("FinancialReferenceId")); + + b.Property("FinancialYear") + .HasColumnType("integer"); + + b.Property("Grade41Costs") + .HasColumnType("real"); + + b.Property("Grade51Costs") + .HasColumnType("real"); + + b.Property("Grade55Costs") + .HasColumnType("real"); + + b.Property("Grade65Costs") + .HasColumnType("real"); + + b.Property("Grade71Costs") + .HasColumnType("real"); + + b.Property("Grade75Costs") + .HasColumnType("real"); + + b.Property("RecoveryTarget") + .HasColumnType("real"); + + b.HasKey("FinancialReferenceId"); + + b.ToTable("FinancialReferences"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FundingSource", b => + { + b.Property("FundingSourceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("FundingSourceId")); + + b.Property("AccountCode") + .HasColumnType("text"); + + b.Property("AmountAvailable") + .HasColumnType("double precision"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("FundingSourceType") + .HasColumnType("integer"); + + b.Property("HasAccountCode") + .HasColumnType("boolean"); + + b.Property("ProjectId") + .HasColumnType("integer"); + + b.HasKey("FundingSourceId"); + + b.HasIndex("ProjectId"); + + b.ToTable("FundingSources"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCode", b => + { + b.Property("InnateCodeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InnateCodeId")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsSensitive") + .HasColumnType("boolean"); + + b.HasKey("InnateCodeId"); + + b.ToTable("InnateCodes"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCodeTask", b => + { + b.Property("InnateCodeTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InnateCodeTaskId")); + + b.Property("Duty") + .HasColumnType("integer"); + + b.Property("InnateCodeId") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("TaskName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("InnateCodeTaskId"); + + b.HasIndex("InnateCodeId"); + + b.ToTable("InnateCodeTasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Invoice", b => + { + b.Property("InvoiceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InvoiceId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceReference") + .HasColumnType("text"); + + b.Property("InvoiceUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("KeyDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectId") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("double precision"); + + b.HasKey("InvoiceId"); + + b.HasIndex("ProjectId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Note", b => + { + b.Property("NoteId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("NoteId")); + + b.Property("AuthorUserId") + .HasColumnType("integer"); + + b.Property("CompletedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("EditedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("EditorUserId") + .HasColumnType("integer"); + + b.Property("HtmlContent") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsFinanceInfo") + .HasColumnType("boolean"); + + b.Property("ProjectId") + .HasColumnType("integer"); + + b.HasKey("NoteId"); + + b.HasIndex("AuthorUserId"); + + b.HasIndex("EditorUserId"); + + b.HasIndex("ProjectId"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.OwnedSkill", b => + { + b.Property("OwnedSkillId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("OwnedSkillId")); + + b.Property("FavouriteSkill") + .HasColumnType("boolean"); + + b.Property("LastUsed") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerPersonId") + .HasColumnType("integer"); + + b.Property("Proficiency") + .HasColumnType("integer"); + + b.Property("SkillTagId") + .HasColumnType("integer"); + + b.HasKey("OwnedSkillId"); + + b.HasIndex("OwnerPersonId"); + + b.HasIndex("SkillTagId"); + + b.ToTable("OwnedSkills"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Payment", b => + { + b.Property("PaymentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PaymentId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceId") + .HasColumnType("integer"); + + b.Property("KeyDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectId") + .HasColumnType("integer"); + + b.Property("SourceFundingSourceId") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("double precision"); + + b.HasKey("PaymentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SourceFundingSourceId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Person", b => + { + b.Property("PersonId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersonId")); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("FTE") + .HasColumnType("double precision"); + + b.Property("LineManagerPersonId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TimesheetTemplateData") + .HasColumnType("text"); + + b.HasKey("PersonId"); + + b.HasIndex("LineManagerPersonId"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Project", b => + { + b.Property("ProjectId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ProjectId")); + + b.Property("ActualCost") + .HasColumnType("double precision"); + + b.Property("ActualIndirectCost") + .HasColumnType("double precision"); + + b.Property("ActualLeadershipCosts") + .HasColumnType("double precision"); + + b.Property("ActualWorkHours") + .HasColumnType("double precision"); + + b.Property("ActualsLastUpdated") + .HasColumnType("text"); + + b.Property("Budget") + .HasColumnType("double precision"); + + b.Property("BudgetedIndirects") + .HasColumnType("double precision"); + + b.Property("CostModel") + .HasColumnType("integer"); + + b.Property("DayRate") + .HasColumnType("double precision"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("InnateActivityInnateCodeId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PI") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlannedCost") + .HasColumnType("double precision"); + + b.Property("PlannedIndirectCost") + .HasColumnType("double precision"); + + b.Property("PlannedLeadershipCosts") + .HasColumnType("double precision"); + + b.Property("PlannedWorkHours") + .HasColumnType("double precision"); + + b.Property("ProjectManagerPersonId") + .HasColumnType("integer"); + + b.Property("ProjectStatus") + .HasColumnType("integer"); + + b.Property("RTP") + .HasColumnType("integer"); + + b.Property("RequestDocLink") + .IsRequired() + .HasColumnType("text"); + + b.Property("SchoolId") + .HasColumnType("integer"); + + b.Property("ScrumProjectLink") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("ProjectId"); + + b.HasIndex("InnateActivityInnateCodeId"); + + b.HasIndex("ProjectManagerPersonId"); + + b.HasIndex("SchoolId"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Resource", b => + { + b.Property("ResourceId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ResourceId")); + + b.Property("ActualCost") + .HasColumnType("double precision"); + + b.Property("ActualIndirectCost") + .HasColumnType("double precision"); + + b.Property("ActualWorkHours") + .HasColumnType("double precision"); + + b.Property("AssignmentFTE") + .HasColumnType("double precision"); + + b.Property("BilledFTE") + .HasColumnType("double precision"); + + b.Property("DayRate") + .HasColumnType("double precision"); + + b.Property("FundedFromFundingSourceId") + .HasColumnType("integer"); + + b.Property("IsProvisional") + .HasColumnType("boolean"); + + b.Property("PersonId") + .HasColumnType("integer"); + + b.Property("PlannedCost") + .HasColumnType("double precision"); + + b.Property("PlannedIndirectCost") + .HasColumnType("double precision"); + + b.Property("PlannedWorkHours") + .HasColumnType("double precision"); + + b.Property("SubTaskId") + .HasColumnType("integer"); + + b.Property("UseProjectDayRate") + .HasColumnType("boolean"); + + b.HasKey("ResourceId"); + + b.HasIndex("FundedFromFundingSourceId"); + + b.HasIndex("PersonId"); + + b.HasIndex("SubTaskId"); + + b.ToTable("Resources"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.School", b => + { + b.Property("SchoolId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SchoolId")); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("FacultyId") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("SchoolId"); + + b.HasIndex("FacultyId"); + + b.ToTable("Schools"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Setting", b => + { + b.Property("SettingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SettingId")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("SettingType") + .HasColumnType("integer"); + + b.Property("SettingValue") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("SettingId"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SkillTag", b => + { + b.Property("SkillTagId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SkillTagId")); + + b.Property("ControlledName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HasValidWikiLink") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rareness") + .HasColumnType("integer"); + + b.Property("RarenessCount") + .HasColumnType("integer"); + + b.HasKey("SkillTagId"); + + b.ToTable("SkillTags"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SubTask", b => + { + b.Property("SubTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SubTaskId")); + + b.Property("ActualCost") + .HasColumnType("double precision"); + + b.Property("ActualIndirectCost") + .HasColumnType("double precision"); + + b.Property("ActualWorkHours") + .HasColumnType("double precision"); + + b.Property("Demand") + .HasColumnType("double precision"); + + b.Property("DurationBillableDays") + .HasColumnType("integer"); + + b.Property("DurationDays") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("HasFixedEndDate") + .HasColumnType("boolean"); + + b.Property("HasFixedStart") + .HasColumnType("boolean"); + + b.Property("IsLeadershipTask") + .HasColumnType("boolean"); + + b.Property("Lag") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OriginalDemand") + .HasColumnType("double precision"); + + b.Property("OwningProjectProjectId") + .HasColumnType("integer"); + + b.Property("PlannedCost") + .HasColumnType("double precision"); + + b.Property("PlannedIndirectCost") + .HasColumnType("double precision"); + + b.Property("PlannedWorkHours") + .HasColumnType("double precision"); + + b.Property("PredecessorSubTaskId") + .HasColumnType("integer"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TaskType") + .HasColumnType("integer"); + + b.Property("UnmetDemand") + .HasColumnType("double precision"); + + b.HasKey("SubTaskId"); + + b.HasIndex("OwningProjectProjectId"); + + b.HasIndex("PredecessorSubTaskId"); + + b.ToTable("SubTasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Timesheet", b => + { + b.Property("TimesheetId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TimesheetId")); + + b.Property("CreatedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DateStatusChanged") + .HasColumnType("timestamp without time zone"); + + b.Property("Info") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("integer"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusChangedByPersonId") + .HasColumnType("integer"); + + b.HasKey("TimesheetId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("StatusChangedByPersonId"); + + b.ToTable("Timesheets"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.TimesheetEntry", b => + { + b.Property("TimesheetEntryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TimesheetEntryId")); + + b.Property("FridayHours") + .HasColumnType("double precision"); + + b.Property("InnateCodeTaskId") + .HasColumnType("integer"); + + b.Property("MondayHours") + .HasColumnType("double precision"); + + b.Property("SaturdayHours") + .HasColumnType("double precision"); + + b.Property("SundayHours") + .HasColumnType("double precision"); + + b.Property("ThursdayHours") + .HasColumnType("double precision"); + + b.Property("TimesheetId") + .HasColumnType("integer"); + + b.Property("TuesdayHours") + .HasColumnType("double precision"); + + b.Property("WednesdayHours") + .HasColumnType("double precision"); + + b.HasKey("TimesheetEntryId"); + + b.HasIndex("InnateCodeTaskId"); + + b.HasIndex("TimesheetId"); + + b.ToTable("TimesheetEntries"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.User", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("UserId")); + + b.Property("CASUserName") + .IsRequired() + .HasColumnType("text"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastLoggedIn") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PersonId") + .HasColumnType("integer"); + + b.Property("RoleType") + .HasColumnType("integer"); + + b.HasKey("UserId"); + + b.HasIndex("PersonId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.WorkloadModelChange", b => + { + b.Property("WorkloadModelChangeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WorkloadModelChangeId")); + + b.Property("ArchitectureFTE") + .HasColumnType("double precision"); + + b.Property("BusinessAsUsualFTE") + .HasColumnType("double precision"); + + b.Property("ChangeDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Grade") + .HasColumnType("integer"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PersonId") + .HasColumnType("integer"); + + b.Property("PersonalDevelopmentFTE") + .HasColumnType("double precision"); + + b.Property("ProjectAndServiceManagementFTE") + .HasColumnType("double precision"); + + b.Property("ProjectManagementFTE") + .HasColumnType("double precision"); + + b.Property("ProjectWorkFTE") + .HasColumnType("double precision"); + + b.Property("ServiceManagementFTE") + .HasColumnType("double precision"); + + b.Property("StaffManagementFTE") + .HasColumnType("double precision"); + + b.HasKey("WorkloadModelChangeId"); + + b.HasIndex("PersonId"); + + b.ToTable("WorkloadModelChanges"); + }); + + modelBuilder.Entity("PersonProject", b => + { + b.Property("FollowedProjectsProjectId") + .HasColumnType("integer"); + + b.Property("FollowersPersonId") + .HasColumnType("integer"); + + b.HasKey("FollowedProjectsProjectId", "FollowersPersonId"); + + b.HasIndex("FollowersPersonId"); + + b.ToTable("PersonProject"); + }); + + modelBuilder.Entity("SkillTagSubTask", b => + { + b.Property("SkillsRequiredSkillTagId") + .HasColumnType("integer"); + + b.Property("TasksNeedingThisSkillSubTaskId") + .HasColumnType("integer"); + + b.HasKey("SkillsRequiredSkillTagId", "TasksNeedingThisSkillSubTaskId"); + + b.HasIndex("TasksNeedingThisSkillSubTaskId"); + + b.ToTable("SkillTagSubTask"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Absence", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany("Absences") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.ApiKey", b => + { + b.HasOne("PPMTool.Data.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.CompetencyAssessment", b => + { + b.HasOne("PPMTool.Data.Entities.Competency", null) + .WithMany("Assessments") + .HasForeignKey("CompetencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Person", null) + .WithMany("Assessments") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FundingSource", b => + { + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany("FundingSources") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCodeTask", b => + { + b.HasOne("PPMTool.Data.Entities.InnateCode", "InnateCode") + .WithMany("Tasks") + .HasForeignKey("InnateCodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InnateCode"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Invoice", b => + { + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany("Invoices") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Note", b => + { + b.HasOne("PPMTool.Data.Entities.User", "Author") + .WithMany("AuthoredNotes") + .HasForeignKey("AuthorUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.User", "Editor") + .WithMany("EditedNotes") + .HasForeignKey("EditorUserId"); + + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Editor"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.OwnedSkill", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Owner") + .WithMany("OwnedSkills") + .HasForeignKey("OwnerPersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SkillTag", "SkillTag") + .WithMany("OwnedSkills") + .HasForeignKey("SkillTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("SkillTag"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Payment", b => + { + b.HasOne("PPMTool.Data.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId"); + + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany("Payments") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.FundingSource", "Source") + .WithMany("PaymentsFromSource") + .HasForeignKey("SourceFundingSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Invoice"); + + b.Navigation("Project"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Person", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "LineManager") + .WithMany("PeopleManaged") + .HasForeignKey("LineManagerPersonId"); + + b.Navigation("LineManager"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Project", b => + { + b.HasOne("PPMTool.Data.Entities.InnateCode", "InnateActivity") + .WithMany() + .HasForeignKey("InnateActivityInnateCodeId"); + + b.HasOne("PPMTool.Data.Entities.Person", "ProjectManager") + .WithMany("ManagedProjects") + .HasForeignKey("ProjectManagerPersonId"); + + b.HasOne("PPMTool.Data.Entities.School", "School") + .WithMany() + .HasForeignKey("SchoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InnateActivity"); + + b.Navigation("ProjectManager"); + + b.Navigation("School"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Resource", b => + { + b.HasOne("PPMTool.Data.Entities.FundingSource", "FundedFrom") + .WithMany("ResourcesFunded") + .HasForeignKey("FundedFromFundingSourceId"); + + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SubTask", "SubTask") + .WithMany("AssignedResources") + .HasForeignKey("SubTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FundedFrom"); + + b.Navigation("Person"); + + b.Navigation("SubTask"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.School", b => + { + b.HasOne("PPMTool.Data.Entities.Faculty", "Faculty") + .WithMany("Schools") + .HasForeignKey("FacultyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Faculty"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SubTask", b => + { + b.HasOne("PPMTool.Data.Entities.Project", "OwningProject") + .WithMany("SubTasks") + .HasForeignKey("OwningProjectProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SubTask", "Predecessor") + .WithMany("Successors") + .HasForeignKey("PredecessorSubTaskId"); + + b.Navigation("OwningProject"); + + b.Navigation("Predecessor"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Timesheet", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Owner") + .WithMany("Timesheets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Person", "StatusChangedBy") + .WithMany("TimesheetsChanged") + .HasForeignKey("StatusChangedByPersonId"); + + b.Navigation("Owner"); + + b.Navigation("StatusChangedBy"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.TimesheetEntry", b => + { + b.HasOne("PPMTool.Data.Entities.InnateCodeTask", "InnateCodeTask") + .WithMany() + .HasForeignKey("InnateCodeTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Timesheet", "Timesheet") + .WithMany("TimesheetEntries") + .HasForeignKey("TimesheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InnateCodeTask"); + + b.Navigation("Timesheet"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.User", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany() + .HasForeignKey("PersonId"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.WorkloadModelChange", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany("WorkloadModelChanges") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("PersonProject", b => + { + b.HasOne("PPMTool.Data.Entities.Project", null) + .WithMany() + .HasForeignKey("FollowedProjectsProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Person", null) + .WithMany() + .HasForeignKey("FollowersPersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SkillTagSubTask", b => + { + b.HasOne("PPMTool.Data.Entities.SkillTag", null) + .WithMany() + .HasForeignKey("SkillsRequiredSkillTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SubTask", null) + .WithMany() + .HasForeignKey("TasksNeedingThisSkillSubTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Competency", b => + { + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Faculty", b => + { + b.Navigation("Schools"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FundingSource", b => + { + b.Navigation("PaymentsFromSource"); + + b.Navigation("ResourcesFunded"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCode", b => + { + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Person", b => + { + b.Navigation("Absences"); + + b.Navigation("Assessments"); + + b.Navigation("ManagedProjects"); + + b.Navigation("OwnedSkills"); + + b.Navigation("PeopleManaged"); + + b.Navigation("Timesheets"); + + b.Navigation("TimesheetsChanged"); + + b.Navigation("WorkloadModelChanges"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Project", b => + { + b.Navigation("FundingSources"); + + b.Navigation("Invoices"); + + b.Navigation("Payments"); + + b.Navigation("SubTasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SkillTag", b => + { + b.Navigation("OwnedSkills"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SubTask", b => + { + b.Navigation("AssignedResources"); + + b.Navigation("Successors"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Timesheet", b => + { + b.Navigation("TimesheetEntries"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.User", b => + { + b.Navigation("AuthoredNotes"); + + b.Navigation("EditedNotes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PPMTool.Migrations.PostgreSql/Migrations/20260615153709_AddActiveFlagToTimesheetTask.cs b/PPMTool.Migrations.PostgreSql/Migrations/20260615153709_AddActiveFlagToTimesheetTask.cs new file mode 100644 index 000000000..4791b7fc8 --- /dev/null +++ b/PPMTool.Migrations.PostgreSql/Migrations/20260615153709_AddActiveFlagToTimesheetTask.cs @@ -0,0 +1,33 @@ +๏ปฟ// SPDX-FileCopyrightText: 2026 University of Manchester +// +// SPDX-License-Identifier: apache-2.0 + +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PPMTool.Migrations.PostgreSql.Migrations +{ + /// + public partial class AddActiveFlagToTimesheetTask : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsActive", + table: "InnateCodeTasks", + type: "boolean", + nullable: false, + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsActive", + table: "InnateCodeTasks"); + } + } +} diff --git a/PPMTool.Migrations.PostgreSql/Migrations/PPMToolContextModelSnapshot.cs b/PPMTool.Migrations.PostgreSql/Migrations/PPMToolContextModelSnapshot.cs index dd5b58a31..7af4c5540 100644 --- a/PPMTool.Migrations.PostgreSql/Migrations/PPMToolContextModelSnapshot.cs +++ b/PPMTool.Migrations.PostgreSql/Migrations/PPMToolContextModelSnapshot.cs @@ -21,7 +21,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("ProductVersion", "10.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -35,13 +35,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AbsenceId")); b.Property("EndDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("PersonId") .HasColumnType("integer"); b.Property("StartDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.HasKey("AbsenceId"); @@ -66,7 +66,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("Key") .IsRequired() @@ -338,6 +338,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("InnateCodeId") .HasColumnType("integer"); + b.Property("IsActive") + .HasColumnType("boolean"); + b.Property("TaskName") .IsRequired() .HasColumnType("text"); @@ -369,7 +372,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("KeyDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("ProjectId") .HasColumnType("integer"); @@ -399,16 +402,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer"); b.Property("CompletedDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("CreatedDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("DueDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("EditedDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("EditorUserId") .HasColumnType("integer"); @@ -446,7 +449,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("boolean"); b.Property("LastUsed") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("OwnerPersonId") .HasColumnType("integer"); @@ -482,7 +485,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer"); b.Property("KeyDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("ProjectId") .HasColumnType("integer"); @@ -513,7 +516,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersonId")); b.Property("EndDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("FTE") .HasColumnType("double precision"); @@ -530,7 +533,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("StartDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("TimesheetTemplateData") .HasColumnType("text"); @@ -582,7 +585,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("EndDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("InnateActivityInnateCodeId") .HasColumnType("integer"); @@ -627,7 +630,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("StartDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.HasKey("ProjectId"); @@ -810,7 +813,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer"); b.Property("EndDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("HasFixedEndDate") .HasColumnType("boolean"); @@ -847,7 +850,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer"); b.Property("StartDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("TaskType") .HasColumnType("integer"); @@ -873,10 +876,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TimesheetId")); b.Property("CreatedDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("DateStatusChanged") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("Info") .HasColumnType("text"); @@ -885,7 +888,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer"); b.Property("StartDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("Status") .HasColumnType("integer"); @@ -997,7 +1000,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("double precision"); b.Property("ChangeDate") - .HasColumnType("timestamp with time zone"); + .HasColumnType("timestamp without time zone"); b.Property("Grade") .HasColumnType("integer"); diff --git a/PPMTool.Migrations.SqlServer/Migrations/20260615153605_AddActiveFlagToTimesheetTask.Designer.cs b/PPMTool.Migrations.SqlServer/Migrations/20260615153605_AddActiveFlagToTimesheetTask.Designer.cs new file mode 100644 index 000000000..f55e48877 --- /dev/null +++ b/PPMTool.Migrations.SqlServer/Migrations/20260615153605_AddActiveFlagToTimesheetTask.Designer.cs @@ -0,0 +1,1465 @@ +๏ปฟ// SPDX-FileCopyrightText: 2026 University of Manchester +// +// SPDX-License-Identifier: apache-2.0 + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PPMTool.Data.Context; + +#nullable disable + +namespace PPMTool.Migrations.SqlServer.Migrations +{ + [DbContext(typeof(PPMToolContext))] + [Migration("20260615153605_AddActiveFlagToTimesheetTask")] + partial class AddActiveFlagToTimesheetTask + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("PPMTool.Data.Entities.Absence", b => + { + b.Property("AbsenceId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("AbsenceId")); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("PersonId") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("AbsenceId"); + + b.HasIndex("PersonId"); + + b.ToTable("Absence"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.ApiKey", b => + { + b.Property("ApiKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ApiKeyId")); + + b.Property("Active") + .HasColumnType("bit"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Key") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerUserId") + .HasColumnType("int"); + + b.HasKey("ApiKeyId"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Competency", b => + { + b.Property("CompetencyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("CompetencyId")); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedDate") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Grade") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LegacyId") + .HasColumnType("nvarchar(max)"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("Objective") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Revision") + .HasColumnType("int"); + + b.Property("RevisionDate") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("CompetencyId"); + + b.ToTable("Competencies"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.CompetencyAssessment", b => + { + b.Property("CompetencyAssessmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("CompetencyAssessmentId")); + + b.Property("CompetencyDescription") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompetencyId") + .HasColumnType("int"); + + b.Property("CompetencyObjective") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompetencyRevision") + .HasColumnType("int"); + + b.Property("DateCreated") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Evidence") + .HasColumnType("nvarchar(max)"); + + b.Property("PersonId") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("CompetencyAssessmentId"); + + b.HasIndex("CompetencyId"); + + b.HasIndex("PersonId"); + + b.ToTable("CompetencyAssessments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Faculty", b => + { + b.Property("FacultyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("FacultyId")); + + b.Property("Code") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FacultyId"); + + b.ToTable("Faculties"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Feature", b => + { + b.Property("FeatureId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("FeatureId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Enabled") + .HasColumnType("bit"); + + b.Property("FeatureType") + .HasColumnType("int"); + + b.Property("MustAlwaysBeEnabled") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("FeatureId"); + + b.ToTable("Features"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FinancialReference", b => + { + b.Property("FinancialReferenceId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("FinancialReferenceId")); + + b.Property("FinancialYear") + .HasColumnType("int"); + + b.Property("Grade41Costs") + .HasColumnType("real"); + + b.Property("Grade51Costs") + .HasColumnType("real"); + + b.Property("Grade55Costs") + .HasColumnType("real"); + + b.Property("Grade65Costs") + .HasColumnType("real"); + + b.Property("Grade71Costs") + .HasColumnType("real"); + + b.Property("Grade75Costs") + .HasColumnType("real"); + + b.Property("RecoveryTarget") + .HasColumnType("real"); + + b.HasKey("FinancialReferenceId"); + + b.ToTable("FinancialReferences"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FundingSource", b => + { + b.Property("FundingSourceId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("FundingSourceId")); + + b.Property("AccountCode") + .HasColumnType("nvarchar(max)"); + + b.Property("AmountAvailable") + .HasColumnType("float"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("FundingSourceType") + .HasColumnType("int"); + + b.Property("HasAccountCode") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("int"); + + b.HasKey("FundingSourceId"); + + b.HasIndex("ProjectId"); + + b.ToTable("FundingSources"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCode", b => + { + b.Property("InnateCodeId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InnateCodeId")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ActivityName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsSensitive") + .HasColumnType("bit"); + + b.HasKey("InnateCodeId"); + + b.ToTable("InnateCodes"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCodeTask", b => + { + b.Property("InnateCodeTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InnateCodeTaskId")); + + b.Property("Duty") + .HasColumnType("int"); + + b.Property("InnateCodeId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("TaskName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("InnateCodeTaskId"); + + b.HasIndex("InnateCodeId"); + + b.ToTable("InnateCodeTasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Invoice", b => + { + b.Property("InvoiceId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InvoiceId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceReference") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("KeyDate") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Value") + .HasColumnType("float"); + + b.HasKey("InvoiceId"); + + b.HasIndex("ProjectId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Note", b => + { + b.Property("NoteId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("NoteId")); + + b.Property("AuthorUserId") + .HasColumnType("int"); + + b.Property("CompletedDate") + .HasColumnType("datetime2"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + b.Property("EditedDate") + .HasColumnType("datetime2"); + + b.Property("EditorUserId") + .HasColumnType("int"); + + b.Property("HtmlContent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsFinanceInfo") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("int"); + + b.HasKey("NoteId"); + + b.HasIndex("AuthorUserId"); + + b.HasIndex("EditorUserId"); + + b.HasIndex("ProjectId"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.OwnedSkill", b => + { + b.Property("OwnedSkillId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("OwnedSkillId")); + + b.Property("FavouriteSkill") + .HasColumnType("bit"); + + b.Property("LastUsed") + .HasColumnType("datetime2"); + + b.Property("OwnerPersonId") + .HasColumnType("int"); + + b.Property("Proficiency") + .HasColumnType("int"); + + b.Property("SkillTagId") + .HasColumnType("int"); + + b.HasKey("OwnedSkillId"); + + b.HasIndex("OwnerPersonId"); + + b.HasIndex("SkillTagId"); + + b.ToTable("OwnedSkills"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Payment", b => + { + b.Property("PaymentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PaymentId")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceId") + .HasColumnType("int"); + + b.Property("KeyDate") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("int"); + + b.Property("SourceFundingSourceId") + .HasColumnType("int"); + + b.Property("Value") + .HasColumnType("float"); + + b.HasKey("PaymentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SourceFundingSourceId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Person", b => + { + b.Property("PersonId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("PersonId")); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("FTE") + .HasColumnType("float"); + + b.Property("LineManagerPersonId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("TimesheetTemplateData") + .HasColumnType("nvarchar(max)"); + + b.HasKey("PersonId"); + + b.HasIndex("LineManagerPersonId"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Project", b => + { + b.Property("ProjectId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ProjectId")); + + b.Property("ActualCost") + .HasColumnType("float"); + + b.Property("ActualIndirectCost") + .HasColumnType("float"); + + b.Property("ActualLeadershipCosts") + .HasColumnType("float"); + + b.Property("ActualWorkHours") + .HasColumnType("float"); + + b.Property("ActualsLastUpdated") + .HasColumnType("nvarchar(max)"); + + b.Property("Budget") + .HasColumnType("float"); + + b.Property("BudgetedIndirects") + .HasColumnType("float"); + + b.Property("CostModel") + .HasColumnType("int"); + + b.Property("DayRate") + .HasColumnType("float"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("InnateActivityInnateCodeId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PI") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlannedCost") + .HasColumnType("float"); + + b.Property("PlannedIndirectCost") + .HasColumnType("float"); + + b.Property("PlannedLeadershipCosts") + .HasColumnType("float"); + + b.Property("PlannedWorkHours") + .HasColumnType("float"); + + b.Property("ProjectManagerPersonId") + .HasColumnType("int"); + + b.Property("ProjectStatus") + .HasColumnType("int"); + + b.Property("RTP") + .HasColumnType("int"); + + b.Property("RequestDocLink") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SchoolId") + .HasColumnType("int"); + + b.Property("ScrumProjectLink") + .HasColumnType("nvarchar(max)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("ProjectId"); + + b.HasIndex("InnateActivityInnateCodeId"); + + b.HasIndex("ProjectManagerPersonId"); + + b.HasIndex("SchoolId"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Resource", b => + { + b.Property("ResourceId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ResourceId")); + + b.Property("ActualCost") + .HasColumnType("float"); + + b.Property("ActualIndirectCost") + .HasColumnType("float"); + + b.Property("ActualWorkHours") + .HasColumnType("float"); + + b.Property("AssignmentFTE") + .HasColumnType("float"); + + b.Property("BilledFTE") + .HasColumnType("float"); + + b.Property("DayRate") + .HasColumnType("float"); + + b.Property("FundedFromFundingSourceId") + .HasColumnType("int"); + + b.Property("IsProvisional") + .HasColumnType("bit"); + + b.Property("PersonId") + .HasColumnType("int"); + + b.Property("PlannedCost") + .HasColumnType("float"); + + b.Property("PlannedIndirectCost") + .HasColumnType("float"); + + b.Property("PlannedWorkHours") + .HasColumnType("float"); + + b.Property("SubTaskId") + .HasColumnType("int"); + + b.Property("UseProjectDayRate") + .HasColumnType("bit"); + + b.HasKey("ResourceId"); + + b.HasIndex("FundedFromFundingSourceId"); + + b.HasIndex("PersonId"); + + b.HasIndex("SubTaskId"); + + b.ToTable("Resources"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.School", b => + { + b.Property("SchoolId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SchoolId")); + + b.Property("Code") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FacultyId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SchoolId"); + + b.HasIndex("FacultyId"); + + b.ToTable("Schools"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Setting", b => + { + b.Property("SettingId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SettingId")); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("SettingType") + .HasColumnType("int"); + + b.Property("SettingValue") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("SettingId"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SkillTag", b => + { + b.Property("SkillTagId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SkillTagId")); + + b.Property("ControlledName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("HasValidWikiLink") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Rareness") + .HasColumnType("int"); + + b.Property("RarenessCount") + .HasColumnType("int"); + + b.HasKey("SkillTagId"); + + b.ToTable("SkillTags"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SubTask", b => + { + b.Property("SubTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SubTaskId")); + + b.Property("ActualCost") + .HasColumnType("float"); + + b.Property("ActualIndirectCost") + .HasColumnType("float"); + + b.Property("ActualWorkHours") + .HasColumnType("float"); + + b.Property("Demand") + .HasColumnType("float"); + + b.Property("DurationBillableDays") + .HasColumnType("int"); + + b.Property("DurationDays") + .HasColumnType("int"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("HasFixedEndDate") + .HasColumnType("bit"); + + b.Property("HasFixedStart") + .HasColumnType("bit"); + + b.Property("IsLeadershipTask") + .HasColumnType("bit"); + + b.Property("Lag") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OriginalDemand") + .HasColumnType("float"); + + b.Property("OwningProjectProjectId") + .HasColumnType("int"); + + b.Property("PlannedCost") + .HasColumnType("float"); + + b.Property("PlannedIndirectCost") + .HasColumnType("float"); + + b.Property("PlannedWorkHours") + .HasColumnType("float"); + + b.Property("PredecessorSubTaskId") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("TaskType") + .HasColumnType("int"); + + b.Property("UnmetDemand") + .HasColumnType("float"); + + b.HasKey("SubTaskId"); + + b.HasIndex("OwningProjectProjectId"); + + b.HasIndex("PredecessorSubTaskId"); + + b.ToTable("SubTasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Timesheet", b => + { + b.Property("TimesheetId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("TimesheetId")); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DateStatusChanged") + .HasColumnType("datetime2"); + + b.Property("Info") + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StatusChangedByPersonId") + .HasColumnType("int"); + + b.HasKey("TimesheetId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("StatusChangedByPersonId"); + + b.ToTable("Timesheets"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.TimesheetEntry", b => + { + b.Property("TimesheetEntryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("TimesheetEntryId")); + + b.Property("FridayHours") + .HasColumnType("float"); + + b.Property("InnateCodeTaskId") + .HasColumnType("int"); + + b.Property("MondayHours") + .HasColumnType("float"); + + b.Property("SaturdayHours") + .HasColumnType("float"); + + b.Property("SundayHours") + .HasColumnType("float"); + + b.Property("ThursdayHours") + .HasColumnType("float"); + + b.Property("TimesheetId") + .HasColumnType("int"); + + b.Property("TuesdayHours") + .HasColumnType("float"); + + b.Property("WednesdayHours") + .HasColumnType("float"); + + b.HasKey("TimesheetEntryId"); + + b.HasIndex("InnateCodeTaskId"); + + b.HasIndex("TimesheetId"); + + b.ToTable("TimesheetEntries"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.User", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("UserId")); + + b.Property("CASUserName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastLoggedIn") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PersonId") + .HasColumnType("int"); + + b.Property("RoleType") + .HasColumnType("int"); + + b.HasKey("UserId"); + + b.HasIndex("PersonId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.WorkloadModelChange", b => + { + b.Property("WorkloadModelChangeId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("WorkloadModelChangeId")); + + b.Property("ArchitectureFTE") + .HasColumnType("float"); + + b.Property("BusinessAsUsualFTE") + .HasColumnType("float"); + + b.Property("ChangeDate") + .HasColumnType("datetime2"); + + b.Property("Grade") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("PersonId") + .HasColumnType("int"); + + b.Property("PersonalDevelopmentFTE") + .HasColumnType("float"); + + b.Property("ProjectAndServiceManagementFTE") + .HasColumnType("float"); + + b.Property("ProjectManagementFTE") + .HasColumnType("float"); + + b.Property("ProjectWorkFTE") + .HasColumnType("float"); + + b.Property("ServiceManagementFTE") + .HasColumnType("float"); + + b.Property("StaffManagementFTE") + .HasColumnType("float"); + + b.HasKey("WorkloadModelChangeId"); + + b.HasIndex("PersonId"); + + b.ToTable("WorkloadModelChanges"); + }); + + modelBuilder.Entity("PersonProject", b => + { + b.Property("FollowedProjectsProjectId") + .HasColumnType("int"); + + b.Property("FollowersPersonId") + .HasColumnType("int"); + + b.HasKey("FollowedProjectsProjectId", "FollowersPersonId"); + + b.HasIndex("FollowersPersonId"); + + b.ToTable("PersonProject"); + }); + + modelBuilder.Entity("SkillTagSubTask", b => + { + b.Property("SkillsRequiredSkillTagId") + .HasColumnType("int"); + + b.Property("TasksNeedingThisSkillSubTaskId") + .HasColumnType("int"); + + b.HasKey("SkillsRequiredSkillTagId", "TasksNeedingThisSkillSubTaskId"); + + b.HasIndex("TasksNeedingThisSkillSubTaskId"); + + b.ToTable("SkillTagSubTask"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Absence", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany("Absences") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.ApiKey", b => + { + b.HasOne("PPMTool.Data.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.CompetencyAssessment", b => + { + b.HasOne("PPMTool.Data.Entities.Competency", null) + .WithMany("Assessments") + .HasForeignKey("CompetencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Person", null) + .WithMany("Assessments") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FundingSource", b => + { + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany("FundingSources") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCodeTask", b => + { + b.HasOne("PPMTool.Data.Entities.InnateCode", "InnateCode") + .WithMany("Tasks") + .HasForeignKey("InnateCodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InnateCode"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Invoice", b => + { + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany("Invoices") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Note", b => + { + b.HasOne("PPMTool.Data.Entities.User", "Author") + .WithMany("AuthoredNotes") + .HasForeignKey("AuthorUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.User", "Editor") + .WithMany("EditedNotes") + .HasForeignKey("EditorUserId"); + + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Editor"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.OwnedSkill", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Owner") + .WithMany("OwnedSkills") + .HasForeignKey("OwnerPersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SkillTag", "SkillTag") + .WithMany("OwnedSkills") + .HasForeignKey("SkillTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("SkillTag"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Payment", b => + { + b.HasOne("PPMTool.Data.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId"); + + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany("Payments") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.FundingSource", "Source") + .WithMany("PaymentsFromSource") + .HasForeignKey("SourceFundingSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Invoice"); + + b.Navigation("Project"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Person", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "LineManager") + .WithMany("PeopleManaged") + .HasForeignKey("LineManagerPersonId"); + + b.Navigation("LineManager"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Project", b => + { + b.HasOne("PPMTool.Data.Entities.InnateCode", "InnateActivity") + .WithMany() + .HasForeignKey("InnateActivityInnateCodeId"); + + b.HasOne("PPMTool.Data.Entities.Person", "ProjectManager") + .WithMany("ManagedProjects") + .HasForeignKey("ProjectManagerPersonId"); + + b.HasOne("PPMTool.Data.Entities.School", "School") + .WithMany() + .HasForeignKey("SchoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InnateActivity"); + + b.Navigation("ProjectManager"); + + b.Navigation("School"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Resource", b => + { + b.HasOne("PPMTool.Data.Entities.FundingSource", "FundedFrom") + .WithMany("ResourcesFunded") + .HasForeignKey("FundedFromFundingSourceId"); + + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SubTask", "SubTask") + .WithMany("AssignedResources") + .HasForeignKey("SubTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FundedFrom"); + + b.Navigation("Person"); + + b.Navigation("SubTask"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.School", b => + { + b.HasOne("PPMTool.Data.Entities.Faculty", "Faculty") + .WithMany("Schools") + .HasForeignKey("FacultyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Faculty"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SubTask", b => + { + b.HasOne("PPMTool.Data.Entities.Project", "OwningProject") + .WithMany("SubTasks") + .HasForeignKey("OwningProjectProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SubTask", "Predecessor") + .WithMany("Successors") + .HasForeignKey("PredecessorSubTaskId"); + + b.Navigation("OwningProject"); + + b.Navigation("Predecessor"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Timesheet", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Owner") + .WithMany("Timesheets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Person", "StatusChangedBy") + .WithMany("TimesheetsChanged") + .HasForeignKey("StatusChangedByPersonId"); + + b.Navigation("Owner"); + + b.Navigation("StatusChangedBy"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.TimesheetEntry", b => + { + b.HasOne("PPMTool.Data.Entities.InnateCodeTask", "InnateCodeTask") + .WithMany() + .HasForeignKey("InnateCodeTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Timesheet", "Timesheet") + .WithMany("TimesheetEntries") + .HasForeignKey("TimesheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InnateCodeTask"); + + b.Navigation("Timesheet"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.User", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany() + .HasForeignKey("PersonId"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.WorkloadModelChange", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany("WorkloadModelChanges") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("PersonProject", b => + { + b.HasOne("PPMTool.Data.Entities.Project", null) + .WithMany() + .HasForeignKey("FollowedProjectsProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Person", null) + .WithMany() + .HasForeignKey("FollowersPersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SkillTagSubTask", b => + { + b.HasOne("PPMTool.Data.Entities.SkillTag", null) + .WithMany() + .HasForeignKey("SkillsRequiredSkillTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SubTask", null) + .WithMany() + .HasForeignKey("TasksNeedingThisSkillSubTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Competency", b => + { + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Faculty", b => + { + b.Navigation("Schools"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FundingSource", b => + { + b.Navigation("PaymentsFromSource"); + + b.Navigation("ResourcesFunded"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCode", b => + { + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Person", b => + { + b.Navigation("Absences"); + + b.Navigation("Assessments"); + + b.Navigation("ManagedProjects"); + + b.Navigation("OwnedSkills"); + + b.Navigation("PeopleManaged"); + + b.Navigation("Timesheets"); + + b.Navigation("TimesheetsChanged"); + + b.Navigation("WorkloadModelChanges"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Project", b => + { + b.Navigation("FundingSources"); + + b.Navigation("Invoices"); + + b.Navigation("Payments"); + + b.Navigation("SubTasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SkillTag", b => + { + b.Navigation("OwnedSkills"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SubTask", b => + { + b.Navigation("AssignedResources"); + + b.Navigation("Successors"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Timesheet", b => + { + b.Navigation("TimesheetEntries"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.User", b => + { + b.Navigation("AuthoredNotes"); + + b.Navigation("EditedNotes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PPMTool.Migrations.SqlServer/Migrations/20260615153605_AddActiveFlagToTimesheetTask.cs b/PPMTool.Migrations.SqlServer/Migrations/20260615153605_AddActiveFlagToTimesheetTask.cs new file mode 100644 index 000000000..d21e42654 --- /dev/null +++ b/PPMTool.Migrations.SqlServer/Migrations/20260615153605_AddActiveFlagToTimesheetTask.cs @@ -0,0 +1,33 @@ +๏ปฟ// SPDX-FileCopyrightText: 2026 University of Manchester +// +// SPDX-License-Identifier: apache-2.0 + +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PPMTool.Migrations.SqlServer.Migrations +{ + /// + public partial class AddActiveFlagToTimesheetTask : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsActive", + table: "InnateCodeTasks", + type: "bit", + nullable: false, + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsActive", + table: "InnateCodeTasks"); + } + } +} diff --git a/PPMTool.Migrations.SqlServer/Migrations/PPMToolContextModelSnapshot.cs b/PPMTool.Migrations.SqlServer/Migrations/PPMToolContextModelSnapshot.cs index 33533f2ef..693554e9e 100644 --- a/PPMTool.Migrations.SqlServer/Migrations/PPMToolContextModelSnapshot.cs +++ b/PPMTool.Migrations.SqlServer/Migrations/PPMToolContextModelSnapshot.cs @@ -21,7 +21,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("ProductVersion", "10.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -338,6 +338,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("InnateCodeId") .HasColumnType("int"); + b.Property("IsActive") + .HasColumnType("bit"); + b.Property("TaskName") .IsRequired() .HasColumnType("nvarchar(max)"); diff --git a/PPMTool.Migrations.Sqlite/Migrations/20260615153309_AddActiveFlagToTimesheetTask.Designer.cs b/PPMTool.Migrations.Sqlite/Migrations/20260615153309_AddActiveFlagToTimesheetTask.Designer.cs new file mode 100644 index 000000000..bf256ce9a --- /dev/null +++ b/PPMTool.Migrations.Sqlite/Migrations/20260615153309_AddActiveFlagToTimesheetTask.Designer.cs @@ -0,0 +1,1410 @@ +๏ปฟ// SPDX-FileCopyrightText: 2026 University of Manchester +// +// SPDX-License-Identifier: apache-2.0 + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PPMTool.Data.Context; + +#nullable disable + +namespace PPMTool.Migrations.Sqlite.Migrations +{ + [DbContext(typeof(PPMToolContext))] + [Migration("20260615153309_AddActiveFlagToTimesheetTask")] + partial class AddActiveFlagToTimesheetTask + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.8"); + + modelBuilder.Entity("PPMTool.Data.Entities.Absence", b => + { + b.Property("AbsenceId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.HasKey("AbsenceId"); + + b.HasIndex("PersonId"); + + b.ToTable("Absence"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.ApiKey", b => + { + b.Property("ApiKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerUserId") + .HasColumnType("INTEGER"); + + b.HasKey("ApiKeyId"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Competency", b => + { + b.Property("CompetencyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Grade") + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("LegacyId") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Objective") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Revision") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("CompetencyId"); + + b.ToTable("Competencies"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.CompetencyAssessment", b => + { + b.Property("CompetencyAssessmentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CompetencyDescription") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CompetencyId") + .HasColumnType("INTEGER"); + + b.Property("CompetencyObjective") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CompetencyRevision") + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Evidence") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("CompetencyAssessmentId"); + + b.HasIndex("CompetencyId"); + + b.HasIndex("PersonId"); + + b.ToTable("CompetencyAssessments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Faculty", b => + { + b.Property("FacultyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("FacultyId"); + + b.ToTable("Faculties"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Feature", b => + { + b.Property("FeatureId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("FeatureType") + .HasColumnType("INTEGER"); + + b.Property("MustAlwaysBeEnabled") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("FeatureId"); + + b.ToTable("Features"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FinancialReference", b => + { + b.Property("FinancialReferenceId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FinancialYear") + .HasColumnType("INTEGER"); + + b.Property("Grade41Costs") + .HasColumnType("REAL"); + + b.Property("Grade51Costs") + .HasColumnType("REAL"); + + b.Property("Grade55Costs") + .HasColumnType("REAL"); + + b.Property("Grade65Costs") + .HasColumnType("REAL"); + + b.Property("Grade71Costs") + .HasColumnType("REAL"); + + b.Property("Grade75Costs") + .HasColumnType("REAL"); + + b.Property("RecoveryTarget") + .HasColumnType("REAL"); + + b.HasKey("FinancialReferenceId"); + + b.ToTable("FinancialReferences"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FundingSource", b => + { + b.Property("FundingSourceId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountCode") + .HasColumnType("TEXT"); + + b.Property("AmountAvailable") + .HasColumnType("REAL"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FundingSourceType") + .HasColumnType("INTEGER"); + + b.Property("HasAccountCode") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("INTEGER"); + + b.HasKey("FundingSourceId"); + + b.HasIndex("ProjectId"); + + b.ToTable("FundingSources"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCode", b => + { + b.Property("InnateCodeId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsSensitive") + .HasColumnType("INTEGER"); + + b.HasKey("InnateCodeId"); + + b.ToTable("InnateCodes"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCodeTask", b => + { + b.Property("InnateCodeTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Duty") + .HasColumnType("INTEGER"); + + b.Property("InnateCodeId") + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("TaskName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnateCodeTaskId"); + + b.HasIndex("InnateCodeId"); + + b.ToTable("InnateCodeTasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Invoice", b => + { + b.Property("InvoiceId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InvoiceReference") + .HasColumnType("TEXT"); + + b.Property("InvoiceUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("KeyDate") + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("InvoiceId"); + + b.HasIndex("ProjectId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Note", b => + { + b.Property("NoteId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorUserId") + .HasColumnType("INTEGER"); + + b.Property("CompletedDate") + .HasColumnType("TEXT"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("DueDate") + .HasColumnType("TEXT"); + + b.Property("EditedDate") + .HasColumnType("TEXT"); + + b.Property("EditorUserId") + .HasColumnType("INTEGER"); + + b.Property("HtmlContent") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsFinanceInfo") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .HasColumnType("INTEGER"); + + b.HasKey("NoteId"); + + b.HasIndex("AuthorUserId"); + + b.HasIndex("EditorUserId"); + + b.HasIndex("ProjectId"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.OwnedSkill", b => + { + b.Property("OwnedSkillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FavouriteSkill") + .HasColumnType("INTEGER"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("OwnerPersonId") + .HasColumnType("INTEGER"); + + b.Property("Proficiency") + .HasColumnType("INTEGER"); + + b.Property("SkillTagId") + .HasColumnType("INTEGER"); + + b.HasKey("OwnedSkillId"); + + b.HasIndex("OwnerPersonId"); + + b.HasIndex("SkillTagId"); + + b.ToTable("OwnedSkills"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Payment", b => + { + b.Property("PaymentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("INTEGER"); + + b.Property("KeyDate") + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .HasColumnType("INTEGER"); + + b.Property("SourceFundingSourceId") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("PaymentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SourceFundingSourceId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Person", b => + { + b.Property("PersonId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("FTE") + .HasColumnType("REAL"); + + b.Property("LineManagerPersonId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ShortName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("TimesheetTemplateData") + .HasColumnType("TEXT"); + + b.HasKey("PersonId"); + + b.HasIndex("LineManagerPersonId"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Project", b => + { + b.Property("ProjectId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActualCost") + .HasColumnType("REAL"); + + b.Property("ActualIndirectCost") + .HasColumnType("REAL"); + + b.Property("ActualLeadershipCosts") + .HasColumnType("REAL"); + + b.Property("ActualWorkHours") + .HasColumnType("REAL"); + + b.Property("ActualsLastUpdated") + .HasColumnType("TEXT"); + + b.Property("Budget") + .HasColumnType("REAL"); + + b.Property("BudgetedIndirects") + .HasColumnType("REAL"); + + b.Property("CostModel") + .HasColumnType("INTEGER"); + + b.Property("DayRate") + .HasColumnType("REAL"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("InnateActivityInnateCodeId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PI") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlannedCost") + .HasColumnType("REAL"); + + b.Property("PlannedIndirectCost") + .HasColumnType("REAL"); + + b.Property("PlannedLeadershipCosts") + .HasColumnType("REAL"); + + b.Property("PlannedWorkHours") + .HasColumnType("REAL"); + + b.Property("ProjectManagerPersonId") + .HasColumnType("INTEGER"); + + b.Property("ProjectStatus") + .HasColumnType("INTEGER"); + + b.Property("RTP") + .HasColumnType("INTEGER"); + + b.Property("RequestDocLink") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SchoolId") + .HasColumnType("INTEGER"); + + b.Property("ScrumProjectLink") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.HasKey("ProjectId"); + + b.HasIndex("InnateActivityInnateCodeId"); + + b.HasIndex("ProjectManagerPersonId"); + + b.HasIndex("SchoolId"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Resource", b => + { + b.Property("ResourceId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActualCost") + .HasColumnType("REAL"); + + b.Property("ActualIndirectCost") + .HasColumnType("REAL"); + + b.Property("ActualWorkHours") + .HasColumnType("REAL"); + + b.Property("AssignmentFTE") + .HasColumnType("REAL"); + + b.Property("BilledFTE") + .HasColumnType("REAL"); + + b.Property("DayRate") + .HasColumnType("REAL"); + + b.Property("FundedFromFundingSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvisional") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("PlannedCost") + .HasColumnType("REAL"); + + b.Property("PlannedIndirectCost") + .HasColumnType("REAL"); + + b.Property("PlannedWorkHours") + .HasColumnType("REAL"); + + b.Property("SubTaskId") + .HasColumnType("INTEGER"); + + b.Property("UseProjectDayRate") + .HasColumnType("INTEGER"); + + b.HasKey("ResourceId"); + + b.HasIndex("FundedFromFundingSourceId"); + + b.HasIndex("PersonId"); + + b.HasIndex("SubTaskId"); + + b.ToTable("Resources"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.School", b => + { + b.Property("SchoolId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FacultyId") + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("SchoolId"); + + b.HasIndex("FacultyId"); + + b.ToTable("Schools"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Setting", b => + { + b.Property("SettingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("SettingType") + .HasColumnType("INTEGER"); + + b.Property("SettingValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("SettingId"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SkillTag", b => + { + b.Property("SkillTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ControlledName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("HasValidWikiLink") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Rareness") + .HasColumnType("INTEGER"); + + b.Property("RarenessCount") + .HasColumnType("INTEGER"); + + b.HasKey("SkillTagId"); + + b.ToTable("SkillTags"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SubTask", b => + { + b.Property("SubTaskId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActualCost") + .HasColumnType("REAL"); + + b.Property("ActualIndirectCost") + .HasColumnType("REAL"); + + b.Property("ActualWorkHours") + .HasColumnType("REAL"); + + b.Property("Demand") + .HasColumnType("REAL"); + + b.Property("DurationBillableDays") + .HasColumnType("INTEGER"); + + b.Property("DurationDays") + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("HasFixedEndDate") + .HasColumnType("INTEGER"); + + b.Property("HasFixedStart") + .HasColumnType("INTEGER"); + + b.Property("IsLeadershipTask") + .HasColumnType("INTEGER"); + + b.Property("Lag") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OriginalDemand") + .HasColumnType("REAL"); + + b.Property("OwningProjectProjectId") + .HasColumnType("INTEGER"); + + b.Property("PlannedCost") + .HasColumnType("REAL"); + + b.Property("PlannedIndirectCost") + .HasColumnType("REAL"); + + b.Property("PlannedWorkHours") + .HasColumnType("REAL"); + + b.Property("PredecessorSubTaskId") + .HasColumnType("INTEGER"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("TaskType") + .HasColumnType("INTEGER"); + + b.Property("UnmetDemand") + .HasColumnType("REAL"); + + b.HasKey("SubTaskId"); + + b.HasIndex("OwningProjectProjectId"); + + b.HasIndex("PredecessorSubTaskId"); + + b.ToTable("SubTasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Timesheet", b => + { + b.Property("TimesheetId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("DateStatusChanged") + .HasColumnType("TEXT"); + + b.Property("Info") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("StatusChangedByPersonId") + .HasColumnType("INTEGER"); + + b.HasKey("TimesheetId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("StatusChangedByPersonId"); + + b.ToTable("Timesheets"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.TimesheetEntry", b => + { + b.Property("TimesheetEntryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FridayHours") + .HasColumnType("REAL"); + + b.Property("InnateCodeTaskId") + .HasColumnType("INTEGER"); + + b.Property("MondayHours") + .HasColumnType("REAL"); + + b.Property("SaturdayHours") + .HasColumnType("REAL"); + + b.Property("SundayHours") + .HasColumnType("REAL"); + + b.Property("ThursdayHours") + .HasColumnType("REAL"); + + b.Property("TimesheetId") + .HasColumnType("INTEGER"); + + b.Property("TuesdayHours") + .HasColumnType("REAL"); + + b.Property("WednesdayHours") + .HasColumnType("REAL"); + + b.HasKey("TimesheetEntryId"); + + b.HasIndex("InnateCodeTaskId"); + + b.HasIndex("TimesheetId"); + + b.ToTable("TimesheetEntries"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.User", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CASUserName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastLoggedIn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("RoleType") + .HasColumnType("INTEGER"); + + b.HasKey("UserId"); + + b.HasIndex("PersonId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.WorkloadModelChange", b => + { + b.Property("WorkloadModelChangeId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArchitectureFTE") + .HasColumnType("REAL"); + + b.Property("BusinessAsUsualFTE") + .HasColumnType("REAL"); + + b.Property("ChangeDate") + .HasColumnType("TEXT"); + + b.Property("Grade") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("PersonalDevelopmentFTE") + .HasColumnType("REAL"); + + b.Property("ProjectAndServiceManagementFTE") + .HasColumnType("REAL"); + + b.Property("ProjectManagementFTE") + .HasColumnType("REAL"); + + b.Property("ProjectWorkFTE") + .HasColumnType("REAL"); + + b.Property("ServiceManagementFTE") + .HasColumnType("REAL"); + + b.Property("StaffManagementFTE") + .HasColumnType("REAL"); + + b.HasKey("WorkloadModelChangeId"); + + b.HasIndex("PersonId"); + + b.ToTable("WorkloadModelChanges"); + }); + + modelBuilder.Entity("PersonProject", b => + { + b.Property("FollowedProjectsProjectId") + .HasColumnType("INTEGER"); + + b.Property("FollowersPersonId") + .HasColumnType("INTEGER"); + + b.HasKey("FollowedProjectsProjectId", "FollowersPersonId"); + + b.HasIndex("FollowersPersonId"); + + b.ToTable("PersonProject"); + }); + + modelBuilder.Entity("SkillTagSubTask", b => + { + b.Property("SkillsRequiredSkillTagId") + .HasColumnType("INTEGER"); + + b.Property("TasksNeedingThisSkillSubTaskId") + .HasColumnType("INTEGER"); + + b.HasKey("SkillsRequiredSkillTagId", "TasksNeedingThisSkillSubTaskId"); + + b.HasIndex("TasksNeedingThisSkillSubTaskId"); + + b.ToTable("SkillTagSubTask"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Absence", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany("Absences") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.ApiKey", b => + { + b.HasOne("PPMTool.Data.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.CompetencyAssessment", b => + { + b.HasOne("PPMTool.Data.Entities.Competency", null) + .WithMany("Assessments") + .HasForeignKey("CompetencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Person", null) + .WithMany("Assessments") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FundingSource", b => + { + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany("FundingSources") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCodeTask", b => + { + b.HasOne("PPMTool.Data.Entities.InnateCode", "InnateCode") + .WithMany("Tasks") + .HasForeignKey("InnateCodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InnateCode"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Invoice", b => + { + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany("Invoices") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Note", b => + { + b.HasOne("PPMTool.Data.Entities.User", "Author") + .WithMany("AuthoredNotes") + .HasForeignKey("AuthorUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.User", "Editor") + .WithMany("EditedNotes") + .HasForeignKey("EditorUserId"); + + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Editor"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.OwnedSkill", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Owner") + .WithMany("OwnedSkills") + .HasForeignKey("OwnerPersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SkillTag", "SkillTag") + .WithMany("OwnedSkills") + .HasForeignKey("SkillTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("SkillTag"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Payment", b => + { + b.HasOne("PPMTool.Data.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId"); + + b.HasOne("PPMTool.Data.Entities.Project", "Project") + .WithMany("Payments") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.FundingSource", "Source") + .WithMany("PaymentsFromSource") + .HasForeignKey("SourceFundingSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Invoice"); + + b.Navigation("Project"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Person", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "LineManager") + .WithMany("PeopleManaged") + .HasForeignKey("LineManagerPersonId"); + + b.Navigation("LineManager"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Project", b => + { + b.HasOne("PPMTool.Data.Entities.InnateCode", "InnateActivity") + .WithMany() + .HasForeignKey("InnateActivityInnateCodeId"); + + b.HasOne("PPMTool.Data.Entities.Person", "ProjectManager") + .WithMany("ManagedProjects") + .HasForeignKey("ProjectManagerPersonId"); + + b.HasOne("PPMTool.Data.Entities.School", "School") + .WithMany() + .HasForeignKey("SchoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InnateActivity"); + + b.Navigation("ProjectManager"); + + b.Navigation("School"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Resource", b => + { + b.HasOne("PPMTool.Data.Entities.FundingSource", "FundedFrom") + .WithMany("ResourcesFunded") + .HasForeignKey("FundedFromFundingSourceId"); + + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SubTask", "SubTask") + .WithMany("AssignedResources") + .HasForeignKey("SubTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FundedFrom"); + + b.Navigation("Person"); + + b.Navigation("SubTask"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.School", b => + { + b.HasOne("PPMTool.Data.Entities.Faculty", "Faculty") + .WithMany("Schools") + .HasForeignKey("FacultyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Faculty"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SubTask", b => + { + b.HasOne("PPMTool.Data.Entities.Project", "OwningProject") + .WithMany("SubTasks") + .HasForeignKey("OwningProjectProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SubTask", "Predecessor") + .WithMany("Successors") + .HasForeignKey("PredecessorSubTaskId"); + + b.Navigation("OwningProject"); + + b.Navigation("Predecessor"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Timesheet", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Owner") + .WithMany("Timesheets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Person", "StatusChangedBy") + .WithMany("TimesheetsChanged") + .HasForeignKey("StatusChangedByPersonId"); + + b.Navigation("Owner"); + + b.Navigation("StatusChangedBy"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.TimesheetEntry", b => + { + b.HasOne("PPMTool.Data.Entities.InnateCodeTask", "InnateCodeTask") + .WithMany() + .HasForeignKey("InnateCodeTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Timesheet", "Timesheet") + .WithMany("TimesheetEntries") + .HasForeignKey("TimesheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InnateCodeTask"); + + b.Navigation("Timesheet"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.User", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany() + .HasForeignKey("PersonId"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.WorkloadModelChange", b => + { + b.HasOne("PPMTool.Data.Entities.Person", "Person") + .WithMany("WorkloadModelChanges") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("PersonProject", b => + { + b.HasOne("PPMTool.Data.Entities.Project", null) + .WithMany() + .HasForeignKey("FollowedProjectsProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.Person", null) + .WithMany() + .HasForeignKey("FollowersPersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SkillTagSubTask", b => + { + b.HasOne("PPMTool.Data.Entities.SkillTag", null) + .WithMany() + .HasForeignKey("SkillsRequiredSkillTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PPMTool.Data.Entities.SubTask", null) + .WithMany() + .HasForeignKey("TasksNeedingThisSkillSubTaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Competency", b => + { + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Faculty", b => + { + b.Navigation("Schools"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.FundingSource", b => + { + b.Navigation("PaymentsFromSource"); + + b.Navigation("ResourcesFunded"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.InnateCode", b => + { + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Person", b => + { + b.Navigation("Absences"); + + b.Navigation("Assessments"); + + b.Navigation("ManagedProjects"); + + b.Navigation("OwnedSkills"); + + b.Navigation("PeopleManaged"); + + b.Navigation("Timesheets"); + + b.Navigation("TimesheetsChanged"); + + b.Navigation("WorkloadModelChanges"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Project", b => + { + b.Navigation("FundingSources"); + + b.Navigation("Invoices"); + + b.Navigation("Payments"); + + b.Navigation("SubTasks"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SkillTag", b => + { + b.Navigation("OwnedSkills"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.SubTask", b => + { + b.Navigation("AssignedResources"); + + b.Navigation("Successors"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.Timesheet", b => + { + b.Navigation("TimesheetEntries"); + }); + + modelBuilder.Entity("PPMTool.Data.Entities.User", b => + { + b.Navigation("AuthoredNotes"); + + b.Navigation("EditedNotes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PPMTool.Migrations.Sqlite/Migrations/20260615153309_AddActiveFlagToTimesheetTask.cs b/PPMTool.Migrations.Sqlite/Migrations/20260615153309_AddActiveFlagToTimesheetTask.cs new file mode 100644 index 000000000..ca3888b72 --- /dev/null +++ b/PPMTool.Migrations.Sqlite/Migrations/20260615153309_AddActiveFlagToTimesheetTask.cs @@ -0,0 +1,33 @@ +๏ปฟ// SPDX-FileCopyrightText: 2026 University of Manchester +// +// SPDX-License-Identifier: apache-2.0 + +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PPMTool.Migrations.Sqlite.Migrations +{ + /// + public partial class AddActiveFlagToTimesheetTask : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsActive", + table: "InnateCodeTasks", + type: "INTEGER", + nullable: false, + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsActive", + table: "InnateCodeTasks"); + } + } +} diff --git a/PPMTool.Migrations.Sqlite/Migrations/PPMToolContextModelSnapshot.cs b/PPMTool.Migrations.Sqlite/Migrations/PPMToolContextModelSnapshot.cs index f7c7c3575..e7e8c7edb 100644 --- a/PPMTool.Migrations.Sqlite/Migrations/PPMToolContextModelSnapshot.cs +++ b/PPMTool.Migrations.Sqlite/Migrations/PPMToolContextModelSnapshot.cs @@ -19,7 +19,7 @@ partial class PPMToolContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.8"); modelBuilder.Entity("PPMTool.Data.Entities.Absence", b => { @@ -313,6 +313,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("InnateCodeId") .HasColumnType("INTEGER"); + b.Property("IsActive") + .HasColumnType("INTEGER"); + b.Property("TaskName") .IsRequired() .HasColumnType("TEXT"); diff --git a/PPMTool.Tests/API/BaseApiTest.cs b/PPMTool.Tests/API/BaseApiTest.cs index d4242aff3..a74b18a56 100644 --- a/PPMTool.Tests/API/BaseApiTest.cs +++ b/PPMTool.Tests/API/BaseApiTest.cs @@ -27,6 +27,12 @@ public static HttpClient GetClientAsManager() return client; } + [OneTimeSetUp] + public virtual void OneTimeSetup() + { + SetupForAPI(); + } + public void SetupForAPI() { // Get the API key to use from the database @@ -80,5 +86,20 @@ public void SetupForAPI() throw new Exception("No valid API keys found for a manager with a report in the database. Please create one for testing."); } } + + /// + /// Gets the start date for a date range query (one month ago). + /// + protected static string GetStartDate() => DateTime.Now.AddMonths(-1).ToString("yyyy-MM-dd"); + + /// + /// Gets the end date for a date range query (today). + /// + protected static string GetEndDate() => DateTime.Now.ToString("yyyy-MM-dd"); + + /// + /// Gets the current year for year-based queries. + /// + protected static int GetCurrentYear() => DateTime.Now.Year; } } diff --git a/PPMTool.Tests/API/LeaveBookings/EndpointOKTests.cs b/PPMTool.Tests/API/LeaveBookings/EndpointOKTests.cs new file mode 100644 index 000000000..2b536b3d2 --- /dev/null +++ b/PPMTool.Tests/API/LeaveBookings/EndpointOKTests.cs @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2026 University of Manchester +// +// SPDX-License-Identifier: apache-2.0 + +namespace PPMTool.Tests.API.LeaveBookings +{ + [TestFixture] + public class EndpointOKTests : BaseApiTest + { + [Test] + public async Task GetStaffBookingsForYearShouldReturnOKOrErrorDependingOnDatabaseAvailability() + { + using (var client = GetClientAsManager()) + { + var response = await client.GetAsync($"/leavebookings/getForSelfAndStaff?year={GetCurrentYear()}"); + // This endpoint may return 500 if the Leave Bookings database is not available + // We just verify that the endpoint is accessible and returns a valid response + Assert.That( + response.StatusCode == System.Net.HttpStatusCode.OK || + response.StatusCode == System.Net.HttpStatusCode.InternalServerError || + response.StatusCode == System.Net.HttpStatusCode.BadRequest + ); + } + } + } +} diff --git a/PPMTool.Tests/API/Skills/EndpointOKTests.cs b/PPMTool.Tests/API/Skills/EndpointOKTests.cs index ac35bc6a1..071e50dc5 100644 --- a/PPMTool.Tests/API/Skills/EndpointOKTests.cs +++ b/PPMTool.Tests/API/Skills/EndpointOKTests.cs @@ -16,5 +16,25 @@ public async Task GetAllSkillsShouldReturnOK() Assert.That(response.IsSuccessStatusCode); } } + + [Test] + public async Task GetAllSkillsForPersonShouldReturnOK() + { + using (var client = GetClientAsManager()) + { + var response = await client.GetAsync("/skills/getAllForPerson"); + Assert.That(response.IsSuccessStatusCode); + } + } + + [Test] + public async Task GetAllSkillsGroupedShouldReturnOK() + { + using (var client = GetClientAsManager()) + { + var response = await client.GetAsync("/skills/getAllGrouped"); + Assert.That(response.IsSuccessStatusCode); + } + } } -} \ No newline at end of file +} diff --git a/PPMTool.Tests/API/Timesheets/EndpointOKTests.cs b/PPMTool.Tests/API/Timesheets/EndpointOKTests.cs index 2a0dd7a0d..9854f4238 100644 --- a/PPMTool.Tests/API/Timesheets/EndpointOKTests.cs +++ b/PPMTool.Tests/API/Timesheets/EndpointOKTests.cs @@ -7,5 +7,24 @@ namespace PPMTool.Tests.API.Timesheets [TestFixture] public class EndpointOKTests : BaseApiTest { + [Test] + public async Task GetTimesheetEntriesForPersonForDateRangeShouldReturnOK() + { + using (var client = GetClientAsManager()) + { + var response = await client.GetAsync($"/timesheets/getEntries?startDate={GetStartDate()}&endDate={GetEndDate()}"); + Assert.That(response.IsSuccessStatusCode); + } + } + + [Test] + public async Task GetTimesheetBookingsByCodeAndTaskShouldReturnOK() + { + using (var client = GetClientAsManager()) + { + var response = await client.GetAsync($"/timesheets/getByCodeTask?startDate={GetStartDate()}&endDate={GetEndDate()}"); + Assert.That(response.IsSuccessStatusCode); + } + } } -} \ No newline at end of file +} diff --git a/PPMTool.Tests/API/WorkloadModelAnalysis/EndpointOKTests.cs b/PPMTool.Tests/API/WorkloadModelAnalysis/EndpointOKTests.cs new file mode 100644 index 000000000..a6d4f0de9 --- /dev/null +++ b/PPMTool.Tests/API/WorkloadModelAnalysis/EndpointOKTests.cs @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2026 University of Manchester +// +// SPDX-License-Identifier: apache-2.0 + +namespace PPMTool.Tests.API.WorkloadModelAnalysis +{ + [TestFixture] + public class EndpointOKTests : BaseApiTest + { + [Test] + public async Task GetWorkloadAnalysisDataShouldReturnOK() + { + using (var client = GetClientAsManager()) + { + // Query requires personNames parameter + // Use empty string to get data for the API key owner by default + var response = await client.GetAsync($"/wlm/getAnalysis?personNames=&startDate={GetStartDate()}&endDate={GetEndDate()}"); + + // The endpoint can return various status codes depending on parameters: + // 200 OK if successful + // 400 Bad Request if parameters are invalid + // We verify the endpoint is accessible and returns a valid response + Assert.That(response.IsSuccessStatusCode || response.StatusCode == System.Net.HttpStatusCode.BadRequest); + } + } + + [Test] + public async Task GetWorkloadAnalysisDataWithComparisonShouldReturnOK() + { + using (var client = GetClientAsManager()) + { + var response = await client.GetAsync($"/wlm/getAnalysis?personNames=&startDate={GetStartDate()}&endDate={GetEndDate()}&compareToWLM=true&normalisedByTotalHours=true"); + + Assert.That(response.IsSuccessStatusCode || response.StatusCode == System.Net.HttpStatusCode.BadRequest); + } + } + } +} diff --git a/PPMTool.Tests/Core/PageLoadTests.cs b/PPMTool.Tests/Core/PageLoadTests.cs index a83587679..4e1e40498 100644 --- a/PPMTool.Tests/Core/PageLoadTests.cs +++ b/PPMTool.Tests/Core/PageLoadTests.cs @@ -4,22 +4,323 @@ namespace PPMTool.Tests.Core { - [Parallelizable(ParallelScope.Self)] + [Parallelizable(ParallelScope.None)] [TestFixture] public class PageLoadTests : PageTest { - [Test] - public async Task HomepageShouldLoadWithCorrectTitleAndNoCrashBanner() + // Timeouts (in milliseconds) + private const int pageLoadTimeoutMs = 5000; + private const int pageTitleTimeoutMs = 2000; + private const int loginButtonTimeoutMs = 2000; + private const int tearDownDelayMs = 1000; + private const int pageStabilizationDelayMs = 1000; + private const int loginAuthenticationDelayMs = 1000; + private const int loginRetryDelayMs = 1000; + + // Retry configuration + private const int navigationRetries = 5; + private const int retryDelayMs = 1000; + private const int maxLoginAttempts = 3; + + [TearDown] + public async Task TearDownTest() + { + // Clear cookies and storage to ensure clean state between tests + // This forces re-authentication each time + try + { + await Page.Context.ClearCookiesAsync(); + await Page.EvaluateAsync("() => localStorage.clear()"); + await Page.EvaluateAsync("() => sessionStorage.clear()"); + } + catch + { + // Ignore errors during cleanup + } + + // Add a delay between tests to allow the server to recover + await Task.Delay(tearDownDelayMs); + } + + /// + /// Verifies that a page loads correctly by checking the title and ensuring that the Blazor crash banner is not visible. + /// + /// + /// + /// + /// + private async Task VerifyPageLoaded(string url, string expectedTitle, bool skipLoginCheck = false) { - // Navigate the browser to the homepage - await Page.GotoAsync($"{Setup.BaseUrl}"); + // Navigate to the page with retry logic + await NavigateWithRetryAsync($"{Setup.BaseUrl}{url}"); + + // Check if we're on the login page (unauthenticated) by checking the page title + if (!skipLoginCheck) + { + // Wait a bit for the page to stabilize + await Task.Delay(pageStabilizationDelayMs); + + // Wait for the page to have a title using timeout via context options + var titleRegex = new Regex(".*"); + await Expect(Page).ToHaveTitleAsync(titleRegex); + + var currentTitle = await Page.TitleAsync(); + if (currentTitle == "CapX - Log In") + { + // We're on the login page, need to authenticate + Console.WriteLine($"Detected login page, authenticating before navigating to {url}"); + await HandleLoginAndNavigateAsync(url); + } + else + { + // Verify we have the content we expect by waiting for page to fully load + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + } // Assert that the page title is correct - await Expect(Page).ToHaveTitleAsync("CapX - Log In"); + await Expect(Page).ToHaveTitleAsync(expectedTitle); // Assert that the Blazor crash banner is not visible var crashBanner = Page.Locator("#blazor-error-ui"); - await Expect(crashBanner).Not.ToBeVisibleAsync(); + await Expect(crashBanner).ToBeHiddenAsync(); + } + + /// + /// Navigates to a URL with retry logic to handle temporary connection issues. + /// + /// + /// + private async Task NavigateWithRetryAsync(string url) + { + int retries = 0; + while (retries < navigationRetries) + { + try + { + await Page.GotoAsync(url, new() { WaitUntil = WaitUntilState.NetworkIdle }); + return; + } + catch (Exception ex) when (retries < navigationRetries - 1 && + (ex.Message.Contains("ERR_CONNECTION_REFUSED") || + ex.Message.Contains("ERR_ABORTED") || + ex.Message.Contains("ERR_NETWORK_CHANGED"))) + { + retries++; + await Task.Delay(retryDelayMs); + } + } + + // Final attempt without retry + await Page.GotoAsync(url, new() { WaitUntil = WaitUntilState.NetworkIdle }); + } + + /// + /// Handles the login process by clicking the Log In button and waiting for authentication to complete, + /// then navigates to the target URL. Includes retry logic to handle transient failures. + /// + /// + /// + private async Task HandleLoginAndNavigateAsync(string targetUrl) + { + int loginAttempts = 0; + + while (loginAttempts < maxLoginAttempts) + { + try + { + // Find the Log in button - in LOCAL mode this link contains the username parameter + var loginButton = Page.Locator("a:has-text('Log in')").First; + + if (await loginButton.IsVisibleAsync()) + { + Console.WriteLine($"Login attempt {loginAttempts + 1}/{maxLoginAttempts}: Clicking login button"); + + // Click the Log in button + await loginButton.ClickAsync(); + + // Wait for navigation to complete + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // Small delay to let authentication finish + await Task.Delay(loginAuthenticationDelayMs); + + // Navigate to the target page + await NavigateWithRetryAsync($"{Setup.BaseUrl}{targetUrl}"); + + // Verify we're not on the login page anymore + var titleAfterLogin = await Page.TitleAsync(); + if (titleAfterLogin != "CapX - Log In") + { + Console.WriteLine($"Successfully authenticated and navigated to {targetUrl}"); + return; + } + else + { + throw new InvalidOperationException("Still on login page after clicking login button"); + } + } + else + { + throw new InvalidOperationException("Login button not visible on login page"); + } + } + catch (Exception ex) + { + loginAttempts++; + if (loginAttempts >= maxLoginAttempts) + { + throw new InvalidOperationException($"Failed to authenticate after {maxLoginAttempts} attempts: {ex.Message}", ex); + } + + Console.WriteLine($"Login attempt failed: {ex.Message}. Retrying..."); + await Task.Delay(loginRetryDelayMs); + } + } + } + + [Test] + public async Task LogInPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/", "CapX - Log In", true); + } + + [Test] + public async Task MyProjectsPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/myprojects", "My Projects - CapX"); + } + + [Test] + public async Task ProjectsPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/projects", "Projects - CapX"); + } + + [Test] + public async Task PeoplePageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/people", "People - CapX"); + } + + [Test] + public async Task CapacityPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/capacity", "Capacity - CapX"); + } + + [Test] + public async Task TimesheetsPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/timesheets", "Timesheets - CapX"); + } + + [Test] + public async Task AbsencesPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/absences", "Absences - CapX"); + } + + [Test] + public async Task CompetencyFrameworkPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/competencies", "Development Journey - CapX"); + } + + [Test] + public async Task ManageSkillsPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/manageskills", "Manage Skills - CapX"); + } + + [Test] + public async Task ManageAccessPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/manageaccess", "Manage Access - CapX"); + } + + [Test] + public async Task ManageSettingsPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/managesettings", "Manage Settings - CapX"); + } + + [Test] + public async Task ManageFeaturesPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/managefeatures", "Manage Features - CapX"); + } + + [Test] + public async Task ManageOrgUnitsPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/manageorgunits", "Manage Org Units - CapX"); + } + + [Test] + public async Task ManageInnateCodesPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/managecodes", "Manage Timesheet Codes - CapX"); + } + + [Test] + public async Task ManageFinancialItemsPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/managefinancialitems", "Manage Finance Items - CapX"); + } + + [Test] + public async Task ManageFinancialReferencesPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/managefinref", "Manage Financial Refs - CapX"); + } + + [Test] + public async Task FinanceSummaryPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/managefinancialitems/summary", "Finance Summary - CapX"); + } + + [Test] + public async Task DataDashboardPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/datadashboard", "Data Dashboard - CapX"); + } + + [Test] + public async Task EstimateCostPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/estimatecost", "Estimate Cost - CapX"); + } + + [Test] + public async Task ManagementCapacityPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/managementcapacity", "Management Capacity - CapX"); + } + + [Test] + public async Task WorkloadModelAnalysisPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/wlmanalysis", "WLM Analysis - CapX"); + } + + [Test] + public async Task ProjectBulletinBoardPageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/projectbulletinboard", "Available Projects - CapX"); + } + + [Test] + public async Task UserProfilePageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/profile", "Profile - CapX"); + } + + [Test] + public async Task NothingHerePageShouldLoadWithCorrectTitleAndNoCrashBanner() + { + await VerifyPageLoaded("/nothinghere", "Nothing Here - CapX"); } } } diff --git a/PPMTool.Tests/PPMTool.Tests.csproj b/PPMTool.Tests/PPMTool.Tests.csproj index 058f8f85c..a5c0e447e 100644 --- a/PPMTool.Tests/PPMTool.Tests.csproj +++ b/PPMTool.Tests/PPMTool.Tests.csproj @@ -16,14 +16,14 @@ SPDX-License-Identifier: apache-2.0 - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -35,6 +35,7 @@ SPDX-License-Identifier: apache-2.0 + diff --git a/PPMTool.Tests/Setup.cs b/PPMTool.Tests/Setup.cs index 702dcdc69..5338867ed 100644 --- a/PPMTool.Tests/Setup.cs +++ b/PPMTool.Tests/Setup.cs @@ -10,13 +10,51 @@ public class Setup public static string BaseUrl { get; } = "https://localhost:5001"; [OneTimeSetUp] - public void SetupForAll() + public async Task SetupForAll() { + // Wait for the server to be ready before running tests + await WaitForServerAsync(); } [OneTimeTearDown] public void TearDown() { } + + /// + /// Waits for the application server to be ready before running tests. + /// This ensures that the server is accessible and responding to requests. + /// + private static async Task WaitForServerAsync(int maxRetries = 30, int delayMs = 1000) + { + using var handler = new HttpClientHandler(); + // Ignore SSL certificate issues for localhost testing + handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true; + + using var client = new HttpClient(handler); + var retries = 0; + + while (retries < maxRetries) + { + try + { + var response = await client.GetAsync($"{BaseUrl}/"); + if (response.IsSuccessStatusCode) + { + Console.WriteLine("โœ“ Server is ready"); + return; + } + } + catch (Exception ex) + { + Console.WriteLine($"Server not ready yet ({retries + 1}/{maxRetries}): {ex.Message}"); + } + + retries++; + await Task.Delay(delayMs); + } + + throw new InvalidOperationException($"Server at {BaseUrl} did not become ready after {maxRetries * delayMs}ms"); + } } -} \ No newline at end of file +} diff --git a/PPMTool/App.razor b/PPMTool/App.razor index 51d90e2cf..c30fb06e2 100644 --- a/PPMTool/App.razor +++ b/PPMTool/App.razor @@ -45,7 +45,6 @@ SPDX-License-Identifier: apache-2.0 .auth-message p { margin: 0 0 1.25rem; - color: #4b5563; }

- +

@@ -87,11 +87,31 @@ SPDX-License-Identifier: apache-2.0 ValueProperty="Value" TextProperty="Text" /> + + + + + + + + + diff --git a/PPMTool/Pages/AddInnateCode.razor.cs b/PPMTool/Pages/AddInnateCode.razor.cs index e1800b8c4..746427cea 100644 --- a/PPMTool/Pages/AddInnateCode.razor.cs +++ b/PPMTool/Pages/AddInnateCode.razor.cs @@ -20,7 +20,11 @@ public partial class AddInnateCode : DataGridPage [Inject] private InnateCodeService InnateCodeService { get; set; } + [Inject] + private TimesheetService TimesheetService { get; set; } + private InnateCode innateCode; + private List tasksWithBookings = new List(); /// /// Represents a duty and its text string representation for dropdown sources. @@ -32,14 +36,17 @@ public record DutyOption(Duty Value, string Text); private IEnumerable duties = Enum.GetValues().Select(d => new DutyOption(d, d.GetDescription())); - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { - base.OnInitialized(); + await base.OnInitializedAsync(); if (InnateCodeId > 0) { innateCode = InnateCodeService.GetById(Context, InnateCodeId); dataGridEntities = innateCode.Tasks.ToList(); + + // Check the booking status of the tasks and build list + tasksWithBookings = await TimesheetService.GetTaskIdsWithBookingsForCodeAsync(Context, InnateCodeId); } else { @@ -154,9 +161,24 @@ private async void AddDefaults() { TaskName = "Maintenance", InnateCode = innateCode, - Duty = Duty.ProjectWork + Duty = Duty.BAU }); await dataGrid.Reload(); } + + /// + /// Callback when the switch is changed + /// + private void ActivityActiveChanged(bool value) + { + // If setting to false then deactivate all the tasks + if (!value) + { + foreach (var entity in dataGridEntities) + { + entity.IsActive = false; + } + } + } } } diff --git a/PPMTool/Pages/AddPerson.razor.cs b/PPMTool/Pages/AddPerson.razor.cs index 85c50372c..9fa05f7cd 100644 --- a/PPMTool/Pages/AddPerson.razor.cs +++ b/PPMTool/Pages/AddPerson.razor.cs @@ -172,6 +172,9 @@ private void EditSkills() } } + /// + /// Method to handle the submit action for the form, including validation and saving the person model + /// private void HandleSubmit() { ClearErrorMessage(); @@ -205,6 +208,9 @@ private void HandleSubmit() messageStore.Add(() => personModel.ShortName, "Duplicate initials found!"); } } + + // Update any linked user display name + UserService.UpdateDisplayName(Context, personModel); } else { diff --git a/PPMTool/Pages/AddPersonSkill.razor b/PPMTool/Pages/AddPersonSkill.razor index 0a817effa..1cfd984d2 100644 --- a/PPMTool/Pages/AddPersonSkill.razor +++ b/PPMTool/Pages/AddPersonSkill.razor @@ -65,7 +65,7 @@ SPDX-License-Identifier: apache-2.0 Data="@ownedTags" TItem="OwnedSkill"> - + - - - + + + - + @if (skillsEnabled) @@ -95,7 +95,7 @@ SPDX-License-Identifier: apache-2.0 - + diff --git a/PPMTool/Pages/People.razor.cs b/PPMTool/Pages/People.razor.cs index bd54972e4..d26baa982 100644 --- a/PPMTool/Pages/People.razor.cs +++ b/PPMTool/Pages/People.razor.cs @@ -112,87 +112,45 @@ private void LoadData(LoadDataArgs args) query = query.Where(x => ActiveUser.Person != null && x.PersonId == ActiveUser.Person!.PersonId); } - // Now apply the custom filters (pattern matching syntax for non-null object) - if (args.Filters is { } filters && filters.Any()) + // ---- GRID FILTERING ---- + if (!string.IsNullOrWhiteSpace(args.Filter)) { - // Filter on Skill Tags - var filter = args.Filters.FirstOrDefault(x => x.Property == "SkillTags"); - var filterValue = filter?.FilterValue as string; - if (filter != null && filterValue != null) - { - query = query.Where(x => x.OwnedSkills.Any(x => x.SkillTag.Name.Trim().ToLower().Contains(filterValue.Trim().ToLower()))); - } + query = query.Where(args.Filter); + } + + // ---- SORTING ---- + if (args.Sorts != null && args.Sorts.Count() > 0) + { + var sort = args.Sorts.First(); - // Add filter on Line Manager - filter = filters.FirstOrDefault(f => f.Property == "LineManager"); - filterValue = (filter?.FilterValue as string)?.Trim(); - if (!string.IsNullOrEmpty(filterValue)) + // Special-case sort + if (sort.Property == "SkillsCount") { - var filterValueLower = filterValue.ToLower(); - query = query.Where(x => - x.LineManager != null && - (x.LineManager.Name ?? "").Trim().ToLower().Contains(filterValueLower)); + query = sort.SortOrder == SortOrder.Ascending + ? query.OrderBy(x => x.OwnedSkills.Count()) + : query.OrderByDescending(x => x.OwnedSkills.Count()); } - - // Add any filters than are none of the special ones - var stdFilters = args.Filters.Where(x => x.Property != "SkillTags" && x.Property != "LineManager"); - if (stdFilters.Any()) + else { - // Filter via the Where method - query = query.Where(args.Filter); + query = query.OrderBy(args.OrderBy); } } - - // Sorting needed - if (!string.IsNullOrEmpty(args.OrderBy)) + else if (!string.IsNullOrWhiteSpace(args.OrderBy)) { - // Check details of the sort - if (args.Sorts is { } sorts && sorts.Any()) - { - var sort = args.Sorts?.First(); - if (sort.Property == "SkillsCount") - { - // Apply the ordering process on skills count manually - if (sort.SortOrder == SortOrder.Ascending) - { - query = query.OrderBy(x => TagService.GetCountForPerson(Context, x.PersonId)); - } - else - { - query = query.OrderByDescending(x => TagService.GetCountForPerson(Context, x.PersonId)); - } - } - else if (sort.Property == "LineManager") - { - query = sort.SortOrder == SortOrder.Ascending ? - query.OrderBy(x => x.LineManager != null ? x.LineManager.Name : "") : - query.OrderByDescending(x => x.LineManager != null ? x.LineManager.Name : ""); - } - - // No special handling required - else - { - // Sort via the OrderBy method - query = query.OrderBy(args.OrderBy); - } - } + query = query.OrderBy(args.OrderBy); } - // Important!!! Make sure the Count property of RadzenDataGrid is set. - var data = query.ToList(); - count = data.Count(); + // ---- COUNT BEFORE PAGING ---- + count = query.Count(); - // Perform paging via Skip and Take. - if (args.Skip == null) - { - people = data.Take(pageCount).ToList(); - } - else - { - people = data.Skip(args.Skip.Value).Take(args.Top.Value).ToList(); - } + // ---- PAGING ---- + var skip = args.Skip ?? 0; + var take = args.Top ?? pageCount; + + people = query.Skip(skip).Take(take).ToList(); - Debug.WriteLine($"** {data.Count()} people loaded. {people.Count()} displayed."); + Debug.WriteLine($"** {count} people loaded. {people.Count()} displayed."); } + } } \ No newline at end of file diff --git a/PPMTool/Pages/Projects.razor b/PPMTool/Pages/Projects.razor index 1ccabce30..385c24148 100644 --- a/PPMTool/Pages/Projects.razor +++ b/PPMTool/Pages/Projects.razor @@ -44,7 +44,7 @@ SPDX-License-Identifier: apache-2.0 - + @@ -71,18 +71,25 @@ SPDX-License-Identifier: apache-2.0 @p.ProjectStatus.ToNiceString() - + - + - + - + @@ -113,20 +127,48 @@ SPDX-License-Identifier: apache-2.0 @(p.CostModel.GetAttribute().Name) - + - - - + + + } - +