From 299d6ff4dc50e406491cd0954281fa2682995470 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 22 Oct 2026 15:42:36 +0200 Subject: [PATCH] Improved compliance monitoring --- .../Services/Execution/ExecutionService.cs | 10 + .../Services/Grants/GrantsService.cs | 5 + .../Services/Projects/ProjectService.cs | 33 +- .../Services/Projects/Records.cs | 13 +- PostFundManagement.ConsoleUI/src/App.tsx | 17 + PostFundManagement.ConsoleUI/src/auth/rbac.ts | 2 + .../src/components/layout/AppLayout.tsx | 7 + .../projects/ProjectComplianceModal.tsx | 510 ++++ .../projects/ProjectMilestonesModal.tsx | 93 +- .../components/projects/ProjectViewModal.tsx | 69 +- .../src/components/ui/StatusBadge.tsx | 13 +- .../src/lib/formatters.ts | 32 + .../src/pages/BeneficiariesPage.tsx | 117 +- .../src/pages/CompliancePage.tsx | 529 ++++ .../src/pages/ExecutiveDashboard.tsx | 162 +- .../src/pages/ProjectsPage.tsx | 124 +- .../src/services/api.ts | 5 + PostFundManagement.ConsoleUI/src/types.ts | 17 +- .../Entities/ComplianceItem.cs | 13 +- PostFundManagement.Domain/Entities/Outcome.cs | 4 +- PostFundManagement.Domain/Enums.cs | 9 +- .../Extensions/Mappers.cs | 4 +- .../Models/ComplianceItem.cs | 6 +- PostFundManagement.Domain/Models/Outcome.cs | 4 +- ...lianceStatusAndBeneficiaryLink.Designer.cs | 2519 +++++++++++++++++ ...AddedComplianceStatusAndBeneficiaryLink.cs | 60 + .../ApplicationDbContextModelSnapshot.cs | 17 + 27 files changed, 4245 insertions(+), 149 deletions(-) create mode 100644 PostFundManagement.ConsoleUI/src/components/projects/ProjectComplianceModal.tsx create mode 100644 PostFundManagement.ConsoleUI/src/pages/CompliancePage.tsx create mode 100644 PostFundManagement.Infrastructure/Database/Migrations/20261022124137_AddedComplianceStatusAndBeneficiaryLink.Designer.cs create mode 100644 PostFundManagement.Infrastructure/Database/Migrations/20261022124137_AddedComplianceStatusAndBeneficiaryLink.cs diff --git a/PostFundManagement.Application/Services/Execution/ExecutionService.cs b/PostFundManagement.Application/Services/Execution/ExecutionService.cs index 0d26dfb..b256359 100644 --- a/PostFundManagement.Application/Services/Execution/ExecutionService.cs +++ b/PostFundManagement.Application/Services/Execution/ExecutionService.cs @@ -94,6 +94,16 @@ public sealed class ExecutionService(IDbContextFactory con await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + var disbursement = await context.Disbursements.AsNoTracking() + .FirstOrDefaultAsync(d => d.Id == decodeResult.Value, cancellationToken); + if (disbursement is null) + return Result.Fail(NotFoundError.Create("Disbursement not found.")); + + var isAwardApproved = await context.Awards + .AnyAsync(a => a.ProjectId == disbursement.ProjectId && a.Status == ApprovalStatus.Approved, cancellationToken); + if (!isAwardApproved) + return Result.Fail(ValidationError.Create("Cannot approve disbursement: The project's linked award is not approved or does not exist.")); + var changedTotal = await context.Disbursements.Where(d => d.Id == decodeResult.Value) .ExecuteUpdateAsync(f => f .SetProperty(f => f.Status, ApprovalStatus.Approved) diff --git a/PostFundManagement.Application/Services/Grants/GrantsService.cs b/PostFundManagement.Application/Services/Grants/GrantsService.cs index 60065aa..63c8f40 100644 --- a/PostFundManagement.Application/Services/Grants/GrantsService.cs +++ b/PostFundManagement.Application/Services/Grants/GrantsService.cs @@ -161,6 +161,11 @@ public sealed class GrantsService(IDbContextFactory contex if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) return Result.Fail(NotFoundError.New); + var hasApprovedAward = await context.Awards + .AnyAsync(a => a.ProjectId == request.ProjectId && a.Status == ApprovalStatus.Approved, cancellationToken); + if (!hasApprovedAward) + return Result.Fail(ValidationError.Create("Cannot create budget: The project's linked award is not approved or does not exist.")); + var budget = new Domain.Entities.Budget { ProjectId = request.ProjectId, diff --git a/PostFundManagement.Application/Services/Projects/ProjectService.cs b/PostFundManagement.Application/Services/Projects/ProjectService.cs index eecc9ec..fb9cc63 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectService.cs @@ -20,6 +20,27 @@ public sealed class ProjectService(IDbContextFactory conte if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken)) return Result.Fail(NotFoundError.New); + if (request.Status == ApprovalStatus.Approved) + { + var hasUnmetCompliance = await context.ComplianceItems + .AnyAsync(c => c.ProjectId == request.ProjectId && c.Status == ComplianceStatus.NotMet, cancellationToken); + if (hasUnmetCompliance) + return Result.Fail(ValidationError.Create("Project cannot be approved because some compliance items are not met.")); + + var projectBeneficiaryIds = await context.Beneficiaries + .Where(b => b.ProjectId == request.ProjectId) + .Select(b => b.Id) + .ToListAsync(cancellationToken); + + if (projectBeneficiaryIds.Count > 0) + { + var hasUnmetOutcomes = await context.Outcomes + .AnyAsync(o => projectBeneficiaryIds.Contains(o.BeneficiaryId) && o.Status == ComplianceStatus.NotMet, cancellationToken); + if (hasUnmetOutcomes) + return Result.Fail(ValidationError.Create("Project cannot be approved because some beneficiary outcomes are not met.")); + } + } + var changedTotal = await context.Projects .Where(f => f.Id == request.ProjectId) .ExecuteUpdateAsync(f => f @@ -544,8 +565,9 @@ public sealed class ProjectService(IDbContextFactory conte CreatedAt = DateTime.UtcNow, CreatedBy = userId, ProjectId = request.ProjectId, + BeneficiaryId = request.BeneficiaryId, Requirements = request.Requirements, - Status = ApprovalStatus.Pending, + Status = request.Status, TenantId = tenantContext.TenantId, Title = request.Title.Humanize(LetterCasing.Title).Trim() }); @@ -563,7 +585,10 @@ public sealed class ProjectService(IDbContextFactory conte .ExecuteUpdateAsync(f => f .SetProperty(u => u.Title, request.Title.Humanize(LetterCasing.Title).Trim()) .SetProperty(u => u.Requirements, request.Requirements) - .SetProperty(u => u.Status, request.Status), cancellationToken); + .SetProperty(u => u.BeneficiaryId, request.BeneficiaryId) + .SetProperty(u => u.Status, request.Status) + .SetProperty(u => u.UpdatedAt, DateTime.UtcNow) + .SetProperty(u => u.VerifiedBy, userId), cancellationToken); return Result.OkIf(changedTotal > 0, UpdateError.Create("No records changed")); })); @@ -768,6 +793,7 @@ public sealed class ProjectService(IDbContextFactory conte CreatedBy = userId, Description = request.Description, Name = request.Name.Humanize(LetterCasing.Title).Trim(), + Status = request.Status, TenantId = tenantContext.TenantId }); @@ -783,7 +809,8 @@ public sealed class ProjectService(IDbContextFactory conte .Where(o => o.Id == request.OutcomeId) .ExecuteUpdateAsync(f => f .SetProperty(u => u.Description, request.Description) - .SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim()), cancellationToken); + .SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim()) + .SetProperty(u => u.Status, request.Status), cancellationToken); return Result.OkIf(changedTotal > 0, UpdateError.Create("No records changed")); })); diff --git a/PostFundManagement.Application/Services/Projects/Records.cs b/PostFundManagement.Application/Services/Projects/Records.cs index 4229c05..5ba6a1b 100644 --- a/PostFundManagement.Application/Services/Projects/Records.cs +++ b/PostFundManagement.Application/Services/Projects/Records.cs @@ -15,11 +15,13 @@ public sealed record CreateSiteVisitRequest([Required] long ProjectId, public sealed record UpdateOutcomeRequest([Required] long OutcomeId, [Required][MaxLength(LabelLength)] string Name, - [Required][MaxLength(TextLength)] string Description); + [Required][MaxLength(TextLength)] string Description, + [Required] ComplianceStatus Status = ComplianceStatus.NotMet); public sealed record CreateOutcomeRequest([Required] long BeneficiaryId, [Required][MaxLength(LabelLength)] string Name, - [Required][MaxLength(TextLength)] string Description); + [Required][MaxLength(TextLength)] string Description, + ComplianceStatus Status = ComplianceStatus.NotMet); public sealed record UpdateIssueRequest([Required] long IssueId, [Required][MaxLength(LabelLength)] string Title, @@ -53,11 +55,14 @@ public sealed record CreateBeneficiaryRequest([Required] long ProjectId, public sealed record UpdateComplianceItemRequest([Required] long ComplianceItemId, [Required][MaxLength(LabelLength)] string Title, [Required][MaxLength(TextLength)] string Requirements, - [Required] ApprovalStatus Status); + [Required] ComplianceStatus Status, + long? BeneficiaryId = null); public sealed record CreateComplianceItemRequest([Required] long ProjectId, [Required][MaxLength(LabelLength)] string Title, - [Required][MaxLength(TextLength)] string Requirements); + [Required][MaxLength(TextLength)] string Requirements, + ComplianceStatus Status = ComplianceStatus.NotMet, + long? BeneficiaryId = null); public sealed record UpdateChangeRequest([Required] long ChangeRequestId, [Required][MaxLength(LabelLength)] string Title, diff --git a/PostFundManagement.ConsoleUI/src/App.tsx b/PostFundManagement.ConsoleUI/src/App.tsx index 5fa292b..558c633 100644 --- a/PostFundManagement.ConsoleUI/src/App.tsx +++ b/PostFundManagement.ConsoleUI/src/App.tsx @@ -14,6 +14,7 @@ const ProjectsPage = lazy(() => import('./pages/ProjectsPage').then(m => ({ defa const BeneficiariesPage = lazy(() => import('./pages/BeneficiariesPage').then(m => ({ default: m.BeneficiariesPage }))); const ChangeManagementPage = lazy(() => import('./pages/ChangeManagementPage').then(m => ({ default: m.ChangeManagementPage }))); const SiteVisitsPage = lazy(() => import('./pages/SiteVisitsPage').then(m => ({ default: m.SiteVisitsPage }))); +const CompliancePage = lazy(() => import('./pages/CompliancePage').then(m => ({ default: m.CompliancePage }))); const GrantsPage = lazy(() => import('./pages/GrantsPage').then(m => ({ default: m.GrantsPage }))); const MonitoringPage = lazy(() => import('./pages/MonitoringPage').then(m => ({ default: m.MonitoringPage }))); const TenantManagementPage = lazy(() => import('./pages/TenantManagementPage').then(m => ({ default: m.TenantManagementPage }))); @@ -132,6 +133,22 @@ export default function App() { } /> + + + + } + /> + + + + } + /> void; + project: Project | null; + programme: Programme | null; +} + +export const ProjectComplianceModal: React.FC = ({ + isOpen, + onClose, + project, + programme, +}) => { + const projectId = project?.id; + + // Queries + const { data: complianceItems = [], isLoading: loadingCompliance } = useQuery({ + queryKey: ['complianceItems', projectId], + queryFn: () => (projectId ? getProjectComplianceItems(projectId) : Promise.resolve([])), + enabled: isOpen && !!projectId, + }); + + const { data: projectBeneficiaries = [], isLoading: loadingBeneficiaries } = useQuery({ + queryKey: ['projectBeneficiaries', projectId], + queryFn: () => (projectId ? getProjectBeneficiaries(projectId) : Promise.resolve([])), + enabled: isOpen && !!projectId, + }); + + const { data: risks = [], isLoading: loadingRisks } = useQuery({ + queryKey: ['risks', projectId], + queryFn: () => (projectId ? getProjectRisks(projectId) : Promise.resolve([])), + enabled: isOpen && !!projectId, + }); + + const { data: milestones = [] } = useQuery({ + queryKey: ['milestones', projectId], + queryFn: () => (projectId ? getMilestones(projectId) : Promise.resolve([])), + enabled: isOpen && !!projectId, + }); + + const { data: activities = [] } = useQuery({ + queryKey: ['activities', projectId], + queryFn: () => (projectId ? getActivities(projectId) : Promise.resolve([])), + enabled: isOpen && !!projectId, + }); + + const { data: allBudgets = [] } = useQuery({ + queryKey: ['budgets'], + queryFn: () => getBudgets(1, 100), + enabled: isOpen && !!projectId, + }); + +// Beneficiaries already filtered by project + + // Beneficiary outcomes query + const { data: outcomes = [], isLoading: loadingOutcomes } = useQuery({ + queryKey: ['beneficiaryOutcomes', projectBeneficiaries.map((b) => b.id)], + queryFn: async (): Promise => { + if (projectBeneficiaries.length === 0) return []; + const results = await Promise.all( + projectBeneficiaries.map(async (b) => { + try { + return await getBeneficiaryOutcomes(b.id); + } catch { + return []; + } + }) + ); + return results.flat(); + }, + enabled: isOpen && projectBeneficiaries.length > 0, + }); + + if (!project) return null; + + // Compliance calculations + const totalCompCount = complianceItems.length; + const metCompCount = complianceItems.filter( + (c) => Number(c.status) === ComplianceStatus.Met || Number(c.status) === ComplianceStatus.Exceeded + ).length; + const unmetCompCount = totalCompCount - metCompCount; + + // Outcomes calculations + const totalOutcomesCount = outcomes.length; + const metOutcomesCount = outcomes.filter( + (o) => Number(o.status) === ComplianceStatus.Met || Number(o.status) === ComplianceStatus.Exceeded + ).length; + const unmetOutcomesCount = totalOutcomesCount - metOutcomesCount; + + // Overall compliance verdict (Invariant: 100% met required for approval) + const is100PercentCompliant = + unmetCompCount === 0 && unmetOutcomesCount === 0; + + // Financial calculations + const projectBudgets = allBudgets.filter((b) => String(b.projectId) === String(projectId)); + const approvedBudgetTotal = projectBudgets + .filter((b) => Number(b.status) === ApprovalStatus.Approved || b.status === undefined) + .reduce((sum, b) => sum + (Number(b.amount) || 0), 0); + const totalActivityBudget = activities.reduce((sum, a) => sum + (Number(a.amount) || 0), 0); + const financialVariance = approvedBudgetTotal - totalActivityBudget; + const isWithinBudget = financialVariance >= 0; + + // Milestone Progress + const verifiedMilestonesCount = milestones.filter((m) => m.status === ApprovalStatus.Approved).length; + const milestoneProgress = milestones.length > 0 ? Math.round((verifiedMilestonesCount / milestones.length) * 100) : 0; + + return ( + +
+ {/* Top Verdict Banner */} +
+
+
+ {is100PercentCompliant ? ( + + ) : ( + + )} +
+
+
+

+ {is100PercentCompliant + ? '100% Fully Compliant — Approved for Project Governance' + : 'Action Required: Project is Noncompliant'} +

+ + {is100PercentCompliant ? 'Eligible for Approval' : 'Approvals Blocked'} + +
+

+ {is100PercentCompliant + ? 'All statutory compliance requirements and beneficiary impact outcomes have met or exceeded threshold requirements.' + : `This project has ${unmetCompCount} unmet compliance item(s) and ${unmetOutcomesCount} unmet beneficiary outcome(s). Per governance policy, projects cannot be approved until all compliance items and outcomes are 100% met.`} +

+
+
+ +
+
+ Budget At Risk + + {is100PercentCompliant ? 'R 0.00' : formatZAR(approvedBudgetTotal)} + +
+
+
+ + {/* 4 Summary Metric Cards */} +
+
+ + + Compliance Requirements + +
+ + {metCompCount}/{totalCompCount} + + + ({totalCompCount > 0 ? Math.round((metCompCount / totalCompCount) * 100) : 100}%) + +
+ + {unmetCompCount === 0 ? 'All items satisfied' : `${unmetCompCount} pending fulfillment`} + +
+ +
+ + + Beneficiary Outcomes + +
+ + {metOutcomesCount}/{totalOutcomesCount} + + + ({totalOutcomesCount > 0 ? Math.round((metOutcomesCount / totalOutcomesCount) * 100) : 100}%) + +
+ + Across {projectBeneficiaries.length} beneficiary group{projectBeneficiaries.length === 1 ? '' : 's'} + +
+ +
+ + + Financial Variance + +
+ + {isWithinBudget ? `+${formatZAR(financialVariance)}` : `-${formatZAR(Math.abs(financialVariance))}`} + +
+ + Budget: {formatZAR(approvedBudgetTotal)} + +
+ +
+ + + Active Project Risks + +
+ {risks.length} + + ({risks.filter((r) => (Number(r.likelihood) || 1) * (Number(r.impact) || 1) >= 12).length} High) + +
+ ISO 31000 risk register +
+
+ + {/* Section 1: Statutory Compliance Requirements */} +
+
+
+ +

+ Statutory Compliance Requirements ({complianceItems.length}) +

+
+ + {metCompCount} of {complianceItems.length} Met / Exceeded + +
+ + {loadingCompliance ? ( +
Loading compliance register...
+ ) : complianceItems.length === 0 ? ( +
+ No specific statutory compliance requirements captured for this project yet. +
+ ) : ( +
+ + + + + + + + + + + + {complianceItems.map((ci) => { + const linkedBen = projectBeneficiaries.find( + (b) => String(b.id) === String(ci.beneficiaryId) + ); + return ( + + + + + + + + ); + })} + +
TitleRequirements SpecificationLinked BeneficiaryStatusCreated
{ci.title} + {ci.requirements || 'N/A'} + + {linkedBen ? ( + + {linkedBen.name} + + ) : ( + Project-Wide + )} + + + + {formatDate(ci.createdAt)} +
+
+ )} +
+ + {/* Section 2: Affected Beneficiaries & Mapped Outcomes */} +
+
+
+ +

+ Affected Beneficiaries & Mapped Impact Outcomes ({projectBeneficiaries.length}) +

+
+ + Total Target: {projectBeneficiaries.reduce((sum, b) => sum + (Number(b.targetCount) || 0), 0)} Reached:{' '} + {projectBeneficiaries.reduce((sum, b) => sum + (Number(b.reachedCount) || 0), 0)} + +
+ + {loadingBeneficiaries || loadingOutcomes ? ( +
Loading beneficiaries & outcomes...
+ ) : projectBeneficiaries.length === 0 ? ( +
+ No target beneficiaries mapped to this project yet. +
+ ) : ( +
+ {projectBeneficiaries.map((b) => { + const benOutcomes = outcomes.filter((o) => String(o.beneficiaryId) === String(b.id)); + return ( +
+
+
+ {b.name} + + {b.category} • {b.location} + +
+
+ + Target: {b.targetCount} + + • + + Reached: {b.reachedCount} + +
+
+ + {/* Mapped Outcomes */} +
+ + Mapped Expected Outcomes ({benOutcomes.length}) + + {benOutcomes.length === 0 ? ( +

No expected outcomes recorded for this group.

+ ) : ( +
+ {benOutcomes.map((o) => ( +
+
+ {o.name} + {o.description} +
+ +
+ ))} +
+ )} +
+
+ ); + })} +
+ )} +
+ + {/* Section 3: Risk Profile (ISO 31000) */} +
+
+
+ +

+ Active Project Risks Register ({risks.length}) +

+
+ ISO 31000 Risk Management +
+ + {loadingRisks ? ( +
Loading risk register...
+ ) : risks.length === 0 ? ( +
+ No active risks logged on this project register. +
+ ) : ( +
+ + + + + + + + + + + {risks.map((r) => { + const score = (Number(r.likelihood) || 1) * (Number(r.impact) || 1); + return ( + + + + + + + ); + })} + +
Risk TitleCategoryScoreMitigation Strategy
{r.title}{r.category || 'Operational'} + = 12 + ? 'bg-rose-500/20 text-rose-300 border border-rose-500/30' + : score >= 8 + ? 'bg-amber-500/20 text-amber-300 border border-amber-500/30' + : 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30' + }`} + > + Score: {score} + + + {r.mitigation || 'No mitigation recorded'} +
+
+ )} +
+ + {/* Footer Actions */} +
+ +
+
+
+ ); +}; diff --git a/PostFundManagement.ConsoleUI/src/components/projects/ProjectMilestonesModal.tsx b/PostFundManagement.ConsoleUI/src/components/projects/ProjectMilestonesModal.tsx index 1bd76d9..ab77cfc 100644 --- a/PostFundManagement.ConsoleUI/src/components/projects/ProjectMilestonesModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/projects/ProjectMilestonesModal.tsx @@ -214,53 +214,78 @@ export const ProjectMilestonesModal: React.FC = ({ title={`Milestones & Deliverables: ${project.name}`} subtitle={`Project #${project.id} • Statutory Work Breakdown Schedule`} helpText="Contractual milestones represent formal deliverables. The monetary value of each milestone is dynamically aggregated from its scheduled activities vs indicator actuals. The project's total value is the sum of all milestones." - maxWidth="xl" + maxWidth="4xl" >
{/* KPI Cards: Project Value Roll-Up & Performance */} -
-
- - - Total Project Value - -
- {formatZAR(totalProjectMilestonesValue)} +
+
+
+ + + Total Project Value +
- Sum of all milestones +
+
+ {formatZAR(totalProjectMilestonesValue)} +
+
+ Sum of all milestones
-
- - - Approved Budget - -
- {formatZAR(approvedBudgetTotal)} +
+
+ + + Approved Budget +
- Allocated budget ceiling +
+
+ {formatZAR(approvedBudgetTotal)} +
+
+ Allocated budget ceiling
-
- - - Partner Actuals - -
- {formatZAR(totalProjectActuals)} +
+
+ + + Partner Actuals +
- Incurred material costs +
+
+ {formatZAR(totalProjectActuals)} +
+
+ Incurred material costs
-
- - - Milestone Progress - -
- {progressPct}% +
+
+ + + Milestone Progress +
- +
+
+ {progressPct}% +
+
+ {verifiedCount} of {milestones.length} verified
diff --git a/PostFundManagement.ConsoleUI/src/components/projects/ProjectViewModal.tsx b/PostFundManagement.ConsoleUI/src/components/projects/ProjectViewModal.tsx index e269a5b..4a73a63 100644 --- a/PostFundManagement.ConsoleUI/src/components/projects/ProjectViewModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/projects/ProjectViewModal.tsx @@ -239,14 +239,26 @@ export const ProjectViewModal: React.FC = ({ {formatDate(project.startedAt)} → {formatDate(project.endedAt)} ({durationDays} Days)
- {approvedBudgetTotal > 0 && ( - <> - • - - Budget: {formatZAR(approvedBudgetTotal)} - - - )} + • + + Budget: {formatZAR(approvedBudgetTotal)} + + • + + Project Value: {formatZAR(totalActivityBudget)} + + • + = 0 + ? 'text-emerald-400 bg-emerald-500/10 border-emerald-500/30' + : 'text-rose-400 bg-rose-500/10 border-rose-500/30' + }`} + title="Approved Budget minus Total Project Value" + > + Variance: {approvedBudgetTotal - totalActivityBudget >= 0 ? '+' : ''} + {formatZAR(approvedBudgetTotal - totalActivityBudget)} +
@@ -505,6 +517,16 @@ export const ProjectViewModal: React.FC = ({ #{m.id}
+ {(() => { + const mActivities = activities.filter((a) => String(a.milestoneId) === String(m.id)); + const mValue = mActivities.reduce((sum, a) => sum + (Number(a.amount) || 0), 0); + return ( +
+ Milestone Value: + {formatZAR(mValue)} +
+ ); + })()}
); })} @@ -549,10 +571,33 @@ export const ProjectViewModal: React.FC = ({ key={act.id} className="p-2.5 rounded-xl bg-slate-900/80 border border-slate-800 text-xs space-y-1 hover:border-slate-700 transition" > -
- {act.title} - {formatZAR(act.amount)} -
+ {(() => { + const actAmount = Number(act.amount) || 0; + const actIndicators = indicators.filter((ind) => String(ind.activityId) === String(act.id)); + const actActuals = actIndicators.reduce((sum, ind) => sum + (Number(ind.actualCost) || 0), 0); + const actVariance = actAmount - actActuals; + const isPositive = actVariance >= 0; + return ( + <> +
+ {act.title} +
+ {formatZAR(actAmount)} + + {isPositive ? `+${formatZAR(actVariance)}` : `-${formatZAR(Math.abs(actVariance))}`} + +
+
+ + ); + })()}
{act.category || 'Operations'} {formatDate(act.performedAt)} diff --git a/PostFundManagement.ConsoleUI/src/components/ui/StatusBadge.tsx b/PostFundManagement.ConsoleUI/src/components/ui/StatusBadge.tsx index 9c845c8..d2d170e 100644 --- a/PostFundManagement.ConsoleUI/src/components/ui/StatusBadge.tsx +++ b/PostFundManagement.ConsoleUI/src/components/ui/StatusBadge.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import { ApprovalStatus, Priority, ActivityStatus, UserRole } from '../../types'; +import { ApprovalStatus, Priority, ActivityStatus, UserRole, ComplianceStatus } from '../../types'; import { getApprovalStatusBadge, getPriorityBadge, getActivityStatusBadge, getUserRoleBadge, + getComplianceStatusBadge, } from '../../lib/formatters'; export const ApprovalStatusBadge: React.FC<{ status: ApprovalStatus | number | null | undefined }> = ({ status }) => { @@ -17,6 +18,16 @@ export const ApprovalStatusBadge: React.FC<{ status: ApprovalStatus | number | n ); }; +export const ComplianceStatusBadge: React.FC<{ status: ComplianceStatus | number | null | undefined }> = ({ status }) => { + const badge = getComplianceStatusBadge(status ?? ComplianceStatus.NotMet); + return ( + + {badge.dot && } + {badge.label} + + ); +}; + export const PriorityBadge: React.FC<{ priority: Priority | number | null | undefined }> = ({ priority }) => { const badge = getPriorityBadge(priority ?? Priority.Medium); return ( diff --git a/PostFundManagement.ConsoleUI/src/lib/formatters.ts b/PostFundManagement.ConsoleUI/src/lib/formatters.ts index 7067efc..2803cc0 100644 --- a/PostFundManagement.ConsoleUI/src/lib/formatters.ts +++ b/PostFundManagement.ConsoleUI/src/lib/formatters.ts @@ -1,5 +1,6 @@ import { ApprovalStatus, + ComplianceStatus, ContractStatus, ActivityStatus, Priority, @@ -271,6 +272,37 @@ export const getApprovalStatusBadge = (status: ApprovalStatus | number) => { } }; +export const getComplianceStatusBadge = (status: ComplianceStatus | number) => { + const s = Number(status); + switch (s) { + case ComplianceStatus.Exceeded: + return { + label: 'Exceeded Expectation', + className: 'bg-purple-500/15 text-purple-300 border-purple-500/30 font-semibold', + dot: 'bg-purple-400', + }; + case ComplianceStatus.Met: + return { + label: 'Met', + className: 'bg-emerald-500/15 text-emerald-300 border-emerald-500/30 font-semibold', + dot: 'bg-emerald-400', + }; + case ComplianceStatus.NotMet: + default: + return { + label: 'Not Met', + className: 'bg-rose-500/15 text-rose-300 border-rose-500/30 font-semibold', + dot: 'bg-rose-400', + }; + } +}; + +export const COMPLIANCE_STATUS_OPTIONS = [ + { value: ComplianceStatus.NotMet, label: 'Not Met' }, + { value: ComplianceStatus.Met, label: 'Met' }, + { value: ComplianceStatus.Exceeded, label: 'Exceeded Expectation' }, +]; + export const getContractStatusBadge = (status: ContractStatus | number) => { const s = Number(status); switch (s) { diff --git a/PostFundManagement.ConsoleUI/src/pages/BeneficiariesPage.tsx b/PostFundManagement.ConsoleUI/src/pages/BeneficiariesPage.tsx index efece38..c29207b 100644 --- a/PostFundManagement.ConsoleUI/src/pages/BeneficiariesPage.tsx +++ b/PostFundManagement.ConsoleUI/src/pages/BeneficiariesPage.tsx @@ -42,9 +42,9 @@ import { ExportButton } from '../components/ui/ExportButton'; import { ExportColumn } from '../utils/exportUtils'; import { useTableSort, SortableTh } from '../utils/tableSort'; import { useRbac } from '../auth/rbac'; -import { ApprovalStatusBadge } from '../components/ui/StatusBadge'; -import { Beneficiary, ComplianceItem, Outcome, ApprovalStatus } from '../types'; -import { getApprovalStatusBadge, formatDate, APPROVAL_STATUS_OPTIONS } from '../lib/formatters'; +import { ApprovalStatusBadge, ComplianceStatusBadge } from '../components/ui/StatusBadge'; +import { Beneficiary, ComplianceItem, Outcome, ApprovalStatus, ComplianceStatus } from '../types'; +import { getApprovalStatusBadge, getComplianceStatusBadge, formatDate, APPROVAL_STATUS_OPTIONS, COMPLIANCE_STATUS_OPTIONS } from '../lib/formatters'; const beneficiarySchema = z.object({ name: z.string().min(2, 'Beneficiary name required'), @@ -58,11 +58,13 @@ const complianceSchema = z.object({ title: z.string().min(3, 'Compliance title required'), requirements: z.string().min(5, 'Requirements detail required'), status: z.coerce.number().optional(), + beneficiaryId: z.string().optional(), }); const outcomeSchema = z.object({ name: z.string().min(3, 'Outcome name required'), description: z.string().min(5, 'Description required'), + status: z.coerce.number().optional(), }); type BeneficiaryFormValues = z.infer; @@ -224,7 +226,13 @@ export const BeneficiariesPage: React.FC = () => { mutationFn: (vals: ComplianceFormValues) => { const targetProjId = compTargetProjectId || (selectedProjectId !== 'all' ? selectedProjectId : projects[0]?.id); if (!targetProjId) throw new Error('No project selected'); - return createComplianceItem({ projectId: targetProjId, title: vals.title, requirements: vals.requirements }); + return createComplianceItem({ + projectId: targetProjId, + title: vals.title, + requirements: vals.requirements, + status: vals.status ?? ComplianceStatus.NotMet, + beneficiaryId: vals.beneficiaryId && vals.beneficiaryId !== 'none' ? Number(vals.beneficiaryId) : null, + }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['complianceItems'] }); @@ -242,7 +250,8 @@ export const BeneficiariesPage: React.FC = () => { complianceItemId: editingComp.id, title: vals.title, requirements: vals.requirements, - status: vals.status ?? ApprovalStatus.Pending, + status: vals.status ?? ComplianceStatus.NotMet, + beneficiaryId: vals.beneficiaryId && vals.beneficiaryId !== 'none' ? Number(vals.beneficiaryId) : null, }); }, onSuccess: () => { @@ -258,7 +267,12 @@ export const BeneficiariesPage: React.FC = () => { const createOutcomeMutation = useMutation({ mutationFn: (vals: OutcomeFormValues) => { if (!selectedBenForDetail) throw new Error('No beneficiary selected'); - return createOutcome({ beneficiaryId: selectedBenForDetail.id, name: vals.name, description: vals.description }); + return createOutcome({ + beneficiaryId: selectedBenForDetail.id, + name: vals.name, + description: vals.description, + status: vals.status ?? ComplianceStatus.NotMet, + }); }, onSuccess: () => { refetchOutcomes(); @@ -360,7 +374,8 @@ export const BeneficiariesPage: React.FC = () => { compForm.reset({ title: fresh.title || '', requirements: fresh.requirements || '', - status: fresh.status ?? ApprovalStatus.Pending, + status: fresh.status ?? ComplianceStatus.NotMet, + beneficiaryId: fresh.beneficiaryId ? String(fresh.beneficiaryId) : 'none', }); setIsCompModalOpen(true); } catch { @@ -368,7 +383,8 @@ export const BeneficiariesPage: React.FC = () => { compForm.reset({ title: c.title || '', requirements: c.requirements || '', - status: c.status ?? ApprovalStatus.Pending, + status: c.status ?? ComplianceStatus.NotMet, + beneficiaryId: c.beneficiaryId ? String(c.beneficiaryId) : 'none', }); setIsCompModalOpen(true); } @@ -868,7 +884,7 @@ export const BeneficiariesPage: React.FC = () => { {c.requirements || 'None'} - + {formatDate(c.createdAt)} @@ -1018,45 +1034,36 @@ export const BeneficiariesPage: React.FC = () => { />
- {editingComp && ( -
- - {Number(editingComp.status) === ApprovalStatus.Approved ? ( -
- - Approved (Immutable - Cannot Be Changed) -
- ) : ( - - )} -
- )} +
+ + +
+ +
+ + +
+
+ + +
+
+
+
+ + {/* 5 Headline KPI Cards */} +
+ {/* Card 1: Overall Rate */} +
+
+ Compliance Rate +
+ +
+
+
+

{overallComplianceRate}%

+

100% threshold target

+
+
+ + {/* Card 2: Compliant Projects */} +
+
+ Compliant +
+ +
+
+
+

{compliantProjectsCount}

+

Eligible for approval

+
+
+ + {/* Card 3: Noncompliant Projects */} +
+
+ Noncompliant +
+ +
+
+
+

{noncompliantProjectsCount}

+

Approvals blocked

+
+
+ + {/* Card 4: Total Capital At Risk */} +
+
+ Capital At Risk +
+ +
+
+
+

{formatCompactZAR(totalBudgetAtRisk)}

+

{formatZAR(totalBudgetAtRisk)}

+
+
+ + {/* Card 5: Beneficiaries Monitored */} +
+
+ Beneficiaries +
+ +
+
+
+

{totalBeneficiaries}

+

Impact groups monitored

+
+
+
+ + {/* Filter Toolbar */} +
+
+
+ + setSearchTerm(e.target.value)} + className="pl-9 pr-3 py-1.5 bg-[#090d16] border border-slate-700/80 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:border-blue-500 w-64 transition" + /> +
+ +
+ Programme: + +
+ +
+ Compliance: + +
+
+ + + Showing {filteredRows.length} of {projectComplianceStats.length} projects + +
+ + {/* Projects Compliance Register Table */} +
+
+
+ +

+ Project Compliance Matrix & Audit Register +

+
+ + Invariant: 100% compliance required for formal project approval + +
+ +
+ + + + + + + + + + + + + + + {loadingProjects || loadingComps ? ( + + + + ) : filteredRows.length === 0 ? ( + + + + ) : ( + filteredRows.map((r) => ( + + + + + + + + + + + )) + )} + +
Project Name & IDProgrammeCompliance StatusRequirements MetOutcomes MetApproved BudgetCapital At RiskActions
+
+
+ Loading compliance matrix... +
+
+ No projects matching active compliance filters. +
+ {r.project.name} + ID #{r.project.id} • {getIndustrySectorLabel(r.project.sector)} + + {r.programme?.name || `Prog #${r.project.programmeId}`} + + {r.isCompliant ? ( + + + 100% Compliant + + ) : ( + + + Noncompliant ({r.unmetItems + r.unmetOutcomes} Unmet) + + )} + + + {r.metItems}/{r.totalItems} + + + + {r.metOutcomes}/{r.totalOutcomes} + + + {formatZAR(r.approvedBudget)} + + 0 ? 'text-rose-400' : 'text-emerald-400'}> + {formatZAR(r.budgetAtRisk)} + + + +
+
+
+ + {/* Compliance Audit Modal */} + { + setIsAuditModalOpen(false); + setSelectedProjectForAudit(null); + }} + project={selectedProjectForAudit} + programme={selectedProjectForAudit ? programmeMap.get(selectedProjectForAudit.programmeId) || null : null} + /> +
+ ); +}; diff --git a/PostFundManagement.ConsoleUI/src/pages/ExecutiveDashboard.tsx b/PostFundManagement.ConsoleUI/src/pages/ExecutiveDashboard.tsx index 7a2a121..2792cc4 100644 --- a/PostFundManagement.ConsoleUI/src/pages/ExecutiveDashboard.tsx +++ b/PostFundManagement.ConsoleUI/src/pages/ExecutiveDashboard.tsx @@ -15,6 +15,7 @@ import { ShieldAlert, RotateCcw, LayoutDashboard, + ShieldCheck, } from 'lucide-react'; import { getDashboard, @@ -26,9 +27,10 @@ import { getDisbursements, getAllRisks, getAllIndicators, + getProjectComplianceItems, } from '../services/api'; import { formatCompactZAR, formatZAR, getIndustrySectorLabel } from '../lib/formatters'; -import { ApprovalStatus } from '../types'; +import { ApprovalStatus, ComplianceStatus } from '../types'; import { RiskHeatmapMatrix } from '../components/ui/RiskHeatmapMatrix'; import { HelpTooltip } from '../components/ui/HelpTooltip'; import { useTableSort, SortableTh } from '../utils/tableSort'; @@ -39,6 +41,10 @@ interface AttentionProject { programme: string; status: string; risk: string; + complianceStatus: string; + isCompliant: boolean; + unmetCount: number; + budgetAtRisk: number; financialVariance: string; varianceType: string; maxScore: number; @@ -108,6 +114,25 @@ export const ExecutiveDashboard: React.FC = () => { queryFn: () => getAllIndicators(), }); + const { data: projectComplianceMap = new Map() } = useQuery({ + queryKey: ['projectComplianceMap', filteredProjects.map((p) => p.id)], + queryFn: async () => { + const map = new Map(); + await Promise.all( + filteredProjects.map(async (p) => { + try { + const items = await getProjectComplianceItems(p.id); + map.set(p.id, items); + } catch { + map.set(p.id, []); + } + }) + ); + return map; + }, + enabled: filteredProjects.length > 0, + }); + const refetchAll = () => { refetchDashboard(); queryClient.invalidateQueries(); @@ -217,6 +242,34 @@ export const ExecutiveDashboard: React.FC = () => { return dashboard?.atRiskCount ?? 0; }, [dashboard, filteredRisks]); + // Compliance and Capital at Risk calculations + const totalBudgetAtRisk = useMemo(() => { + const targetProjects = filteredProjects.length > 0 ? filteredProjects : projects; + return targetProjects.reduce((sum, p) => { + const pComps = projectComplianceMap.get(p.id) || []; + const hasUnmet = pComps.some((c) => Number(c.status) === ComplianceStatus.NotMet || Number(c.status) === 1); + const isPendingOrRejected = p.status === ApprovalStatus.Rejected || p.status === ApprovalStatus.Pending; + if (hasUnmet || isPendingOrRejected) { + const pBudgets = budgets.filter((b) => String(b.projectId) === String(p.id)); + const pApproved = pBudgets + .filter((b) => Number(b.status) === ApprovalStatus.Approved || b.status === undefined) + .reduce((bSum, b) => bSum + (Number(b.amount) || 0), 0); + return sum + pApproved; + } + return sum; + }, 0); + }, [filteredProjects, projects, projectComplianceMap, budgets]); + + const complianceRate = useMemo(() => { + const targetProjects = filteredProjects.length > 0 ? filteredProjects : projects; + if (targetProjects.length === 0) return 100; + const compliantCount = targetProjects.filter((p) => { + const pComps = projectComplianceMap.get(p.id) || []; + return !pComps.some((c) => Number(c.status) === ComplianceStatus.NotMet || Number(c.status) === 1); + }).length; + return Math.round((compliantCount / targetProjects.length) * 100); + }, [filteredProjects, projects, projectComplianceMap]); + const beneficiariesReached = useMemo(() => { if (dashboard?.beneficiariesReached && dashboard.beneficiariesReached > 0) { return dashboard.beneficiariesReached; @@ -325,21 +378,38 @@ export const ExecutiveDashboard: React.FC = () => { varianceType = 'negative'; } + const pComps = projectComplianceMap.get(p.id) || []; + const unmetCount = pComps.filter( + (c) => Number(c.status) === ComplianceStatus.NotMet || Number(c.status) === 1 + ).length; + const isCompliant = unmetCount === 0; + + const pBudgets = budgets.filter((b) => String(b.projectId) === String(p.id)); + const pApprovedBudget = pBudgets + .filter((b) => Number(b.status) === ApprovalStatus.Approved || b.status === undefined) + .reduce((sum, b) => sum + (Number(b.amount) || 0), 0); + + const projectBudgetAtRisk = !isCompliant || riskLevel === 'RED' || riskLevel === 'AMBER' ? pApprovedBudget : 0; + return { id: p.id, name: p.name, programme: p.programmeName || `Programme ${p.programmeId}`, status: statusLabel, risk: riskLevel, + complianceStatus: isCompliant ? 'Compliant' : `Noncompliant (${unmetCount} Unmet)`, + isCompliant, + unmetCount, + budgetAtRisk: projectBudgetAtRisk, financialVariance: variance, varianceType, maxScore, }; }) - .filter((p) => p.risk === 'RED' || p.risk === 'AMBER') - .sort((a, b) => b.maxScore - a.maxScore) - .slice(0, 5); - }, [projects, filteredProjects, risks]); + .filter((p) => p.risk === 'RED' || p.risk === 'AMBER' || !p.isCompliant) + .sort((a, b) => b.budgetAtRisk - a.budgetAtRisk || b.maxScore - a.maxScore) + .slice(0, 8); + }, [projects, filteredProjects, risks, projectComplianceMap, budgets]); const { sortedData: sortedAttentionProjects, @@ -498,8 +568,8 @@ export const ExecutiveDashboard: React.FC = () => {
- {/* 6 Headline Portfolio KPI Cards */} -
+ {/* 8 Headline Portfolio KPI Cards with Compliance & Budget At Risk */} +
{/* Card 1: Portfolio Value */}
@@ -637,6 +707,50 @@ export const ExecutiveDashboard: React.FC = () => {

Directly impacted

+ + {/* Card 7: Budget At Risk */} +
+
+
+ Budget At Risk + +
+
+ +
+
+
+

{formatCompactZAR(totalBudgetAtRisk)}

+

{formatZAR(totalBudgetAtRisk)}

+
+
+ + {/* Card 8: Compliance Health */} +
+
+
+ Compliance + +
+
+ +
+
+
+

{complianceRate}%

+

100% statutory gate

+
+
{/* Visual Models Section (Sector Breakdown & Disbursements) */} @@ -774,7 +888,7 @@ export const ExecutiveDashboard: React.FC = () => {
-

Projects Requiring Attention

+

Projects Flagged At Risk & Noncompliant

{ Status + + Compliance + Risk - - Financial Variance + + Budget At Risk Action @@ -819,7 +936,7 @@ export const ExecutiveDashboard: React.FC = () => { {sortedAttentionProjects.length === 0 ? ( - + No active projects currently flagged for executive attention. All projects are operating within target thresholds. @@ -844,6 +961,19 @@ export const ExecutiveDashboard: React.FC = () => { {p.status} + + {p.isCompliant ? ( + + + Compliant + + ) : ( + + + Noncompliant ({p.unmetCount} Unmet) + + )} + { {p.risk} - - - {p.financialVariance} + + 0 ? 'text-rose-400' : 'text-emerald-400'}> + {formatZAR(p.budgetAtRisk)} diff --git a/PostFundManagement.ConsoleUI/src/pages/ProjectsPage.tsx b/PostFundManagement.ConsoleUI/src/pages/ProjectsPage.tsx index 974db36..525fee4 100644 --- a/PostFundManagement.ConsoleUI/src/pages/ProjectsPage.tsx +++ b/PostFundManagement.ConsoleUI/src/pages/ProjectsPage.tsx @@ -29,6 +29,7 @@ import { Filter, FileText, Activity, + ShieldCheck, } from 'lucide-react'; import { getProjects, @@ -40,6 +41,7 @@ import { createIndicator, recordIndicatorActual, getBudgets, + getActivities, } from '../services/api'; import { Modal } from '../components/ui/Modal'; import { HelpTooltip } from '../components/ui/HelpTooltip'; @@ -73,6 +75,7 @@ import { getUserEmail, } from '../lib/formatters'; import { ProjectViewModal } from '../components/projects/ProjectViewModal'; +import { ProjectComplianceModal } from '../components/projects/ProjectComplianceModal'; import { ProjectFormModal } from '../components/projects/ProjectFormModal'; import { ProjectMilestonesModal } from '../components/projects/ProjectMilestonesModal'; import { ProjectActivitiesModal } from '../components/projects/ProjectActivitiesModal'; @@ -192,6 +195,8 @@ export const ProjectsPage: React.FC = () => { const [editingProject, setEditingProject] = useState(null); const [activeModalProject, setActiveModalProject] = useState(null); + const [selectedProjectForCompliance, setSelectedProjectForCompliance] = useState(null); + const [isComplianceModalOpen, setIsComplianceModalOpen] = useState(false); const [isMilestonesModalOpen, setIsMilestonesModalOpen] = useState(false); const [isActivitiesModalOpen, setIsActivitiesModalOpen] = useState(false); const [isIndicatorsModalOpen, setIsIndicatorsModalOpen] = useState(false); @@ -237,6 +242,31 @@ export const ProjectsPage: React.FC = () => { queryFn: () => getBudgets(1, 100), }); + const { data: allProjectActivities = new Map() } = useQuery({ + queryKey: ['allProjectActivities', projects.map((p) => p.id)], + queryFn: async () => { + const map = new Map(); + await Promise.all( + projects.map(async (p) => { + try { + const acts = await getActivities(p.id); + map.set(p.id, acts); + } catch { + map.set(p.id, []); + } + }) + ); + return map; + }, + enabled: projects.length > 0, + }); + + const handleOpenComplianceStatus = (p: Project, e: React.MouseEvent) => { + e.stopPropagation(); + setSelectedProjectForCompliance(p); + setIsComplianceModalOpen(true); + }; + // Programme lookup map const programmeMap = useMemo(() => { const map = new Map(); @@ -977,13 +1007,24 @@ export const ProjectsPage: React.FC = () => { />
- Actions + +
+ Project Variance + +
+ + Actions {loadingProjects ? ( - +
Loading project directory... @@ -992,7 +1033,7 @@ export const ProjectsPage: React.FC = () => { ) : sortedProjects.length === 0 ? ( - +

No matching projects found

@@ -1166,17 +1207,51 @@ export const ProjectsPage: React.FC = () => {
- {/* Row Action: View Project (Opens Large WBS & Gantt Console) */} - e.stopPropagation()}> - + {/* Project Variance Column */} + {(() => { + const approvedBudget = approvedBudgetsByProject.get(p.id) || 0; + const pActs = allProjectActivities.get(p.id) || []; + const pActsTotal = pActs.reduce((sum, a) => sum + (Number(a.amount) || 0), 0); + const pVariance = approvedBudget - pActsTotal; + const isPos = pVariance >= 0; + return ( + e.stopPropagation()}> + + {isPos ? `+${formatZAR(pVariance)}` : `-${formatZAR(Math.abs(pVariance))}`} + + + ); + })()} + + {/* Row Action: View Project & Compliance Status */} + e.stopPropagation()}> +
+ + +
); @@ -1367,7 +1442,7 @@ export const ProjectsPage: React.FC = () => { {sortedIndicatorsData.length === 0 ? ( - + {indicatorSearchTerm || indicatorProjectFilter !== 'all' || indicatorUnitFilter !== 'all' ? 'No indicators match your filter criteria.' @@ -1389,10 +1464,11 @@ export const ProjectsPage: React.FC = () => { setSelectedIndicatorForActual(ind); setActualInput(ind.actualAmount); }} - className="inline-flex items-center gap-1 text-xs font-semibold text-blue-400 hover:text-white bg-blue-500/10 hover:bg-blue-600 px-3 py-1.5 rounded-lg border border-blue-500/25 transition cursor-pointer" + className="inline-flex items-center gap-1 text-xs font-semibold text-slate-300 hover:text-white bg-slate-800/80 hover:bg-slate-700 px-3 py-1.5 rounded-lg border border-slate-700/60 transition cursor-pointer" + title="Edit actual realized value in place" > - - Record Actual + + Edit Actual @@ -1657,6 +1733,16 @@ export const ProjectsPage: React.FC = () => {
)} {/* Project View Modal (Comprehensive Console, Navigable Gantt Timeline & WBS) */} + { + setIsComplianceModalOpen(false); + setSelectedProjectForCompliance(null); + }} + project={selectedProjectForCompliance} + programme={selectedProjectForCompliance ? programmeMap.get(selectedProjectForCompliance.programmeId) || null : null} + /> + { @@ -1886,7 +1972,7 @@ export const ProjectsPage: React.FC = () => { setSelectedIndicatorForActual(null)} - title="Record Indicator Actual Value" + title="Edit Indicator Actual Value" subtitle={selectedIndicatorForActual?.name || ''} helpText="Logs a certified field or audited measurement against the target, recalculating the achievement percentage and RAG rating (BR-009)." > diff --git a/PostFundManagement.ConsoleUI/src/services/api.ts b/PostFundManagement.ConsoleUI/src/services/api.ts index e07e021..3269808 100644 --- a/PostFundManagement.ConsoleUI/src/services/api.ts +++ b/PostFundManagement.ConsoleUI/src/services/api.ts @@ -1758,6 +1758,7 @@ export const createOutcome = async (payload: CreateOutcomeRequest): Promise pk.Id); builder.Property(f => f.TenantId).IsRequired(); builder.Property(f => f.ProjectId).IsRequired(); + builder.Property(f => f.BeneficiaryId).IsRequired(false); builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.VerifiedBy).IsRequired(false); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); @@ -30,7 +33,7 @@ public sealed class ComplianceItemConfiguration : IEntityTypeConfiguration f.Title).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Requirements).IsRequired().HasMaxLength(MediumTextLength); builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1") - .HasSentinel(ApprovalStatus.Pending); + .HasSentinel(ComplianceStatus.NotMet); builder.HasOne(f => f.Project) .WithMany() @@ -38,6 +41,12 @@ public sealed class ComplianceItemConfiguration : IEntityTypeConfiguration f.Beneficiary) + .WithMany() + .HasForeignKey(fk => fk.BeneficiaryId) + .IsRequired(false) + .OnDelete(DeleteBehavior.SetNull); + builder.HasOne(f => f.Creator) .WithMany() .HasForeignKey(fk => fk.CreatedBy) @@ -50,4 +59,4 @@ public sealed class ComplianceItemConfiguration : IEntityTypeConfiguration builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength); + builder.Property(f => f.Status).IsRequired().HasConversion().HasDefaultValueSql("1") + .HasSentinel(ComplianceStatus.NotMet); builder.HasOne(f => f.Tenant) .WithMany() @@ -44,4 +46,4 @@ public sealed class OutcomeConfiguration : IEntityTypeConfiguration .IsRequired() .OnDelete(DeleteBehavior.NoAction); } -} \ No newline at end of file +} diff --git a/PostFundManagement.Domain/Enums.cs b/PostFundManagement.Domain/Enums.cs index bb75ca1..d8d354a 100644 --- a/PostFundManagement.Domain/Enums.cs +++ b/PostFundManagement.Domain/Enums.cs @@ -197,4 +197,11 @@ public enum ActivityStatus : int Active = 1, Inactive = 2, Disabled = 3, -} \ No newline at end of file +} + +public enum ComplianceStatus : int +{ + NotMet = 1, + Met = 2, + Exceeded = 3, +} diff --git a/PostFundManagement.Domain/Extensions/Mappers.cs b/PostFundManagement.Domain/Extensions/Mappers.cs index 84c13be..8650db4 100644 --- a/PostFundManagement.Domain/Extensions/Mappers.cs +++ b/PostFundManagement.Domain/Extensions/Mappers.cs @@ -137,6 +137,7 @@ public static class Mappers CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, ProjectId = entity.ProjectId, + BeneficiaryId = entity.BeneficiaryId, Requirements = entity.Requirements, Status = entity.Status, Title = entity.Title, @@ -288,7 +289,8 @@ public static class Mappers CreatedAt = entity.CreatedAt, CreatedBy = entity.CreatedBy, Description = entity.Description, - Name = entity.Name + Name = entity.Name, + Status = entity.Status }; public static Portfolio Map(this Entities.Portfolio entity) => new() diff --git a/PostFundManagement.Domain/Models/ComplianceItem.cs b/PostFundManagement.Domain/Models/ComplianceItem.cs index d8a1bb7..29bae7e 100644 --- a/PostFundManagement.Domain/Models/ComplianceItem.cs +++ b/PostFundManagement.Domain/Models/ComplianceItem.cs @@ -8,11 +8,13 @@ public class ComplianceItem public long ProjectId { get; set; } + public long? BeneficiaryId { get; set; } + public string? Title { get; set; } public string? Requirements { get; set; } - public ApprovalStatus Status { get; set; } + public ComplianceStatus Status { get; set; } = ComplianceStatus.NotMet; public Guid CreatedBy { get; set; } @@ -21,4 +23,4 @@ public class ComplianceItem public DateTime CreatedAt { get; set; } public DateTime? UpdatedAt { get; set; } -} \ No newline at end of file +} diff --git a/PostFundManagement.Domain/Models/Outcome.cs b/PostFundManagement.Domain/Models/Outcome.cs index a541337..b079bdf 100644 --- a/PostFundManagement.Domain/Models/Outcome.cs +++ b/PostFundManagement.Domain/Models/Outcome.cs @@ -15,4 +15,6 @@ public class Outcome public string? Name { get; set; } public string? Description { get; set; } -} \ No newline at end of file + + public ComplianceStatus Status { get; set; } = ComplianceStatus.NotMet; +} diff --git a/PostFundManagement.Infrastructure/Database/Migrations/20261022124137_AddedComplianceStatusAndBeneficiaryLink.Designer.cs b/PostFundManagement.Infrastructure/Database/Migrations/20261022124137_AddedComplianceStatusAndBeneficiaryLink.Designer.cs new file mode 100644 index 0000000..ff865bf --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Migrations/20261022124137_AddedComplianceStatusAndBeneficiaryLink.Designer.cs @@ -0,0 +1,2519 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PostFundManagement.Infrastructure.Database; + +#nullable disable + +namespace PostFundManagement.Infrastructure.Database.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20261022124137_AddedComplianceStatusAndBeneficiaryLink")] + partial class AddedComplianceStatusAndBeneficiaryLink + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Activity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("MilestoneId") + .HasColumnType("bigint"); + + b.Property("PerformedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("MilestoneId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Activities", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.PrimitiveCollection("Changes") + .HasColumnType("text[]"); + + b.Property("EntityId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Error") + .HasColumnType("text"); + + b.Property("PerformedBy") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("Timestamp") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("AuditLogs", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("OrganisationId"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Awards", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Beneficiary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Location") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("ReachedCount") + .HasColumnType("integer"); + + b.Property("TargetCount") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UserId"); + + b.ToTable("Beneficiaries", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Budget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.ToTable("Budgets", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ChangeRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApprovedBy") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Justification") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("RevisedBudget") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("RevisedEndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedBy"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.ToTable("ChangeRequests", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ComplianceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BeneficiaryId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Requirements") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerifiedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BeneficiaryId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.HasIndex("VerifiedBy"); + + b.ToTable("ComplianceItems", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Contract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AwardId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("EffectiveAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalSignatureId") + .HasColumnType("text"); + + b.Property("SignedDocumentUrl") + .HasColumnType("text"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("TotalValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("AwardId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.HasIndex("TenantId"); + + b.ToTable("Contracts", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Disbursement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("MilestoneId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("MilestoneId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Disbursements", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Evidence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("IndicatorId") + .HasColumnType("bigint"); + + b.Property("MilestoneId") + .HasColumnType("bigint"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("9"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.HasKey("Id"); + + b.HasIndex("ActivityId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("MilestoneId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Evidences", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Indicator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityId") + .HasColumnType("bigint"); + + b.Property("ActualAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ActualCost") + .ValueGeneratedOnAdd() + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasDefaultValueSql("0"); + + b.Property("BaselineAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("TargetAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TargetCost") + .ValueGeneratedOnAdd() + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasDefaultValueSql("0"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("UnitOfMeasure") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.ToTable("Indicators", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Instrument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("TenantId"); + + b.ToTable("Instruments", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Invite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AwardId") + .HasColumnType("bigint"); + + b.Property("ContractId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("InvitedOrganisationId") + .HasColumnType("bigint"); + + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("Recipient") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AwardId"); + + b.HasIndex("ContractId"); + + b.HasIndex("InvitedOrganisationId"); + + b.HasIndex("OrganisationId"); + + b.HasIndex("Recipient"); + + b.HasIndex("TenantId"); + + b.ToTable("Invites", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Issue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AssignedTo") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Resolution") + .HasColumnType("text"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("AssignedTo"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.ToTable("Issues", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Milestone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DueAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Milestones", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Destination") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Error") + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("Platform") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Recipient") + .HasColumnType("uuid"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Subject") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganisationId"); + + b.HasIndex("Recipient"); + + b.HasIndex("TenantId"); + + b.ToTable("Notifications", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Organisation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RegistrationNo") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("5"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("UserId"); + + b.ToTable("Organisations", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Outcome", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BeneficiaryId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("BeneficiaryId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("TenantId"); + + b.ToTable("Outcomes", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("OwnedBy") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("OrganisationId"); + + b.HasIndex("OwnedBy"); + + b.HasIndex("TenantId"); + + b.ToTable("Portfolios", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnedBy") + .HasColumnType("uuid"); + + b.Property("PortfolioId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("OwnedBy"); + + b.HasIndex("PortfolioId"); + + b.HasIndex("TenantId"); + + b.ToTable("Programmes", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("ProjectType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("10"); + + b.Property("Sector") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("11"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Risk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Impact") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("Likelihood") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("3"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.ToTable("Risks", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Rule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProgrammeId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProgrammeId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Rules", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.SiteVisit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Findings") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("InspectorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("VisitedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.ToTable("SiteVisits", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("Tenants", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.TenantOrganisation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("OrganisationId"); + + b.HasIndex("TenantId"); + + b.ToTable("TenantOrganisations", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.TenantUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + + b.Property("TenantId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("TenantId"); + + b.HasIndex("UserId"); + + b.ToTable("TenantUsers", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IdNumber") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("2"); + + b.HasKey("Id"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Activity", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Milestone", "Milestone") + .WithMany() + .HasForeignKey("MilestoneId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Milestone"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Awards") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany("Awards") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Organisation"); + + b.Navigation("Programme"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Beneficiary", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Budget", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ChangeRequest", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApprovedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.ComplianceItem", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Beneficiary", "Beneficiary") + .WithMany() + .HasForeignKey("BeneficiaryId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Verifier") + .WithMany() + .HasForeignKey("VerifiedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Beneficiary"); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + + b.Navigation("Verifier"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Contract", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Award", "Award") + .WithOne("Contract") + .HasForeignKey("PostFundManagement.Domain.Entities.Contract", "AwardId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Award"); + + b.Navigation("Creator"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Disbursement", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Milestone", "Milestone") + .WithMany() + .HasForeignKey("MilestoneId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Milestone"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Evidence", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Activity", "Activity") + .WithMany("Evidences") + .HasForeignKey("ActivityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Indicator", "Indicator") + .WithMany() + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Milestone", "Milestone") + .WithMany() + .HasForeignKey("MilestoneId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Activity"); + + b.Navigation("Creator"); + + b.Navigation("Indicator"); + + b.Navigation("Milestone"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Indicator", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Activity", "Activity") + .WithMany("Indicators") + .HasForeignKey("ActivityId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Activity"); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Instrument", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Instruments") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Programme"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Invite", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Award", "Award") + .WithMany() + .HasForeignKey("AwardId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Contract", "Contract") + .WithMany() + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "InvitedOrganisation") + .WithMany() + .HasForeignKey("InvitedOrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "RecipientUser") + .WithMany() + .HasForeignKey("Recipient") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Award"); + + b.Navigation("Contract"); + + b.Navigation("InvitedOrganisation"); + + b.Navigation("Organisation"); + + b.Navigation("RecipientUser"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Issue", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Assignee") + .WithMany() + .HasForeignKey("AssignedTo") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Assignee"); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Milestone", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Notification", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "RecipientUser") + .WithMany() + .HasForeignKey("Recipient") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organisation"); + + b.Navigation("RecipientUser"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Organisation", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Outcome", b => + { + b.HasOne("PostFundManagement.Domain.Entities.Beneficiary", "Beneficiary") + .WithMany() + .HasForeignKey("BeneficiaryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Beneficiary"); + + b.Navigation("Creator"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Organisation"); + + b.Navigation("Owner"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Portfolio", "Portfolio") + .WithMany("Programmes") + .HasForeignKey("PortfolioId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Owner"); + + b.Navigation("Portfolio"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Projects") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Programme"); + + b.Navigation("Tenant"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Risk", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Rule", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Programme", "Programme") + .WithMany("Rules") + .HasForeignKey("ProgrammeId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Creator"); + + b.Navigation("Programme"); + + b.Navigation("Tenant"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.SiteVisit", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Tenant", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "Updator") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Creator"); + + b.Navigation("Updator"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.TenantOrganisation", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Organisation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.TenantUser", b => + { + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PostFundManagement.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Activity", b => + { + b.Navigation("Evidences"); + + b.Navigation("Indicators"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b => + { + b.Navigation("Contract"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Portfolio", b => + { + b.Navigation("Programmes"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Programme", b => + { + b.Navigation("Awards"); + + b.Navigation("Instruments"); + + b.Navigation("Projects"); + + b.Navigation("Rules"); + }); + + modelBuilder.Entity("PostFundManagement.Domain.Entities.Project", b => + { + b.Navigation("Awards"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PostFundManagement.Infrastructure/Database/Migrations/20261022124137_AddedComplianceStatusAndBeneficiaryLink.cs b/PostFundManagement.Infrastructure/Database/Migrations/20261022124137_AddedComplianceStatusAndBeneficiaryLink.cs new file mode 100644 index 0000000..1bd7f68 --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Migrations/20261022124137_AddedComplianceStatusAndBeneficiaryLink.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PostFundManagement.Infrastructure.Database.Migrations +{ + /// + public partial class AddedComplianceStatusAndBeneficiaryLink : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Status", + table: "Outcomes", + type: "integer", + nullable: false, + defaultValueSql: "1"); + + migrationBuilder.AddColumn( + name: "BeneficiaryId", + table: "ComplianceItems", + type: "bigint", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_ComplianceItems_BeneficiaryId", + table: "ComplianceItems", + column: "BeneficiaryId"); + + migrationBuilder.AddForeignKey( + name: "FK_ComplianceItems_Beneficiaries_BeneficiaryId", + table: "ComplianceItems", + column: "BeneficiaryId", + principalTable: "Beneficiaries", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ComplianceItems_Beneficiaries_BeneficiaryId", + table: "ComplianceItems"); + + migrationBuilder.DropIndex( + name: "IX_ComplianceItems_BeneficiaryId", + table: "ComplianceItems"); + + migrationBuilder.DropColumn( + name: "Status", + table: "Outcomes"); + + migrationBuilder.DropColumn( + name: "BeneficiaryId", + table: "ComplianceItems"); + } + } +} diff --git a/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs b/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs index 26d36d2..f45f224 100644 --- a/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs @@ -393,6 +393,9 @@ namespace PostFundManagement.Infrastructure.Database.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("BeneficiaryId") + .HasColumnType("bigint"); + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -430,6 +433,8 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.HasKey("Id"); + b.HasIndex("BeneficiaryId"); + b.HasIndex("CreatedBy"); b.HasIndex("ProjectId"); @@ -1078,6 +1083,11 @@ namespace PostFundManagement.Infrastructure.Database.Migrations .HasMaxLength(256) .HasColumnType("character varying(256)"); + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValueSql("1"); + b.Property("TenantId") .HasColumnType("bigint"); @@ -1786,6 +1796,11 @@ namespace PostFundManagement.Infrastructure.Database.Migrations modelBuilder.Entity("PostFundManagement.Domain.Entities.ComplianceItem", b => { + b.HasOne("PostFundManagement.Domain.Entities.Beneficiary", "Beneficiary") + .WithMany() + .HasForeignKey("BeneficiaryId") + .OnDelete(DeleteBehavior.SetNull); + b.HasOne("PostFundManagement.Domain.Entities.User", "Creator") .WithMany() .HasForeignKey("CreatedBy") @@ -1809,6 +1824,8 @@ namespace PostFundManagement.Infrastructure.Database.Migrations .HasForeignKey("VerifiedBy") .OnDelete(DeleteBehavior.NoAction); + b.Navigation("Beneficiary"); + b.Navigation("Creator"); b.Navigation("Project");