diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateActivityEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateActivityEndpoint.cs new file mode 100644 index 0000000..1a083fd --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateActivityEndpoint.cs @@ -0,0 +1,36 @@ +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public class UpdateActivityEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPut("api/projects/activities", async (UpdateActivityRequest request, + HttpContext context, IProjectService service, CancellationToken cancellationToken = default) => + await service.UpdateActivityAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .RequireAuthorization(Constants.MainScope) + .WithDescription("Update an existing activity under project (M04)") + .WithName(typeof(UpdateActivityEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} + diff --git a/PostFundManagement.Application/Abstractions/Services/IEvidenceService.cs b/PostFundManagement.Application/Abstractions/Services/IEvidenceService.cs index 4c6cf7b..92a7372 100644 --- a/PostFundManagement.Application/Abstractions/Services/IEvidenceService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IEvidenceService.cs @@ -1,4 +1,4 @@ -using PostFundManagement.Application.Services.Evidence; +using PostFundManagement.Application.Services.Evidence; using PostFundManagement.Domain; using PostFundManagement.Domain.Attributes; using PostFundManagement.Domain.Models; @@ -15,6 +15,10 @@ public interface IEvidenceService [Audit] Task RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default); + [Log] + [Audit] + Task RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, decimal? actualCost, Guid userId, CancellationToken cancellationToken = default); + [Log] Task> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default); diff --git a/PostFundManagement.Application/Abstractions/Services/IProjectService.cs b/PostFundManagement.Application/Abstractions/Services/IProjectService.cs index 53249bb..dd53446 100644 --- a/PostFundManagement.Application/Abstractions/Services/IProjectService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IProjectService.cs @@ -1,4 +1,4 @@ -using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Application.Services.Projects; using PostFundManagement.Domain; using PostFundManagement.Domain.Attributes; using PostFundManagement.Domain.Models; @@ -28,6 +28,10 @@ public interface IProjectService [Audit] Task CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] + [Audit] + Task UpdateActivityAsync(UpdateActivityRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] Task> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default); diff --git a/PostFundManagement.Application/Services/Evidence/EvidenceCacheService.cs b/PostFundManagement.Application/Services/Evidence/EvidenceCacheService.cs index 3be2893..1269095 100644 --- a/PostFundManagement.Application/Services/Evidence/EvidenceCacheService.cs +++ b/PostFundManagement.Application/Services/Evidence/EvidenceCacheService.cs @@ -1,4 +1,4 @@ -using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Services.Shared; using PostFundManagement.Domain; @@ -189,6 +189,9 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa public async Task RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) => await service.RecordIndicatorActualAmountAsync(indicatorIdHash, amount, userId, cancellationToken); + public async Task RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, decimal? actualCost, Guid userId, CancellationToken cancellationToken = default) => + await service.RecordIndicatorActualAmountAsync(indicatorIdHash, amount, actualCost, userId, cancellationToken); + public async Task UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.UpdateEvidenceAsync(request, userId, cancellationToken); } diff --git a/PostFundManagement.Application/Services/Evidence/EvidenceService.cs b/PostFundManagement.Application/Services/Evidence/EvidenceService.cs index 664f0ce..11331ef 100644 --- a/PostFundManagement.Application/Services/Evidence/EvidenceService.cs +++ b/PostFundManagement.Application/Services/Evidence/EvidenceService.cs @@ -1,4 +1,4 @@ -using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Services.Shared; using PostFundManagement.Domain; using PostFundManagement.Domain.Abstractions; @@ -50,6 +50,9 @@ public sealed class EvidenceService(IDbContextFactory cont })); public async Task RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) => + await RecordIndicatorActualAmountAsync(indicatorIdHash, amount, null, userId, cancellationToken); + + public async Task RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, decimal? actualCost, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { var decodeResult = hashService.DecodeLongIdHash(indicatorIdHash); @@ -63,6 +66,7 @@ public sealed class EvidenceService(IDbContextFactory cont var changedTotal = await context.Indicators.Where(i => i.Id == decodeResult.Value) .ExecuteUpdateAsync(f => f .SetProperty(f => f.ActualAmount, amount) + .SetProperty(f => f.ActualCost, f => actualCost.HasValue ? actualCost.Value : f.ActualCost) .SetProperty(f => f.CreatedBy, userId), cancellationToken); return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update indicator.")); @@ -130,14 +134,21 @@ public sealed class EvidenceService(IDbContextFactory cont if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) return Result.Fail(NotFoundError.New); + if (request.ActivityId.HasValue && !await context.Activities.AnyAsync(a => a.Id == request.ActivityId.Value && a.ProjectId == request.ProjectId, cancellationToken)) + return Result.Fail(NotFoundError.Create("Activity not found")); + context.Indicators.Add(new Domain.Entities.Indicator { ProjectId = request.ProjectId, + ActivityId = request.ActivityId, + Name = request.Name, UnitOfMeasure = request.UnitOfMeasure, BaselineAmount = request.BaselineAmount, TargetAmount = request.TargetAmount, ActualAmount = request.BaselineAmount, + TargetCost = request.TargetCost, + ActualCost = 0, CreatedBy = userId, CreatedAt = DateTime.UtcNow }); diff --git a/PostFundManagement.Application/Services/Evidence/Records.cs b/PostFundManagement.Application/Services/Evidence/Records.cs index c03a29f..8da7c1a 100644 --- a/PostFundManagement.Application/Services/Evidence/Records.cs +++ b/PostFundManagement.Application/Services/Evidence/Records.cs @@ -1,4 +1,4 @@ -using PostFundManagement.Domain; +using PostFundManagement.Domain; namespace PostFundManagement.Application.Services.Evidence; @@ -7,7 +7,7 @@ public sealed record UpdateEvidenceRequest([Required] long EvidenceId, [Required] EvidenceType EvidenceType, [Required] ApprovalStatus Status); -public sealed record RecordActualRequest([Required] decimal ActualAmount); +public sealed record RecordActualRequest([Required] decimal ActualAmount, decimal? ActualCost = null); public sealed record CreateMilestoneRequest([Required] long ProjectId, [Required] DateTime DueAt, @@ -18,4 +18,6 @@ public sealed record CreateIndicatorRequest([Required] long ProjectId, [Required][MaxLength(LabelLength)] string Name, [Required] UnitOfMeasure UnitOfMeasure, [Required] decimal BaselineAmount, - [Required] decimal TargetAmount); + [Required] decimal TargetAmount, + long? ActivityId = null, + decimal TargetCost = 0); diff --git a/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs b/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs index 9f7adbf..6937801 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs @@ -1,4 +1,4 @@ -using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Services.Shared; using PostFundManagement.Domain; @@ -12,6 +12,9 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach public async Task CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.CreateActivityAsync(request, userId, cancellationToken); + public async Task UpdateActivityAsync(UpdateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) => + await service.UpdateActivityAsync(request, userId, cancellationToken); + public async Task CreateBeneficiaryAsync(CreateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.CreateBeneficiaryAsync(request, userId, cancellationToken); diff --git a/PostFundManagement.Application/Services/Projects/ProjectService.cs b/PostFundManagement.Application/Services/Projects/ProjectService.cs index 47b8ac0..eecc9ec 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectService.cs @@ -1,4 +1,4 @@ -using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Services.Shared; using PostFundManagement.Domain; using PostFundManagement.Domain.Abstractions; @@ -123,6 +123,29 @@ public sealed class ProjectService(IDbContextFactory conte if (!await context.Milestones.AnyAsync(m => m.Id == request.MilestoneId.Value, cancellationToken)) return Result.Fail(NotFoundError.New); + var approvedBudget = await context.Budgets + .Where(b => b.ProjectId == request.ProjectId && (b.Status == ApprovalStatus.Approved || b.Status == ApprovalStatus.Pending)) + .SumAsync(b => b.Amount, cancellationToken); + + if (approvedBudget == 0) + { + approvedBudget = await context.Awards + .Where(a => a.ProjectId == request.ProjectId) + .SumAsync(a => a.Amount, cancellationToken); + } + + if (approvedBudget > 0) + { + var existingTotal = await context.Activities + .Where(a => a.ProjectId == request.ProjectId) + .SumAsync(a => a.Amount, cancellationToken); + + if (existingTotal + request.Amount > approvedBudget) + { + return Result.Fail(CreateError.Create($"Activity budget R {request.Amount:N2} exceeds the available project budget. Approved budget: R {approvedBudget:N2}, already allocated: R {existingTotal:N2}, remaining: R {(approvedBudget - existingTotal):N2}.")); + } + } + context.Activities.Add(new Domain.Entities.Activity { ProjectId = request.ProjectId, @@ -141,6 +164,54 @@ public sealed class ProjectService(IDbContextFactory conte CreateError.Create("No record was created.")); })); + public async Task UpdateActivityAsync(UpdateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var activity = await context.Activities.FirstOrDefaultAsync(a => a.Id == request.ActivityId, cancellationToken); + if (activity is null) + return Result.Fail(NotFoundError.Create("Activity not found")); + + if (request.MilestoneId.HasValue && !await context.Milestones.AnyAsync(m => m.Id == request.MilestoneId.Value, cancellationToken)) + return Result.Fail(NotFoundError.Create("Milestone not found")); + + var approvedBudget = await context.Budgets + .Where(b => b.ProjectId == request.ProjectId && (b.Status == ApprovalStatus.Approved || b.Status == ApprovalStatus.Pending)) + .SumAsync(b => b.Amount, cancellationToken); + + if (approvedBudget == 0) + { + approvedBudget = await context.Awards + .Where(a => a.ProjectId == request.ProjectId) + .SumAsync(a => a.Amount, cancellationToken); + } + + if (approvedBudget > 0) + { + var otherTotal = await context.Activities + .Where(a => a.ProjectId == request.ProjectId && a.Id != request.ActivityId) + .SumAsync(a => a.Amount, cancellationToken); + + if (otherTotal + request.Amount > approvedBudget) + { + return Result.Fail(UpdateError.Create($"Updated activity budget R {request.Amount:N2} exceeds the available project budget. Approved budget: R {approvedBudget:N2}, other allocated: R {otherTotal:N2}, remaining: R {(approvedBudget - otherTotal):N2}.")); + } + } + + activity.Title = request.Title.Humanize(LetterCasing.Title).Trim(); + activity.Description = request.Description; + activity.Category = request.Category.Humanize(LetterCasing.Title).Trim(); + activity.Amount = request.Amount; + activity.MilestoneId = request.MilestoneId; + activity.PerformedAt = request.PerformedAt; + activity.UpdatedBy = userId; + activity.UpdatedAt = DateTime.UtcNow; + + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, + UpdateError.Create("No changes detected, update cancelled.")); + })); + public async Task> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { diff --git a/PostFundManagement.Application/Services/Projects/Records.cs b/PostFundManagement.Application/Services/Projects/Records.cs index 87cf9ed..4229c05 100644 --- a/PostFundManagement.Application/Services/Projects/Records.cs +++ b/PostFundManagement.Application/Services/Projects/Records.cs @@ -1,4 +1,4 @@ -using PostFundManagement.Domain; +using PostFundManagement.Domain; namespace PostFundManagement.Application.Services.Projects; @@ -106,3 +106,12 @@ public sealed record CreateActivityRequest([Required] long ProjectId, [Required][MaxLength(LabelLength)] string Title, [Required][MaxLength(TextLength)] string Description, DateTime? PerformedAt); + +public sealed record UpdateActivityRequest([Required] long ActivityId, + [Required] long ProjectId, + long? MilestoneId, + [Required][MaxLength(LabelLength)] string Category, + [Required] decimal Amount, + [Required][MaxLength(LabelLength)] string Title, + [Required][MaxLength(TextLength)] string Description, + DateTime? PerformedAt); diff --git a/PostFundManagement.ConsoleUI/src/components/projects/ProjectActivitiesModal.tsx b/PostFundManagement.ConsoleUI/src/components/projects/ProjectActivitiesModal.tsx index 8a3d406..889cad6 100644 --- a/PostFundManagement.ConsoleUI/src/components/projects/ProjectActivitiesModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/projects/ProjectActivitiesModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -14,6 +14,10 @@ import { Flag, X, RotateCcw, + Target, + AlertTriangle, + ChevronRight, + ShieldAlert, } from 'lucide-react'; import { Modal } from '../ui/Modal'; import { HelpTooltip } from '../ui/HelpTooltip'; @@ -23,8 +27,10 @@ import { createActivity, updateActivity, getMilestones, + getProjectIndicators, + getBudgets, } from '../../services/api'; -import { Project, Activity, Milestone } from '../../types'; +import { Project, Activity, Milestone, Indicator, Budget } from '../../types'; import { formatDate, formatZAR } from '../../lib/formatters'; const activitySchema = z.object({ @@ -42,12 +48,14 @@ interface ProjectActivitiesModalProps { isOpen: boolean; onClose: () => void; project: Project | null; + onOpenIndicatorsForActivity?: (activityId: string | number) => void; } export const ProjectActivitiesModal: React.FC = ({ isOpen, onClose, project, + onOpenIndicatorsForActivity, }) => { const queryClient = useQueryClient(); const { success, error: toastError } = useToast(); @@ -73,6 +81,45 @@ export const ProjectActivitiesModal: React.FC = ({ enabled: isOpen && !!projectId, }); + const { data: indicators = [] } = useQuery({ + queryKey: ['indicators', projectId], + queryFn: () => (projectId ? getProjectIndicators(projectId) : Promise.resolve([])), + enabled: isOpen && !!projectId, + }); + + const { data: allBudgets = [] } = useQuery({ + queryKey: ['budgets'], + queryFn: () => getBudgets(1, 100), + enabled: isOpen, + }); + + // Approved project budget calculations + const approvedBudgetTotal = useMemo(() => { + const projectBudgets = allBudgets.filter((b) => String(b.projectId) === String(projectId)); + return projectBudgets + .filter((b) => b.status === undefined || b.status === null || Number(b.status) === 3) + .reduce((sum, b) => sum + (Number(b.amount) || 0), 0); + }, [allBudgets, projectId]); + + const totalAllocated = useMemo( + () => activities.reduce((acc, a) => acc + (Number(a.amount) || 0), 0), + [activities] + ); + + const maxAllowedForForm = useMemo(() => { + if (approvedBudgetTotal <= 0) return Infinity; + const baseRemaining = approvedBudgetTotal - totalAllocated; + if (editingActivity) { + return baseRemaining + (Number(editingActivity.amount) || 0); + } + return Math.max(0, baseRemaining); + }, [approvedBudgetTotal, totalAllocated, editingActivity]); + + const remainingBudget = useMemo(() => { + if (approvedBudgetTotal <= 0) return 0; + return Math.max(0, approvedBudgetTotal - totalAllocated); + }, [approvedBudgetTotal, totalAllocated]); + const form = useForm({ resolver: zodResolver(activitySchema), defaultValues: { @@ -114,6 +161,14 @@ export const ProjectActivitiesModal: React.FC = ({ const mutation = useMutation({ mutationFn: async (data: ActivityFormValues): Promise => { if (!projectId) return; + + const requestedAmount = Number(data.amount); + if (approvedBudgetTotal > 0 && requestedAmount > maxAllowedForForm) { + throw new Error( + `Activity amount (${formatZAR(requestedAmount)}) exceeds the available remaining project budget (${formatZAR(maxAllowedForForm)}).` + ); + } + const parsedMilestoneId = data.milestoneId && String(data.milestoneId).trim() !== '' ? Number(data.milestoneId) @@ -127,7 +182,7 @@ export const ProjectActivitiesModal: React.FC = ({ title: data.title, description: data.description, category: data.category, - amount: Number(data.amount), + amount: requestedAmount, milestoneId: parsedMilestoneId, performedAt: performedAtIso, }); @@ -137,7 +192,7 @@ export const ProjectActivitiesModal: React.FC = ({ title: data.title, description: data.description, category: data.category, - amount: Number(data.amount), + amount: requestedAmount, milestoneId: parsedMilestoneId, performedAt: performedAtIso, }); @@ -145,6 +200,7 @@ export const ProjectActivitiesModal: React.FC = ({ }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['activities', projectId] }); + queryClient.invalidateQueries({ queryKey: ['milestones', projectId] }); queryClient.invalidateQueries({ queryKey: ['projects'] }); success( editingActivity ? 'Activity Updated' : 'Activity Created', @@ -163,8 +219,6 @@ export const ProjectActivitiesModal: React.FC = ({ if (!project) return null; - const totalBudget = activities.reduce((acc, a) => acc + (Number(a.amount) || 0), 0); - return ( = ({ }} title={`Work Breakdown Activities: ${project.name}`} subtitle={`Project #${project.id} • Operational Work Breakdown Structure (WBS)`} - helpText="Granular operational tasks executed under this project. Activities can optionally be linked to contractual milestones and are tracked with specific budget allocations." + helpText="Under each milestone, operational activities are scheduled with budgeted amounts. The sum of all activity budgets across the project cannot exceed the approved project budget." maxWidth="xl" >
- {/* KPI Summary Card */} -
-
+ {/* KPI Summary Cards */} +
+
- Total Allocated Work Budget + Total Allocated Activities -
- {formatZAR(totalBudget)} - ({activities.length} recorded tasks) +
+ {formatZAR(totalAllocated)} + ({activities.length} tasks)
+
+ + + Approved Project Budget + +
+ {formatZAR(approvedBudgetTotal)} +
+
+ +
+ + + Remaining Budget Ceiling + +
+ 0 ? 'text-rose-400' : 'text-blue-400'}`}> + {formatZAR(remainingBudget)} + +
+
+
+ + {/* Action Header */} +
+ + Scheduled Tasks: {activities.length} + +
{!isFormOpen && (
+ {approvedBudgetTotal > 0 && ( +
+ Maximum activity budget allowance: + {formatZAR(maxAllowedForForm)} +
+ )} +
mutation.mutate(data))} className="space-y-3">
@@ -262,6 +352,7 @@ export const ProjectActivitiesModal: React.FC = ({ @@ -350,7 +441,9 @@ export const ProjectActivitiesModal: React.FC = ({

No work activities recorded yet

-

Deconstruct the project into operational tasks with allocated budgets.

+

+ Deconstruct the project milestones into operational tasks with allocated budgets. +

) : ( -
+
{activities.map((a) => { const linkedMilestone = a.milestoneId ? milestones.find((m) => String(m.id) === String(a.milestoneId)) : null; + const actIndicators = indicators.filter((i) => String(i.activityId) === String(a.id)); + const actActualCost = actIndicators.reduce((sum, i) => sum + (Number(i.actualCost) || 0), 0); + const isOverrun = actActualCost > Number(a.amount) && a.amount > 0; + return (
-
-
- {a.title} - - {a.category} - - {a.milestoneId && ( - - - Milestone #{a.milestoneId} - {linkedMilestone ? ` (${linkedMilestone.name})` : ''} +
+
+
+ {a.title} + + {a.category} - )} + {a.milestoneId && ( + + + Milestone #{a.milestoneId} + {linkedMilestone ? ` (${linkedMilestone.name})` : ''} + + )} +
+

{a.description}

+
+ Date: {formatDate(a.performedAt)} + • + ID #{a.id} +
-

{a.description}

-
- Date: {formatDate(a.performedAt)} - • - ID #{a.id} + +
+
+ + {formatZAR(a.amount)} + + {actActualCost > 0 && ( + + Actual: {formatZAR(actActualCost)} + + )} +
+ + + + Evidence + + +
-
- {formatZAR(a.amount)} + {/* Linked Delivery Indicators Section */} +
+
+ + + Delivery Specifications: + + {actIndicators.length === 0 ? ( + No indicators defined for this activity yet. + ) : ( +
+ {actIndicators.map((ind) => ( + + {ind.name}: {ind.actualAmount}/{ind.targetAmount} + + ))} +
+ )} +
- - - Evidence - +
+ {isOverrun && ( + + ⚠️ Overrun: +{formatZAR(actActualCost - Number(a.amount))} + + )} - + {onOpenIndicatorsForActivity && ( + + )} +
); diff --git a/PostFundManagement.ConsoleUI/src/components/projects/ProjectExpandedRow.tsx b/PostFundManagement.ConsoleUI/src/components/projects/ProjectExpandedRow.tsx index ef40fb6..e559e20 100644 --- a/PostFundManagement.ConsoleUI/src/components/projects/ProjectExpandedRow.tsx +++ b/PostFundManagement.ConsoleUI/src/components/projects/ProjectExpandedRow.tsx @@ -173,9 +173,14 @@ export const ProjectExpandedRow: React.FC = ({ {sBadge.label}
-
- - Due: {formatDate(m.dueAt)} +
+ + + Due: {formatDate(m.dueAt)} + + + {formatZAR(activities.filter(a => String(a.milestoneId) === String(m.id)).reduce((s, a) => s + (Number(a.amount) || 0), 0))} +
); diff --git a/PostFundManagement.ConsoleUI/src/components/projects/ProjectIndicatorsModal.tsx b/PostFundManagement.ConsoleUI/src/components/projects/ProjectIndicatorsModal.tsx index 90fe010..80a2e7a 100644 --- a/PostFundManagement.ConsoleUI/src/components/projects/ProjectIndicatorsModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/projects/ProjectIndicatorsModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -9,8 +9,12 @@ import { TrendingUp, X, RotateCcw, - Check, - Edit3, + AlertTriangle, + Briefcase, + Filter, + Info, + ShieldCheck, + Coins, } from 'lucide-react'; import { Modal } from '../ui/Modal'; import { HelpTooltip } from '../ui/HelpTooltip'; @@ -18,16 +22,18 @@ import { useToast } from '../ui/Toast'; import { getProjectIndicators, createIndicator, - recordIndicatorActual, + getActivities, } from '../../services/api'; -import { Project, Indicator, UnitOfMeasure } from '../../types'; -import { getUnitOfMeasureLabel, UNIT_OF_MEASURE_OPTIONS } from '../../lib/formatters'; +import { Project, Indicator, UnitOfMeasure, Activity } from '../../types'; +import { getUnitOfMeasureLabel, UNIT_OF_MEASURE_OPTIONS, formatZAR } from '../../lib/formatters'; const indicatorSchema = z.object({ - name: z.string().min(5, 'Indicator definition required (min 5 chars)'), + name: z.string().min(4, 'Indicator delivery specification required (min 4 chars)'), + activityId: z.string().optional().nullable(), unitOfMeasure: z.coerce.number().min(1), baselineAmount: z.coerce.number().min(0, 'Baseline must be >= 0'), targetAmount: z.coerce.number().min(1, 'Target must be > 0'), + targetCost: z.coerce.number().min(0, 'Target cost must be >= 0').optional(), }); type IndicatorFormValues = z.infer; @@ -36,25 +42,26 @@ interface ProjectIndicatorsModalProps { isOpen: boolean; onClose: () => void; project: Project | null; + initialActivityId?: string | number | null; } export const ProjectIndicatorsModal: React.FC = ({ isOpen, onClose, project, + initialActivityId = null, }) => { const queryClient = useQueryClient(); const { success, error: toastError } = useToast(); const [isFormOpen, setIsFormOpen] = useState(false); - const [recordingActualForId, setRecordingActualForId] = useState(null); - const [actualInput, setActualInput] = useState(0); + const [selectedActivityFilter, setSelectedActivityFilter] = useState('all'); const projectId = project?.id; const { data: indicators = [], - isLoading, + isLoading: isLoadingIndicators, refetch, } = useQuery({ queryKey: ['indicators', projectId], @@ -62,31 +69,49 @@ export const ProjectIndicatorsModal: React.FC = ({ enabled: isOpen && !!projectId, }); + const { + data: activities = [], + isLoading: isLoadingActivities, + } = useQuery({ + queryKey: ['activities', projectId], + queryFn: () => (projectId ? getActivities(projectId) : Promise.resolve([])), + enabled: isOpen && !!projectId, + }); + const form = useForm({ resolver: zodResolver(indicatorSchema), defaultValues: { name: '', + activityId: initialActivityId ? String(initialActivityId) : '', unitOfMeasure: UnitOfMeasure.Count, baselineAmount: 0, targetAmount: 100, + targetCost: 0, }, }); const createMutation = useMutation({ mutationFn: async (data: IndicatorFormValues) => { if (!projectId) return; + const parsedActivityId = data.activityId && String(data.activityId).trim() !== '' + ? data.activityId + : null; + return createIndicator({ projectId: projectId, + activityId: parsedActivityId, name: data.name, unitOfMeasure: Number(data.unitOfMeasure), baselineAmount: Number(data.baselineAmount), targetAmount: Number(data.targetAmount), + targetCost: Number(data.targetCost) || 0, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['indicators', projectId] }); queryClient.invalidateQueries({ queryKey: ['indicators'] }); - success('Indicator Created', 'M&E Key Performance Indicator added to project'); + queryClient.invalidateQueries({ queryKey: ['activities', projectId] }); + success('Delivery Indicator Created', 'Indicator specification linked to work activity'); setIsFormOpen(false); form.reset(); }, @@ -95,20 +120,22 @@ export const ProjectIndicatorsModal: React.FC = ({ }, }); - const actualMutation = useMutation({ - mutationFn: async ({ indicatorId, actualAmount }: { indicatorId: string | number; actualAmount: number }) => { - return recordIndicatorActual(indicatorId, actualAmount); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['indicators', projectId] }); - queryClient.invalidateQueries({ queryKey: ['indicators'] }); - success('Actual Value Logged', 'M&E progress measurement updated successfully'); - setRecordingActualForId(null); - }, - onError: (err: unknown) => { - toastError('Failed to record actual value', err); - }, - }); + // Filter indicators by activity + const filteredIndicators = useMemo(() => { + if (selectedActivityFilter === 'all') return indicators; + if (selectedActivityFilter === 'unlinked') return indicators.filter((i) => !i.activityId); + return indicators.filter((i) => String(i.activityId) === String(selectedActivityFilter)); + }, [indicators, selectedActivityFilter]); + + // Aggregate totals + const totalTargetCost = useMemo( + () => indicators.reduce((sum, i) => sum + (Number(i.targetCost) || 0), 0), + [indicators] + ); + const totalActualCost = useMemo( + () => indicators.reduce((sum, i) => sum + (Number(i.actualCost) || 0), 0), + [indicators] + ); if (!project) return null; @@ -117,34 +144,86 @@ export const ProjectIndicatorsModal: React.FC = ({ isOpen={isOpen} onClose={() => { setIsFormOpen(false); - setRecordingActualForId(null); onClose(); }} - title={`M&E Performance Indicators: ${project.name}`} - subtitle={`Project #${project.id} • Results & Impact Framework`} - helpText="Quantitative indicators monitor baseline vs. target deliverables (e.g. jobs created, patents filed, rural SMMEs trained, renewable MW generated)." + title={`M&E Delivery Indicators: ${project.name}`} + subtitle={`Project #${project.id} • Activity Delivery Specifications & Evidence Requirements`} + helpText="Each indicator specifies an expected delivery output linked to an operational activity. Quantities and material costs are captured as actuals by the partner in the Partner Portal." maxWidth="xl" >
- {/* KPI Summary Card */} -
-
+ {/* KPI Summary Cards */} +
+
- Monitoring & Evaluation Targets + Delivery Indicators -
- {indicators.length} - Tracked performance metrics +
+ {indicators.length} + specifications
+
+ + + Target Delivery Cost + +
+ {formatZAR(totalTargetCost)} +
+
+ +
+ + + Partner Actuals Incurred + +
+ totalTargetCost && totalTargetCost > 0 ? 'text-amber-400' : 'text-emerald-400'}`}> + {formatZAR(totalActualCost)} + +
+
+
+ + {/* Action Header & Activity Filter Bar */} +
+
+ + Filter by Activity: + +
+
{!isFormOpen && (
- {/* Create Indicator Form */} + {/* Indicator Create Form */} {isFormOpen && (
- Define New Key Performance Indicator (KPI) + Define Activity Delivery Indicator & Evidence Expectation @@ -237,17 +353,27 @@ export const ProjectIndicatorsModal: React.FC = ({ disabled={createMutation.isPending} className="px-4 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-indigo-600 to-blue-600 hover:from-indigo-500 hover:to-blue-500 rounded-xl shadow-md shadow-indigo-500/20 cursor-pointer" > - {createMutation.isPending ? 'Saving...' : 'Add Indicator'} + {createMutation.isPending ? 'Saving...' : 'Define Indicator'}
)} + {/* Notice Card Explaining Partner Action */} +
+ +
+ Maker-Checker Operational Notice: ConsoleUI enables Project Managers to establish output specifications and expected delivery costs. Actual item quantities, material expenditures, and verification evidence documents are recorded by the Grantee/Partner via the PartnerUI Portal. +
+
+ {/* Indicators List */}
- Performance Target Registry ({indicators.length}) + + Defined Delivery Specifications ({filteredIndicators.length} of {indicators.length}) +
- {isLoading ? ( + {isLoadingIndicators || isLoadingActivities ? (
Loading indicators...
- ) : indicators.length === 0 ? ( + ) : filteredIndicators.length === 0 ? (
-

No M&E indicators defined yet

-

Track quantitative impact metrics against established baselines.

+

No indicators matching selection

+

+ Define delivery output specifications and link them to operational activities. +

) : ( -
- {indicators.map((ind) => { +
+ {filteredIndicators.map((ind) => { const target = Number(ind.targetAmount) || 1; const actual = Number(ind.actualAmount) || 0; const baseline = Number(ind.baselineAmount) || 0; + const targetCost = Number(ind.targetCost) || 0; + const actualCost = Number(ind.actualCost) || 0; const pct = Math.round((actual / target) * 100); - const isRecordingThis = recordingActualForId === ind.id; + + const parentActivity = activities.find((a) => String(a.id) === String(ind.activityId)); + const isOverrun = (actualCost > targetCost && targetCost > 0) || (parentActivity && actualCost > parentActivity.amount); return (
-
-
+
+
{ind.name} {getUnitOfMeasureLabel(ind.unitOfMeasure)} + {parentActivity ? ( + + + {parentActivity.title} + + ) : ( + + General Project Indicator + + )} +
+
+ Indicator ID #{ind.id} + {ind.activityId && ` • Activity #${ind.activityId}`}
- Indicator ID #{ind.id}
-
+
+ {isOverrun && ( + + + Cost Overrun + + )} + = 100 @@ -309,34 +463,20 @@ export const ProjectIndicatorsModal: React.FC = ({ : 'bg-amber-500/10 text-amber-400 border-amber-500/30' }`} > - {pct}% Achieved + {pct}% Delivered - {!isRecordingThis ? ( - - ) : ( - - )} + + + Partner Verified +
- {/* Progress Bar */} + {/* Deliverable Units Progress Bar */}
= ({ />
- Baseline: {baseline} - Actual: {actual} - Target: {target} + + Baseline: {baseline} + + + Partner Actuals Reported:{' '} + {actual} + + + Target: {target} +
- {/* Inline Record Actual Form */} - {isRecordingThis && ( -
- Update Actual Value: -
- setActualInput(Number(e.target.value))} - className="w-24 px-2 py-1 text-xs border border-slate-700 rounded-lg text-white font-mono bg-[#090d16] focus:outline-none focus:ring-1 focus:ring-indigo-500" - /> - -
+ {/* Financial Cost Specification & Actuals Row */} +
+
+ + Target Cost: {formatZAR(targetCost)} + + + Actual Material Cost:{' '} + targetCost && targetCost > 0 ? 'text-amber-400' : 'text-white'}`}> + {formatZAR(actualCost)} + + + {targetCost > 0 && ( + + Variance: {formatZAR(targetCost - actualCost)} + + )}
- )} + + {isOverrun && ( +
+ Cost exceeds delivery allocation; subject to project variation approval (M15). +
+ )} +
); })} diff --git a/PostFundManagement.ConsoleUI/src/components/projects/ProjectMilestonesModal.tsx b/PostFundManagement.ConsoleUI/src/components/projects/ProjectMilestonesModal.tsx index a093c53..1bd76d9 100644 --- a/PostFundManagement.ConsoleUI/src/components/projects/ProjectMilestonesModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/projects/ProjectMilestonesModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -16,6 +16,10 @@ import { X, RotateCcw, Lock, + Coins, + Briefcase, + Target, + AlertTriangle, } from 'lucide-react'; import { Modal } from '../ui/Modal'; import { HelpTooltip } from '../ui/HelpTooltip'; @@ -24,11 +28,15 @@ import { getMilestones, createMilestone, updateMilestone, + getActivities, + getProjectIndicators, + getBudgets, } from '../../services/api'; -import { Project, Milestone, Priority, ApprovalStatus } from '../../types'; +import { Project, Milestone, Priority, ApprovalStatus, Activity, Indicator } from '../../types'; import { useRbac } from '../../auth/rbac'; import { formatDate, + formatZAR, getApprovalStatusBadge, getPriorityBadge, PRIORITY_OPTIONS, @@ -66,7 +74,7 @@ export const ProjectMilestonesModal: React.FC = ({ const { data: milestones = [], - isLoading, + isLoading: isLoadingMilestones, refetch, } = useQuery({ queryKey: ['milestones', projectId], @@ -74,6 +82,54 @@ export const ProjectMilestonesModal: React.FC = ({ enabled: isOpen && !!projectId, }); + const { + data: activities = [], + isLoading: isLoadingActivities, + } = useQuery({ + queryKey: ['activities', projectId], + queryFn: () => (projectId ? getActivities(projectId) : Promise.resolve([])), + enabled: isOpen && !!projectId, + }); + + const { + data: indicators = [], + isLoading: isLoadingIndicators, + } = useQuery({ + queryKey: ['indicators', projectId], + queryFn: () => (projectId ? getProjectIndicators(projectId) : Promise.resolve([])), + enabled: isOpen && !!projectId, + }); + + const { data: allBudgets = [] } = useQuery({ + queryKey: ['budgets'], + queryFn: () => getBudgets(1, 100), + enabled: isOpen, + }); + + // Calculate project approved budget + const approvedBudgetTotal = useMemo(() => { + const projectBudgets = allBudgets.filter((b) => String(b.projectId) === String(projectId)); + return projectBudgets + .filter((b) => b.status === undefined || b.status === null || Number(b.status) === 3) + .reduce((sum, b) => sum + (Number(b.amount) || 0), 0); + }, [allBudgets, projectId]); + + // Total project value is the sum total from all milestones (sum of their activities) + const totalProjectMilestonesValue = useMemo(() => { + return milestones.reduce((sum, m) => { + const mActs = activities.filter((a) => String(a.milestoneId) === String(m.id)); + return sum + mActs.reduce((s, a) => s + (Number(a.amount) || 0), 0); + }, 0); + }, [milestones, activities]); + + // Total project actuals reported by partner from indicators + const totalProjectActuals = useMemo(() => { + return indicators.reduce((sum, i) => sum + (Number(i.actualCost) || 0), 0); + }, [indicators]); + + const verifiedCount = milestones.filter((m) => m.status === ApprovalStatus.Approved).length; + const progressPct = milestones.length > 0 ? Math.round((verifiedCount / milestones.length) * 100) : 0; + const form = useForm({ resolver: zodResolver(milestoneSchema), defaultValues: { @@ -147,9 +203,6 @@ export const ProjectMilestonesModal: React.FC = ({ if (!project) return null; - const verifiedCount = milestones.filter((m) => m.status === ApprovalStatus.Approved).length; - const progressPct = milestones.length > 0 ? Math.round((verifiedCount / milestones.length) * 100) : 0; - return ( = ({ }} title={`Milestones & Deliverables: ${project.name}`} subtitle={`Project #${project.id} • Statutory Work Breakdown Schedule`} - helpText="Contractual milestones represent formal deliverables specified in the grant contract. Upon verification and approval, they increment the calculated project progress score and unblock disbursement releases." + 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" >
- {/* Progress & Quick Stats Card */} -
-
+ {/* KPI Cards: Project Value Roll-Up & Performance */} +
+
- - Calculated Completion Score + + Total Project Value -
- {progressPct}% - ({verifiedCount} of {milestones.length} deliverables verified) +
+ {formatZAR(totalProjectMilestonesValue)}
+ Sum of all milestones
+
+ + + Approved Budget + +
+ {formatZAR(approvedBudgetTotal)} +
+ Allocated budget ceiling +
+ +
+ + + Partner Actuals + +
+ {formatZAR(totalProjectActuals)} +
+ Incurred material costs +
+ +
+ + + Milestone Progress + +
+ {progressPct}% +
+ + {verifiedCount} of {milestones.length} verified + +
+
+ + {/* Action Header */} +
+ + Deliverable Milestones: {milestones.length} + +
{!isFormOpen && canAdd('projects_milestones') && (
- {isLoading ? ( + {isLoadingMilestones || isLoadingActivities || isLoadingIndicators ? (
Loading milestones...
@@ -339,7 +417,9 @@ export const ProjectMilestonesModal: React.FC = ({

No milestones registered yet

-

Define contractual deliverables to monitor implementation progress.

+

+ Define contractual deliverables to monitor implementation progress. +

) : ( -
+
{milestones.map((m) => { const statusBadge = getApprovalStatusBadge(m.status); const priBadge = getPriorityBadge(m.priority); - const isApproved = m.status === ApprovalStatus.Approved; + + // Milestone activities + const mActivities = activities.filter((a) => String(a.milestoneId) === String(m.id)); + const plannedMilestoneValue = mActivities.reduce( + (sum, a) => sum + (Number(a.amount) || 0), + 0 + ); + + // Milestone indicators (linked to activities under this milestone) + const mActivityIds = new Set(mActivities.map((a) => String(a.id))); + const mIndicators = indicators.filter( + (i) => i.activityId && mActivityIds.has(String(i.activityId)) + ); + const actualMilestoneValue = mIndicators.reduce( + (sum, i) => sum + (Number(i.actualCost) || 0), + 0 + ); + + const isOverrun = actualMilestoneValue > plannedMilestoneValue && plannedMilestoneValue > 0; return (
-
-
- {m.name} - - {statusBadge.label} - - - {priBadge.label} - +
+
+
+ {m.name} + + {statusBadge.label} + + + {priBadge.label} + + {isOverrun && ( + + + Budget Overrun + + )} +
+
+ + + Due: {formatDate(m.dueAt)} + + • + ID #{m.id} +
-
- - - Due: {formatDate(m.dueAt)} - - • - ID #{m.id} + +
+
+ Milestone Value: + + {formatZAR(plannedMilestoneValue)} + +
+ + + + Evidence + + +
-
- - - Evidence - + {/* Breakdown of Activities & Indicators */} +
+
+ + + Activities: {mActivities.length} + + • + + + Indicators: {mIndicators.length} + + • + + Actual Incurred: plannedMilestoneValue && plannedMilestoneValue > 0 ? 'text-amber-400' : 'text-slate-200'}`}>{formatZAR(actualMilestoneValue)} + +
- + {plannedMilestoneValue > 0 && ( +
+ Variance: {formatZAR(plannedMilestoneValue - actualMilestoneValue)} +
+ )}
); diff --git a/PostFundManagement.ConsoleUI/src/pages/ProjectsPage.tsx b/PostFundManagement.ConsoleUI/src/pages/ProjectsPage.tsx index 886772f..974db36 100644 --- a/PostFundManagement.ConsoleUI/src/pages/ProjectsPage.tsx +++ b/PostFundManagement.ConsoleUI/src/pages/ProjectsPage.tsx @@ -195,6 +195,7 @@ export const ProjectsPage: React.FC = () => { const [isMilestonesModalOpen, setIsMilestonesModalOpen] = useState(false); const [isActivitiesModalOpen, setIsActivitiesModalOpen] = useState(false); const [isIndicatorsModalOpen, setIsIndicatorsModalOpen] = useState(false); + const [initialIndicatorActivityId, setInitialIndicatorActivityId] = useState(null); const [isRisksModalOpen, setIsRisksModalOpen] = useState(false); // Queries @@ -1726,6 +1727,11 @@ export const ProjectsPage: React.FC = () => { setActiveModalProject(null); }} project={activeModalProject} + onOpenIndicatorsForActivity={(actId) => { + setInitialIndicatorActivityId(actId); + setIsActivitiesModalOpen(false); + setIsIndicatorsModalOpen(true); + }} /> {/* Indicators Management Modal */} @@ -1734,8 +1740,10 @@ export const ProjectsPage: React.FC = () => { onClose={() => { setIsIndicatorsModalOpen(false); setActiveModalProject(null); + setInitialIndicatorActivityId(null); }} project={activeModalProject} + initialActivityId={initialIndicatorActivityId} /> {/* Risks Management Modal */} diff --git a/PostFundManagement.ConsoleUI/src/services/api.ts b/PostFundManagement.ConsoleUI/src/services/api.ts index b5a5e0b..e07e021 100644 --- a/PostFundManagement.ConsoleUI/src/services/api.ts +++ b/PostFundManagement.ConsoleUI/src/services/api.ts @@ -1216,10 +1216,12 @@ export const getIndicator = async (indicatorId: string | number): Promise => { const res = await apiClient.post('/v1/api/evidence/indicators', { projectId: toIntegerId(payload.projectId), + activityId: toNullableIntegerId(payload.activityId), name: payload.name, unitOfMeasure: Number(payload.unitOfMeasure) || 1, baselineAmount: Number(payload.baselineAmount) || 0, targetAmount: Number(payload.targetAmount) || 0, + targetCost: Number(payload.targetCost) || 0, }); return res.data; }; diff --git a/PostFundManagement.ConsoleUI/src/types.ts b/PostFundManagement.ConsoleUI/src/types.ts index c689954..8b5b97a 100644 --- a/PostFundManagement.ConsoleUI/src/types.ts +++ b/PostFundManagement.ConsoleUI/src/types.ts @@ -742,21 +742,26 @@ export interface Indicator { id: number | string; tenantId?: number | string; projectId: number | string; + activityId?: number | string | null; name: string | null; unitOfMeasure?: UnitOfMeasure | null; baselineAmount: number; targetAmount: number; actualAmount: number; + targetCost?: number; + actualCost?: number; createdBy: string; createdAt: string; } export interface CreateIndicatorRequest { projectId: number | string; + activityId?: number | string | null; name: string; unitOfMeasure: UnitOfMeasure; baselineAmount: number; targetAmount: number; + targetCost?: number; } export interface Risk { diff --git a/PostFundManagement.Domain/Entities/Activity.cs b/PostFundManagement.Domain/Entities/Activity.cs index a6fe592..b3fd09b 100644 --- a/PostFundManagement.Domain/Entities/Activity.cs +++ b/PostFundManagement.Domain/Entities/Activity.cs @@ -18,6 +18,8 @@ public class Activity : Models.Activity, ITenantEntity public virtual Milestone? Milestone { get; set; } public virtual ICollection Evidences { get; set; } = []; + + public virtual ICollection Indicators { get; set; } = []; } public sealed class ActivityConfiguration : IEntityTypeConfiguration diff --git a/PostFundManagement.Domain/Entities/Indicator.cs b/PostFundManagement.Domain/Entities/Indicator.cs index b31d03e..cef1f92 100644 --- a/PostFundManagement.Domain/Entities/Indicator.cs +++ b/PostFundManagement.Domain/Entities/Indicator.cs @@ -9,6 +9,8 @@ public class Indicator : Models.Indicator, ITenantEntity public virtual Project? Project { get; set; } + public virtual Activity? Activity { get; set; } + public virtual User? Creator { get; set; } } @@ -21,6 +23,7 @@ public sealed class IndicatorConfiguration : IEntityTypeConfiguration builder.HasKey(pk => pk.Id); builder.Property(f => f.TenantId).IsRequired(); builder.Property(f => f.ProjectId).IsRequired(); + builder.Property(f => f.ActivityId).IsRequired(false); builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); @@ -28,6 +31,8 @@ public sealed class IndicatorConfiguration : IEntityTypeConfiguration builder.Property(f => f.BaselineAmount).IsRequired().HasPrecision(18, 2); builder.Property(f => f.TargetAmount).IsRequired().HasPrecision(18, 2); builder.Property(f => f.ActualAmount).IsRequired().HasPrecision(18, 2); + builder.Property(f => f.TargetCost).IsRequired().HasPrecision(18, 2).HasDefaultValueSql("0"); + builder.Property(f => f.ActualCost).IsRequired().HasPrecision(18, 2).HasDefaultValueSql("0"); builder.HasOne(f => f.Tenant) .WithMany() @@ -41,10 +46,16 @@ public sealed class IndicatorConfiguration : IEntityTypeConfiguration .IsRequired() .OnDelete(DeleteBehavior.Restrict); + builder.HasOne(f => f.Activity) + .WithMany(a => a.Indicators) + .HasForeignKey(fk => fk.ActivityId) + .IsRequired(false) + .OnDelete(DeleteBehavior.Cascade); + builder.HasOne(f => f.Creator) .WithMany() .HasForeignKey(fk => fk.CreatedBy) .IsRequired() .OnDelete(DeleteBehavior.NoAction); } -} \ No newline at end of file +} diff --git a/PostFundManagement.Domain/Extensions/Mappers.cs b/PostFundManagement.Domain/Extensions/Mappers.cs index 2aeb797..84c13be 100644 --- a/PostFundManagement.Domain/Extensions/Mappers.cs +++ b/PostFundManagement.Domain/Extensions/Mappers.cs @@ -201,7 +201,10 @@ public static class Mappers CreatedBy = entity.CreatedBy, Name = entity.Name, ProjectId = entity.ProjectId, + ActivityId = entity.ActivityId, TargetAmount = entity.TargetAmount, + TargetCost = entity.TargetCost, + ActualCost = entity.ActualCost, UnitOfMeasure = entity.UnitOfMeasure }; diff --git a/PostFundManagement.Domain/Models/Indicator.cs b/PostFundManagement.Domain/Models/Indicator.cs index 9dbccde..dd9230d 100644 --- a/PostFundManagement.Domain/Models/Indicator.cs +++ b/PostFundManagement.Domain/Models/Indicator.cs @@ -8,6 +8,8 @@ public class Indicator public long ProjectId { get; set; } + public long? ActivityId { get; set; } + public Guid CreatedBy { get; set; } public DateTime CreatedAt { get; set; } @@ -21,4 +23,8 @@ public class Indicator public decimal TargetAmount { get; set; } public decimal ActualAmount { get; set; } -} \ No newline at end of file + + public decimal TargetCost { get; set; } + + public decimal ActualCost { get; set; } +} diff --git a/PostFundManagement.Infrastructure/Database/Migrations/20261022112056_AddedActivityToIndicator.Designer.cs b/PostFundManagement.Infrastructure/Database/Migrations/20261022112056_AddedActivityToIndicator.Designer.cs new file mode 100644 index 0000000..86f24a9 --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Migrations/20261022112056_AddedActivityToIndicator.Designer.cs @@ -0,0 +1,2502 @@ +// +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("20261022112056_AddedActivityToIndicator")] + partial class AddedActivityToIndicator + { + /// + 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("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("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("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.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("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/20261022112056_AddedActivityToIndicator.cs b/PostFundManagement.Infrastructure/Database/Migrations/20261022112056_AddedActivityToIndicator.cs new file mode 100644 index 0000000..611e05c --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Migrations/20261022112056_AddedActivityToIndicator.cs @@ -0,0 +1,75 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PostFundManagement.Infrastructure.Database.Migrations +{ + /// + public partial class AddedActivityToIndicator : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ActivityId", + table: "Indicators", + type: "bigint", + nullable: true); + + migrationBuilder.AddColumn( + name: "ActualCost", + table: "Indicators", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + defaultValueSql: "0"); + + migrationBuilder.AddColumn( + name: "TargetCost", + table: "Indicators", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + defaultValueSql: "0"); + + migrationBuilder.CreateIndex( + name: "IX_Indicators_ActivityId", + table: "Indicators", + column: "ActivityId"); + + migrationBuilder.AddForeignKey( + name: "FK_Indicators_Activities_ActivityId", + table: "Indicators", + column: "ActivityId", + principalTable: "Activities", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Indicators_Activities_ActivityId", + table: "Indicators"); + + migrationBuilder.DropIndex( + name: "IX_Indicators_ActivityId", + table: "Indicators"); + + migrationBuilder.DropColumn( + name: "ActivityId", + table: "Indicators"); + + migrationBuilder.DropColumn( + name: "ActualCost", + table: "Indicators"); + + migrationBuilder.DropColumn( + name: "TargetCost", + table: "Indicators"); + } + } +} diff --git a/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs b/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs index 961ef59..26d36d2 100644 --- a/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/PostFundManagement.Infrastructure/Database/Migrations/ApplicationDbContextModelSnapshot.cs @@ -634,10 +634,19 @@ namespace PostFundManagement.Infrastructure.Database.Migrations 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)"); @@ -662,6 +671,12 @@ namespace PostFundManagement.Infrastructure.Database.Migrations .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"); @@ -672,6 +687,8 @@ namespace PostFundManagement.Infrastructure.Database.Migrations b.HasKey("Id"); + b.HasIndex("ActivityId"); + b.HasIndex("CreatedBy"); b.HasIndex("ProjectId"); @@ -1929,6 +1946,11 @@ namespace PostFundManagement.Infrastructure.Database.Migrations 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") @@ -1947,6 +1969,8 @@ namespace PostFundManagement.Infrastructure.Database.Migrations .OnDelete(DeleteBehavior.NoAction) .IsRequired(); + b.Navigation("Activity"); + b.Navigation("Creator"); b.Navigation("Project"); @@ -2440,6 +2464,8 @@ namespace PostFundManagement.Infrastructure.Database.Migrations modelBuilder.Entity("PostFundManagement.Domain.Entities.Activity", b => { b.Navigation("Evidences"); + + b.Navigation("Indicators"); }); modelBuilder.Entity("PostFundManagement.Domain.Entities.Award", b =>