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