From 88a70e123c3f356c49cd30495949a0fc89ecf369 Mon Sep 17 00:00:00 2001 From: Rabi <134292357+Rabi94@users.noreply.github.com> Date: Thu, 21 May 2026 15:43:20 -0300 Subject: [PATCH 1/2] Improve case close-out flow with required reasons --- .../Controllers/ConfigurationController.cs | 40 ++++-- .../OData/LiveODataModelController.cs | 136 ++++++++++-------- .../Authorization/AuthorizationEngine.cs | 4 +- .../Managers/SharedContracts.cs | 2 +- .../Resources/Policies/IPoliciesResource.cs | 9 +- .../Resources/Policies/PoliciesResource.cs | 27 ++++ .../Resources/Referrals/IReferralsResource.cs | 9 +- .../Resources/Referrals/ReferralModel.cs | 19 +++ .../src/Families/FamilyScreen.tsx | 34 ++--- src/caretogether-pwa/src/GeneratedClient.ts | 99 +++++++++++-- .../src/Generic/CloseReasonDrawer.tsx | 112 +++++++++++++++ .../src/Model/ConfigurationModel.ts | 9 ++ .../src/Model/V1CasesModel.ts | 5 +- .../src/Settings/Locations/LocationEdit.tsx | 1 + .../Locations/Tabs/BasicConfiguration.tsx | 34 ++++- .../src/V1Cases/CloseV1CaseDialog.tsx | 116 --------------- .../src/V1Cases/CloseV1CaseDrawer.tsx | 40 ++++++ .../src/V1Cases/ConfirmCloseV1CaseDialog.tsx | 23 --- .../PartneringFamilyTableItem.tsx | 8 +- .../src/V1Referrals/CloseV1ReferralDrawer.tsx | 108 ++------------ swagger.json | 90 ++++++++---- .../MembershipManagerTests.cs | 1 + .../CareTogether.TestData/TestDataProvider.cs | 41 +++--- 23 files changed, 561 insertions(+), 406 deletions(-) create mode 100644 src/caretogether-pwa/src/Generic/CloseReasonDrawer.tsx delete mode 100644 src/caretogether-pwa/src/V1Cases/CloseV1CaseDialog.tsx create mode 100644 src/caretogether-pwa/src/V1Cases/CloseV1CaseDrawer.tsx delete mode 100644 src/caretogether-pwa/src/V1Cases/ConfirmCloseV1CaseDialog.tsx diff --git a/src/CareTogether.Api/Controllers/ConfigurationController.cs b/src/CareTogether.Api/Controllers/ConfigurationController.cs index 0768ec570..f8f34a867 100644 --- a/src/CareTogether.Api/Controllers/ConfigurationController.cs +++ b/src/CareTogether.Api/Controllers/ConfigurationController.cs @@ -22,10 +22,12 @@ public sealed record CurrentFeatureFlags( bool FamilyScreenPageVersionSwitch ); - public sealed record PutLocationPayload( - LocationConfiguration locationConfiguration, - Guid? copyPoliciesFromLocationId - ); + public sealed record PutLocationPayload( + LocationConfiguration locationConfiguration, + Guid? copyPoliciesFromLocationId, + ImmutableList? referralCloseReasons, + ImmutableList? caseCloseReasons + ); [ApiController] [Authorize( @@ -310,15 +312,27 @@ [FromBody] PutLocationPayload newLocationPayload if (!User.IsInRole(SystemConstants.ORGANIZATION_ADMINISTRATOR)) return Forbid(); - if (newLocationConfiguration.Id != default) - { - var updatedLocation = await policiesResource.UpsertLocationDefinitionAsync( - organizationId, - newLocationConfiguration - ); - - return Ok(updatedLocation.OrganizationConfiguration); - } + if (newLocationConfiguration.Id != default) + { + var updatedLocation = await policiesResource.UpsertLocationDefinitionAsync( + organizationId, + newLocationConfiguration + ); + + var updatedConfiguration = + newLocationPayload.referralCloseReasons != null + || newLocationPayload.caseCloseReasons != null + ? await policiesResource.UpdateOrganizationCloseReasonsAsync( + organizationId, + newLocationPayload.referralCloseReasons + ?? updatedLocation.OrganizationConfiguration.ReferralCloseReasons, + newLocationPayload.caseCloseReasons + ?? updatedLocation.OrganizationConfiguration.CaseCloseReasons + ) + : updatedLocation.OrganizationConfiguration; + + return Ok(updatedConfiguration); + } if (copyPoliciesFromLocationId == Guid.Empty) return BadRequest( diff --git a/src/CareTogether.Api/OData/LiveODataModelController.cs b/src/CareTogether.Api/OData/LiveODataModelController.cs index 7a9231a47..d06d7ce9d 100644 --- a/src/CareTogether.Api/OData/LiveODataModelController.cs +++ b/src/CareTogether.Api/OData/LiveODataModelController.cs @@ -163,7 +163,7 @@ public sealed record Referral( DateOnly Opened, DateOnly? Closed, string? ReferralSource, - V1CaseCloseReason? CloseReason, + string? CloseReason, string? PrimaryReasonForReferral ); @@ -286,7 +286,7 @@ public sealed record RoleApproval( string RlsKey ); - public sealed record FamilyRequirementStatus ( + public sealed record FamilyRequirementStatus( Guid OrganizationId, Guid LocationId, [property: Key] Guid FamilyId, @@ -498,7 +498,9 @@ public async Task> GetFamilyRequirementStat [HttpGet("IndividualRequirementStatuses")] [EnableQuery] - public async Task> GetIndividualRequirementStatusesAsync() + public async Task< + IEnumerable + > GetIndividualRequirementStatusesAsync() { var liveModel = await RenderLiveModelAsync(); return liveModel.IndividualRequirementStatuses; @@ -601,7 +603,9 @@ await cache.GetOrAddAsync( acc.CommunityRoleAssignments.Concat(model.CommunityRoleAssignments), acc.RoleApprovals.Concat(model.RoleApprovals), acc.FamilyRequirementStatuses.Concat(model.FamilyRequirementStatuses), - acc.IndividualRequirementStatuses.Concat(model.IndividualRequirementStatuses) + acc.IndividualRequirementStatuses.Concat( + model.IndividualRequirementStatuses + ) ) ); @@ -685,7 +689,9 @@ bool anonymize .ToArrayAsync(); var familiesByLocation = visibleAggregatesByLocation - .Where(zipResult => zipResult.Item2 is FamilyRecordsAggregate fra && !fra.Family.Family.IsTestFamily) + .Where(zipResult => + zipResult.Item2 is FamilyRecordsAggregate fra && !fra.Family.Family.IsTestFamily + ) .Select(zipResult => (zipResult.Item1, (FamilyRecordsAggregate)zipResult.Item2)) .Select(zipResult => ( @@ -925,15 +931,17 @@ await accountsResource.TryGetPersonUserAccountAsync( { foreach (var req in family.VolunteerFamilyInfo.CompletedRequirements) { - familyRequirementStatuses.Add(new FamilyRequirementStatus( - organization.Id, - family.LocationId, - family.Id, - req.RequirementName, - "Complete", - req.CompletedAtUtc, - req.ExpiresAtUtc - )); + familyRequirementStatuses.Add( + new FamilyRequirementStatus( + organization.Id, + family.LocationId, + family.Id, + req.RequirementName, + "Complete", + req.CompletedAtUtc, + req.ExpiresAtUtc + ) + ); } } @@ -941,15 +949,17 @@ await accountsResource.TryGetPersonUserAccountAsync( { foreach (var req in family.VolunteerFamilyInfo.ExemptedRequirements) { - familyRequirementStatuses.Add(new FamilyRequirementStatus( - organization.Id, - family.LocationId, - family.Id, - req.RequirementName, - "Exempted", - req.DueDate, - req.ExemptionExpiresAtUtc - )); + familyRequirementStatuses.Add( + new FamilyRequirementStatus( + organization.Id, + family.LocationId, + family.Id, + req.RequirementName, + "Exempted", + req.DueDate, + req.ExemptionExpiresAtUtc + ) + ); } } @@ -958,15 +968,17 @@ await accountsResource.TryGetPersonUserAccountAsync( { foreach (var missing in family.VolunteerFamilyInfo.MissingRequirements) { - familyRequirementStatuses.Add(new FamilyRequirementStatus( - organization.Id, - family.LocationId, - family.Id, - missing.ActionName, - "Pending", - null, - null - )); + familyRequirementStatuses.Add( + new FamilyRequirementStatus( + organization.Id, + family.LocationId, + family.Id, + missing.ActionName, + "Pending", + null, + null + ) + ); } } } @@ -985,15 +997,17 @@ await accountsResource.TryGetPersonUserAccountAsync( { foreach (var req in volunteerData.CompletedRequirements) { - individualRequirementStatuses.Add(new IndividualRequirementStatus( - organization.Id, - family.LocationId, - personId, - req.RequirementName, - "Complete", - req.CompletedAtUtc, - req.ExpiresAtUtc - )); + individualRequirementStatuses.Add( + new IndividualRequirementStatus( + organization.Id, + family.LocationId, + personId, + req.RequirementName, + "Complete", + req.CompletedAtUtc, + req.ExpiresAtUtc + ) + ); } } @@ -1001,15 +1015,17 @@ await accountsResource.TryGetPersonUserAccountAsync( { foreach (var missing in volunteerData.MissingRequirements) { - individualRequirementStatuses.Add(new IndividualRequirementStatus( - organization.Id, - family.LocationId, - personId, - missing.ActionName, - "Pending", - null, - null - )); + individualRequirementStatuses.Add( + new IndividualRequirementStatus( + organization.Id, + family.LocationId, + personId, + missing.ActionName, + "Pending", + null, + null + ) + ); } } @@ -1017,15 +1033,17 @@ await accountsResource.TryGetPersonUserAccountAsync( { foreach (var exempt in volunteerData.ExemptedRequirements) { - individualRequirementStatuses.Add(new IndividualRequirementStatus( - organization.Id, - family.LocationId, - personId, - exempt.RequirementName, - "Exempted", - exempt.DueDate, - exempt.ExemptionExpiresAtUtc - )); + individualRequirementStatuses.Add( + new IndividualRequirementStatus( + organization.Id, + family.LocationId, + personId, + exempt.RequirementName, + "Exempted", + exempt.DueDate, + exempt.ExemptionExpiresAtUtc + ) + ); } } } diff --git a/src/CareTogether.Core/Engines/Authorization/AuthorizationEngine.cs b/src/CareTogether.Core/Engines/Authorization/AuthorizationEngine.cs index dd39e49a3..6a8be4d5b 100644 --- a/src/CareTogether.Core/Engines/Authorization/AuthorizationEngine.cs +++ b/src/CareTogether.Core/Engines/Authorization/AuthorizationEngine.cs @@ -151,6 +151,7 @@ V1CaseCommand command UnassignStaffFromV1Case => Permission.EditV1CaseStaffAssignments, LinkReferralToCase => Permission.EditV1Case, CloseReferral => Permission.CloseV1Case, + CloseReferralWithReason => Permission.CloseV1Case, ReopenReferral => Permission.CloseV1Case, _ => throw new NotImplementedException( $"The command type '{command.GetType().FullName}' has not been implemented." @@ -943,8 +944,7 @@ ImmutableList contextPermissions ? partneringFamilyInfo .History.Where(activity => contextPermissions.Contains(Permission.ViewV1CaseStaffAssignments) - || activity is not V1CaseStaffAssigned - and not V1CaseStaffUnassigned + || activity is not V1CaseStaffAssigned and not V1CaseStaffUnassigned ) .ToImmutableList() : ImmutableList.Empty, diff --git a/src/CareTogether.Core/Managers/SharedContracts.cs b/src/CareTogether.Core/Managers/SharedContracts.cs index e4fa54c79..ce15d2179 100644 --- a/src/CareTogether.Core/Managers/SharedContracts.cs +++ b/src/CareTogether.Core/Managers/SharedContracts.cs @@ -41,7 +41,7 @@ public sealed record V1Case( Guid Id, DateTime OpenedAtUtc, DateTime? ClosedAtUtc, - V1CaseCloseReason? CloseReason, + string? CloseReason, ImmutableList CompletedRequirements, ImmutableList ExemptedRequirements, ImmutableList MissingRequirements, diff --git a/src/CareTogether.Core/Resources/Policies/IPoliciesResource.cs b/src/CareTogether.Core/Resources/Policies/IPoliciesResource.cs index 2d7043f8d..d0d69134c 100644 --- a/src/CareTogether.Core/Resources/Policies/IPoliciesResource.cs +++ b/src/CareTogether.Core/Resources/Policies/IPoliciesResource.cs @@ -11,7 +11,8 @@ public sealed record OrganizationConfiguration( ImmutableList Locations, ImmutableList Roles, ImmutableList CommunityRoles, - ImmutableList? ReferralCloseReasons + ImmutableList? ReferralCloseReasons, + ImmutableList? CaseCloseReasons ); public sealed record LocationConfiguration( @@ -409,6 +410,12 @@ LocationConfiguration LocationConfiguration LocationConfiguration locationConfiguration ); + Task UpdateOrganizationCloseReasonsAsync( + Guid organizationId, + ImmutableList? referralCloseReasons, + ImmutableList? caseCloseReasons + ); + Task UpsertEffectiveLocationPolicyAsync( Guid organizationId, Guid locationId, diff --git a/src/CareTogether.Core/Resources/Policies/PoliciesResource.cs b/src/CareTogether.Core/Resources/Policies/PoliciesResource.cs index 33ce7b5a2..cc4b41eed 100644 --- a/src/CareTogether.Core/Resources/Policies/PoliciesResource.cs +++ b/src/CareTogether.Core/Resources/Policies/PoliciesResource.cs @@ -160,6 +160,23 @@ LocationConfiguration locationConfiguration return (Render(newConfig), locationConfiguration); } + public async Task UpdateOrganizationCloseReasonsAsync( + Guid organizationId, + ImmutableList? referralCloseReasons, + ImmutableList? caseCloseReasons + ) + { + var config = await configurationStore.GetAsync(organizationId, Guid.Empty, CONFIG); + var newConfig = config with + { + ReferralCloseReasons = referralCloseReasons, + CaseCloseReasons = caseCloseReasons, + }; + + await configurationStore.UpsertAsync(organizationId, Guid.Empty, CONFIG, newConfig); + return Render(newConfig); + } + public async Task UpsertEffectiveLocationPolicyAsync( Guid organizationId, Guid locationId, @@ -250,6 +267,15 @@ public async Task GetOrganizationSecretsAsync(Guid organiza "Need met" ); + private static readonly ImmutableList DefaultCaseCloseReasons = + ImmutableList.Create( + "Not appropriate", + "No capacity", + "No longer needed", + "Resourced", + "Need met" + ); + private OrganizationConfiguration Render(OrganizationConfiguration config) => config with { @@ -271,6 +297,7 @@ config with ) ), ReferralCloseReasons = config.ReferralCloseReasons ?? DefaultReferralCloseReasons, + CaseCloseReasons = config.CaseCloseReasons ?? DefaultCaseCloseReasons, }; } } diff --git a/src/CareTogether.Core/Resources/Referrals/IReferralsResource.cs b/src/CareTogether.Core/Resources/Referrals/IReferralsResource.cs index cf24cbc36..3816d04a1 100644 --- a/src/CareTogether.Core/Resources/Referrals/IReferralsResource.cs +++ b/src/CareTogether.Core/Resources/Referrals/IReferralsResource.cs @@ -13,7 +13,7 @@ public record V1CaseEntry( ImmutableList LinkedV1ReferralIds, DateTime OpenedAtUtc, DateTime? ClosedAtUtc, - V1CaseCloseReason? CloseReason, + string? CloseReason, ImmutableList CompletedRequirements, ImmutableList ExemptedRequirements, ImmutableDictionary CompletedCustomFields, @@ -168,6 +168,13 @@ public sealed record CloseReferral( DateTime ClosedAtUtc ) : V1CaseCommand(FamilyId, ReferralId); + public sealed record CloseReferralWithReason( + Guid FamilyId, + Guid ReferralId, + string CloseReason, + DateTime ClosedAtUtc + ) : V1CaseCommand(FamilyId, ReferralId); + [JsonHierarchyBase] public abstract partial record ArrangementsCommand( Guid FamilyId, diff --git a/src/CareTogether.Core/Resources/Referrals/ReferralModel.cs b/src/CareTogether.Core/Resources/Referrals/ReferralModel.cs index 1a0963d9b..f584891e4 100644 --- a/src/CareTogether.Core/Resources/Referrals/ReferralModel.cs +++ b/src/CareTogether.Core/Resources/Referrals/ReferralModel.cs @@ -218,6 +218,14 @@ v1CaseEntry with timestampUtc ), CloseReferral c => ( + v1CaseEntry with + { + CloseReason = FormatV1CaseCloseReason(c.CloseReason), + ClosedAtUtc = c.ClosedAtUtc, + }, + null + ), + CloseReferralWithReason c => ( v1CaseEntry with { CloseReason = c.CloseReason, @@ -959,6 +967,17 @@ public ImmutableList FindV1CaseEntries(Func pred public V1CaseEntry GetV1CaseEntry(Guid v1CaseId) => v1Cases[v1CaseId]; + private static string FormatV1CaseCloseReason(V1CaseCloseReason closeReason) => + closeReason switch + { + V1CaseCloseReason.NotAppropriate => "Not appropriate", + V1CaseCloseReason.NoCapacity => "No capacity", + V1CaseCloseReason.NoLongerNeeded => "No longer needed", + V1CaseCloseReason.Resourced => "Resourced", + V1CaseCloseReason.NeedMet => "Need met", + _ => closeReason.ToString(), + }; + private V1CaseEntry LinkReferralToCaseEntry( V1CaseEntry v1CaseEntry, LinkReferralToCase command diff --git a/src/caretogether-pwa/src/Families/FamilyScreen.tsx b/src/caretogether-pwa/src/Families/FamilyScreen.tsx index c19cdb8b2..b23f63d90 100644 --- a/src/caretogether-pwa/src/Families/FamilyScreen.tsx +++ b/src/caretogether-pwa/src/Families/FamilyScreen.tsx @@ -32,7 +32,6 @@ import { V1Referral, RoleRemovalReason, V1ReferralStatus, - V1CaseCloseReason, } from '../GeneratedClient'; import { useParams } from 'react-router'; import { @@ -49,7 +48,7 @@ import { AddChildDialog } from './AddChildDialog'; import { AddEditNoteDialog } from '../Notes/AddEditNoteDialog'; import { format } from 'date-fns'; import { UploadFamilyDocumentsDialog } from './UploadFamilyDocumentsDialog'; -import { ConfirmCloseV1CaseDialog } from '../V1Cases/ConfirmCloseV1CaseDialog'; +import { CloseV1CaseDrawer } from '../V1Cases/CloseV1CaseDrawer'; import { OpenNewV1CaseDialog } from '../V1Cases/OpenNewV1CaseDialog'; import { FamilyDocuments } from './FamilyDocuments'; import { useFamilyPermissions } from '../Model/SessionModel'; @@ -180,7 +179,7 @@ export function FamilyScreen() { const allV1Cases: V1Case[] = useMemo(() => { return [...openV1Cases, ...closedV1Cases]; }, [openV1Cases, closedV1Cases]); - const [confirmCloseOpen, setConfirmCloseOpen] = useState(false); + const [closeCaseDrawerOpen, setCloseCaseDrawerOpen] = useState(false); const v1CasesModel = useV1CasesModel(); const referralsLoadable = useLoadable(visibleReferralsQuery); const openReferralId = @@ -234,21 +233,6 @@ export function FamilyScreen() { return { caseRows, unlinkedReferrals }; }, [allV1Cases, familyReferrals]); - async function closeCaseNow() { - if (!selectedV1Case?.id) return; - - await withBackdrop(async () => { - const defaultReason = V1CaseCloseReason.NeedMet; - const closedAtLocal = new Date(); - - await v1CasesModel.closeV1Case( - familyId, - selectedV1Case.id, - defaultReason, - closedAtLocal - ); - }); - } async function reopenCaseNow() { if (!selectedV1Case?.id) return; @@ -821,7 +805,7 @@ export function FamilyScreen() { className="ph-unmask" onClick={(e) => { e.stopPropagation(); - setConfirmCloseOpen(true); + setCloseCaseDrawerOpen(true); }} variant="contained" size="small" @@ -961,13 +945,11 @@ export function FamilyScreen() { - {confirmCloseOpen && ( - setConfirmCloseOpen(false)} - onConfirm={async () => { - setConfirmCloseOpen(false); - await closeCaseNow(); - }} + {closeCaseDrawerOpen && selectedV1Case?.id && ( + setCloseCaseDrawerOpen(false)} /> )} {openNewV1CaseDialogOpen && ( diff --git a/src/caretogether-pwa/src/GeneratedClient.ts b/src/caretogether-pwa/src/GeneratedClient.ts index 04e7bb061..08b1d852d 100644 --- a/src/caretogether-pwa/src/GeneratedClient.ts +++ b/src/caretogether-pwa/src/GeneratedClient.ts @@ -1310,6 +1310,7 @@ export class OrganizationConfiguration implements IOrganizationConfiguration { roles!: RoleDefinition[]; communityRoles!: string[]; referralCloseReasons?: string[] | undefined; + caseCloseReasons?: string[] | undefined; constructor(data?: IOrganizationConfiguration) { if (data) { @@ -1348,6 +1349,11 @@ export class OrganizationConfiguration implements IOrganizationConfiguration { for (let item of _data["referralCloseReasons"]) this.referralCloseReasons!.push(item); } + if (Array.isArray(_data["caseCloseReasons"])) { + this.caseCloseReasons = [] as any; + for (let item of _data["caseCloseReasons"]) + this.caseCloseReasons!.push(item); + } } } @@ -1381,6 +1387,11 @@ export class OrganizationConfiguration implements IOrganizationConfiguration { for (let item of this.referralCloseReasons) data["referralCloseReasons"].push(item); } + if (Array.isArray(this.caseCloseReasons)) { + data["caseCloseReasons"] = []; + for (let item of this.caseCloseReasons) + data["caseCloseReasons"].push(item); + } return data; } } @@ -1391,6 +1402,7 @@ export interface IOrganizationConfiguration { roles: RoleDefinition[]; communityRoles: string[]; referralCloseReasons?: string[] | undefined; + caseCloseReasons?: string[] | undefined; } export class LocationConfiguration implements ILocationConfiguration { @@ -2481,6 +2493,8 @@ export enum Permission { export class PutLocationPayload implements IPutLocationPayload { locationConfiguration!: LocationConfiguration; copyPoliciesFromLocationId?: string | undefined; + referralCloseReasons?: string[] | undefined; + caseCloseReasons?: string[] | undefined; constructor(data?: IPutLocationPayload) { if (data) { @@ -2498,6 +2512,16 @@ export class PutLocationPayload implements IPutLocationPayload { if (_data) { this.locationConfiguration = _data["locationConfiguration"] ? LocationConfiguration.fromJS(_data["locationConfiguration"]) : new LocationConfiguration(); this.copyPoliciesFromLocationId = _data["copyPoliciesFromLocationId"]; + if (Array.isArray(_data["referralCloseReasons"])) { + this.referralCloseReasons = [] as any; + for (let item of _data["referralCloseReasons"]) + this.referralCloseReasons!.push(item); + } + if (Array.isArray(_data["caseCloseReasons"])) { + this.caseCloseReasons = [] as any; + for (let item of _data["caseCloseReasons"]) + this.caseCloseReasons!.push(item); + } } } @@ -2512,6 +2536,16 @@ export class PutLocationPayload implements IPutLocationPayload { data = typeof data === 'object' ? data : {}; data["locationConfiguration"] = this.locationConfiguration ? this.locationConfiguration.toJSON() : undefined; data["copyPoliciesFromLocationId"] = this.copyPoliciesFromLocationId; + if (Array.isArray(this.referralCloseReasons)) { + data["referralCloseReasons"] = []; + for (let item of this.referralCloseReasons) + data["referralCloseReasons"].push(item); + } + if (Array.isArray(this.caseCloseReasons)) { + data["caseCloseReasons"] = []; + for (let item of this.caseCloseReasons) + data["caseCloseReasons"].push(item); + } return data; } } @@ -2519,6 +2553,8 @@ export class PutLocationPayload implements IPutLocationPayload { export interface IPutLocationPayload { locationConfiguration: LocationConfiguration; copyPoliciesFromLocationId?: string | undefined; + referralCloseReasons?: string[] | undefined; + caseCloseReasons?: string[] | undefined; } export class EffectiveLocationPolicy implements IEffectiveLocationPolicy { @@ -6329,7 +6365,7 @@ export class V1Case implements IV1Case { id!: string; openedAtUtc!: Date; closedAtUtc?: Date | undefined; - closeReason?: V1CaseCloseReason | undefined; + closeReason?: string | undefined; completedRequirements!: CompletedRequirementInfo[]; exemptedRequirements!: ExemptedRequirementInfo[]; missingRequirements!: RequirementDefinition[]; @@ -6471,7 +6507,7 @@ export interface IV1Case { id: string; openedAtUtc: Date; closedAtUtc?: Date | undefined; - closeReason?: V1CaseCloseReason | undefined; + closeReason?: string | undefined; completedRequirements: CompletedRequirementInfo[]; exemptedRequirements: ExemptedRequirementInfo[]; missingRequirements: RequirementDefinition[]; @@ -6483,14 +6519,6 @@ export interface IV1Case { linkedV1ReferralIds: string[]; } -export enum V1CaseCloseReason { - NotAppropriate = 0, - NoCapacity = 1, - NoLongerNeeded = 2, - Resourced = 3, - NeedMet = 4, -} - export class CompletedRequirementInfo implements ICompletedRequirementInfo { userId!: string; timestampUtc!: Date; @@ -13835,6 +13863,11 @@ export abstract class V1CaseCommand implements IV1CaseCommand { result.init(data); return result; } + if (data["discriminator"] === "CloseReferralWithReason") { + let result = new CloseReferralWithReason(); + result.init(data); + return result; + } if (data["discriminator"] === "CompleteReferralRequirement") { let result = new CompleteReferralRequirement(); result.init(data); @@ -13978,6 +14011,52 @@ export interface ICloseReferral extends IV1CaseCommand { closedAtUtc: Date; } +export enum V1CaseCloseReason { + NotAppropriate = 0, + NoCapacity = 1, + NoLongerNeeded = 2, + Resourced = 3, + NeedMet = 4, +} + +export class CloseReferralWithReason extends V1CaseCommand implements ICloseReferralWithReason { + closeReason!: string; + closedAtUtc!: Date; + + constructor(data?: ICloseReferralWithReason) { + super(data); + this._discriminator = "CloseReferralWithReason"; + } + + init(_data?: any) { + super.init(_data); + if (_data) { + this.closeReason = _data["closeReason"]; + this.closedAtUtc = _data["closedAtUtc"] ? new Date(_data["closedAtUtc"].toString()) : undefined; + } + } + + static fromJS(data: any): CloseReferralWithReason { + data = typeof data === 'object' ? data : {}; + let result = new CloseReferralWithReason(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["closeReason"] = this.closeReason; + data["closedAtUtc"] = this.closedAtUtc ? this.closedAtUtc.toISOString() : undefined; + super.toJSON(data); + return data; + } +} + +export interface ICloseReferralWithReason extends IV1CaseCommand { + closeReason: string; + closedAtUtc: Date; +} + export class CompleteReferralRequirement extends V1CaseCommand implements ICompleteReferralRequirement { completedRequirementId!: string; requirementName!: string; diff --git a/src/caretogether-pwa/src/Generic/CloseReasonDrawer.tsx b/src/caretogether-pwa/src/Generic/CloseReasonDrawer.tsx new file mode 100644 index 000000000..79a66ec2f --- /dev/null +++ b/src/caretogether-pwa/src/Generic/CloseReasonDrawer.tsx @@ -0,0 +1,112 @@ +import { useState } from 'react'; +import { + Button, + Drawer, + FormControl, + FormControlLabel, + FormLabel, + Grid, + Radio, + RadioGroup, + Typography, +} from '@mui/material'; +import { ValidateDatePicker } from './Forms/ValidateDatePicker'; + +type CloseReasonDrawerProps = { + title: string; + reasons: string[]; + dateLabel: string; + saveLabel: string; + onClose: () => void; + onSave: (reason: string, closedAtLocal: Date) => Promise; +}; + +export function CloseReasonDrawer({ + title, + reasons, + dateLabel, + saveLabel, + onClose, + onSave, +}: CloseReasonDrawerProps) { + const [reason, setReason] = useState(null); + const [closedAtLocal, setClosedAtLocal] = useState(null); + const [dateError, setDateError] = useState(false); + + const canSave = reason !== null && closedAtLocal !== null && !dateError; + + async function save() { + if (!reason || !closedAtLocal) return; + + await onSave(reason, closedAtLocal); + } + + return ( + +
+ + + {title} + + + + + Reason for Closing: + setReason(e.target.value)} + > + {reasons.map((reasonOption) => ( + } + label={reasonOption} + /> + ))} + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/src/caretogether-pwa/src/Model/ConfigurationModel.ts b/src/caretogether-pwa/src/Model/ConfigurationModel.ts index 69edaab4d..59ec6e742 100644 --- a/src/caretogether-pwa/src/Model/ConfigurationModel.ts +++ b/src/caretogether-pwa/src/Model/ConfigurationModel.ts @@ -20,6 +20,7 @@ export type ExtendedOrganizationConfiguration = OrganizationConfiguration & { ethnicities?: string[]; adultFamilyRelationships?: string[]; arrangementReasons?: string[]; + caseCloseReasons?: string[]; referralCloseReasons?: string[]; }; @@ -67,6 +68,14 @@ export const referralCloseReasonsData = selector({ }, }); +export const caseCloseReasonsData = selector({ + key: 'COMPATIBILITY__caseCloseReasonsData', + get: ({ get }) => { + const organizationConfiguration = get(organizationConfigurationQuery); + return organizationConfiguration?.caseCloseReasons ?? []; + }, +}); + export const adultFamilyRelationshipsData = selector({ //TODO: Rename to 'query' key: 'COMPATIBILITY__adultFamilyRelationshipsData', diff --git a/src/caretogether-pwa/src/Model/V1CasesModel.ts b/src/caretogether-pwa/src/Model/V1CasesModel.ts index 7807adda7..373d19568 100644 --- a/src/caretogether-pwa/src/Model/V1CasesModel.ts +++ b/src/caretogether-pwa/src/Model/V1CasesModel.ts @@ -10,8 +10,7 @@ import { EndArrangements, AssignVolunteerFamily, AssignIndividualVolunteer, - V1CaseCloseReason, - CloseReferral as CloseV1Case, + CloseReferralWithReason as CloseV1Case, CreateReferral as CreateV1Case, ReopenReferral as ReopenV1Case, TrackChildLocationChange, @@ -925,7 +924,7 @@ export function useV1CasesModel() { async ( partneringFamilyId: string, v1CaseId: string, - reason: V1CaseCloseReason, + reason: string, closedAtLocal: Date ) => { const command = commandFactory(CloseV1Case, { diff --git a/src/caretogether-pwa/src/Settings/Locations/LocationEdit.tsx b/src/caretogether-pwa/src/Settings/Locations/LocationEdit.tsx index 3e2d78ed5..05eb50d43 100644 --- a/src/caretogether-pwa/src/Settings/Locations/LocationEdit.tsx +++ b/src/caretogether-pwa/src/Settings/Locations/LocationEdit.tsx @@ -151,6 +151,7 @@ export function LocationEdit() { ethnicities: location.ethnicities || [], adultFamilyRelationships: location.adultFamilyRelationships || [], arrangementReasons: location.arrangementReasons || [], + caseCloseReasons: configuration?.caseCloseReasons || [], referralCloseReasons: configuration?.referralCloseReasons || [], }; diff --git a/src/caretogether-pwa/src/Settings/Locations/Tabs/BasicConfiguration.tsx b/src/caretogether-pwa/src/Settings/Locations/Tabs/BasicConfiguration.tsx index 1d2c93595..7831e2e03 100644 --- a/src/caretogether-pwa/src/Settings/Locations/Tabs/BasicConfiguration.tsx +++ b/src/caretogether-pwa/src/Settings/Locations/Tabs/BasicConfiguration.tsx @@ -18,6 +18,7 @@ export type ConfigurationData = { ethnicities: string[]; adultFamilyRelationships: string[]; arrangementReasons: string[]; + caseCloseReasons: string[]; referralCloseReasons: string[]; }; @@ -26,6 +27,7 @@ export type AvailableOptions = { ethnicities: string[]; adultFamilyRelationships: string[]; arrangementReasons: string[]; + caseCloseReasons: string[]; referralCloseReasons: string[]; }; @@ -53,7 +55,7 @@ export default function BasicConfiguration({ const onSubmit: SubmitHandler = async (data) => { withBackdrop(async () => { - const { referralCloseReasons, ...locationData } = data; + const { caseCloseReasons, referralCloseReasons, ...locationData } = data; const updatedOrgConfig = await api.configuration.putLocationDefinition( organizationId, @@ -62,15 +64,12 @@ export default function BasicConfiguration({ ...currentLocationDefinition, ...locationData, }), - }) - ); - - storeEdits( - new OrganizationConfiguration({ - ...updatedOrgConfig, + caseCloseReasons, referralCloseReasons, }) ); + + storeEdits(new OrganizationConfiguration(updatedOrgConfig)); }); }; @@ -171,6 +170,27 @@ export default function BasicConfiguration({ minTypingAreaWidth={120} /> + Case close reasons + + + Here you can customize the list of reasons for closing cases. +
+ These options will be available when closing a case across the + organization. +
+ You can add new ones or remove ones you don’t need anymore, it won’t + change any existing records. +
+ + + Referral close reasons diff --git a/src/caretogether-pwa/src/V1Cases/CloseV1CaseDialog.tsx b/src/caretogether-pwa/src/V1Cases/CloseV1CaseDialog.tsx deleted file mode 100644 index c17f006b7..000000000 --- a/src/caretogether-pwa/src/V1Cases/CloseV1CaseDialog.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useState } from 'react'; -import { - FormControl, - FormControlLabel, - FormLabel, - Grid, - Radio, - RadioGroup, -} from '@mui/material'; -import { V1CaseCloseReason } from '../GeneratedClient'; -import { UpdateDialog } from '../Generic/UpdateDialog'; -import { ValidateDatePicker } from '../Generic/Forms/ValidateDatePicker'; -import { useV1CasesModel } from '../Model/V1CasesModel'; - -interface CloseV1CaseDialogProps { - partneringFamilyId: string; - v1CaseId: string; - onClose: () => void; -} - -export function CloseV1CaseDialog({ - partneringFamilyId, - v1CaseId, - onClose, -}: CloseV1CaseDialogProps) { - const v1CasesModel = useV1CasesModel(); - const [fields, setFields] = useState({ - reason: null as V1CaseCloseReason | null, - closedAtLocal: null as Date | null, - }); - - const [dobError, setDobError] = useState(false); - - const { reason, closedAtLocal } = fields; - - async function save() { - await v1CasesModel.closeV1Case( - partneringFamilyId, - v1CaseId, - reason!, - closedAtLocal! - ); - } - - return ( - reason != null && closedAtLocal != null && !dobError} - > -
- - - - Reason for Closing: - - setFields({ - ...fields, - reason: - V1CaseCloseReason[ - e.target.value as keyof typeof V1CaseCloseReason - ], - }) - } - > - } - label="Not Appropriate" - /> - } - label="No Capacity" - /> - } - label="No Longer Needed" - /> - } - label="Resourced" - /> - } - label="Need Met" - /> - - - - - setFields({ ...fields, closedAtLocal: date })} - onErrorChange={setDobError} - disableFuture - textFieldProps={{ - fullWidth: true, - required: true, - }} - /> - - -
-
- ); -} diff --git a/src/caretogether-pwa/src/V1Cases/CloseV1CaseDrawer.tsx b/src/caretogether-pwa/src/V1Cases/CloseV1CaseDrawer.tsx new file mode 100644 index 000000000..99c164aef --- /dev/null +++ b/src/caretogether-pwa/src/V1Cases/CloseV1CaseDrawer.tsx @@ -0,0 +1,40 @@ +import { useRecoilValue } from 'recoil'; +import { CloseReasonDrawer } from '../Generic/CloseReasonDrawer'; +import { caseCloseReasonsData } from '../Model/ConfigurationModel'; +import { useV1CasesModel } from '../Model/V1CasesModel'; + +interface CloseV1CaseDrawerProps { + partneringFamilyId: string; + v1CaseId: string; + onClose: () => void; +} + +export function CloseV1CaseDrawer({ + partneringFamilyId, + v1CaseId, + onClose, +}: CloseV1CaseDrawerProps) { + const v1CasesModel = useV1CasesModel(); + const caseCloseReasons = useRecoilValue(caseCloseReasonsData); + + async function closeCase(reason: string, closedAtLocal: Date) { + await v1CasesModel.closeV1Case( + partneringFamilyId, + v1CaseId, + reason, + closedAtLocal + ); + onClose(); + } + + return ( + + ); +} diff --git a/src/caretogether-pwa/src/V1Cases/ConfirmCloseV1CaseDialog.tsx b/src/caretogether-pwa/src/V1Cases/ConfirmCloseV1CaseDialog.tsx deleted file mode 100644 index 63cb1e6df..000000000 --- a/src/caretogether-pwa/src/V1Cases/ConfirmCloseV1CaseDialog.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { UpdateDialog } from '../Generic/UpdateDialog'; - -type ConfirmCloseV1CaseDialogProps = { - onClose: () => void; - onConfirm: () => void | Promise; -}; - -export function ConfirmCloseV1CaseDialog({ - onClose, - onConfirm, -}: ConfirmCloseV1CaseDialogProps) { - return ( - { - await onConfirm(); - }} - saveLabel="Yes" - noAutoClose={false} - /> - ); -} diff --git a/src/caretogether-pwa/src/V1Cases/PartneringFamilies/PartneringFamilyTableItem.tsx b/src/caretogether-pwa/src/V1Cases/PartneringFamilies/PartneringFamilyTableItem.tsx index 15e5e4cb9..f45972d2f 100644 --- a/src/caretogether-pwa/src/V1Cases/PartneringFamilies/PartneringFamilyTableItem.tsx +++ b/src/caretogether-pwa/src/V1Cases/PartneringFamilies/PartneringFamilyTableItem.tsx @@ -2,11 +2,7 @@ import { Box, Grid, TableCell, TableRow } from '@mui/material'; import { format } from 'date-fns'; import { useState } from 'react'; import { Phone as PhoneIcon } from '@mui/icons-material'; -import { - ArrangementPhase, - CompletedCustomFieldInfo, - V1CaseCloseReason, -} from '../../GeneratedClient'; +import { ArrangementPhase, CompletedCustomFieldInfo } from '../../GeneratedClient'; import { FamilyName } from '../../Families/FamilyName'; import { TestFamilyBadge } from '../../Families/TestFamilyBadge'; import { LazyLoadMountTrigger } from '../../Utilities/LazyLoadMountTrigger'; @@ -114,7 +110,7 @@ function PartneringFamilyTableRows(props: PartneringFamilyTableItemProps) { const caseStatusText = openV1Case ? 'Open since ' + format(openV1Case.openedAtUtc!, 'MM/dd/yyyy') : latestClosedV1Case?.closeReason != null - ? 'Closed - ' + V1CaseCloseReason[latestClosedV1Case.closeReason] + ? 'Closed - ' + latestClosedV1Case.closeReason : 'No case'; return ( diff --git a/src/caretogether-pwa/src/V1Referrals/CloseV1ReferralDrawer.tsx b/src/caretogether-pwa/src/V1Referrals/CloseV1ReferralDrawer.tsx index 8d4d53746..ccd95dbe3 100644 --- a/src/caretogether-pwa/src/V1Referrals/CloseV1ReferralDrawer.tsx +++ b/src/caretogether-pwa/src/V1Referrals/CloseV1ReferralDrawer.tsx @@ -1,19 +1,7 @@ -import { useState } from 'react'; -import { - Button, - Drawer, - FormControl, - FormControlLabel, - FormLabel, - Grid, - Radio, - RadioGroup, - Typography, -} from '@mui/material'; -import { ValidateDatePicker } from '../Generic/Forms/ValidateDatePicker'; -import { useV1ReferralsModel } from '../Model/V1ReferralsModel'; import { useRecoilValue } from 'recoil'; +import { CloseReasonDrawer } from '../Generic/CloseReasonDrawer'; import { referralCloseReasonsData } from '../Model/ConfigurationModel'; +import { useV1ReferralsModel } from '../Model/V1ReferralsModel'; interface CloseV1ReferralDrawerProps { referralId: string; @@ -27,93 +15,19 @@ export function CloseV1ReferralDrawer({ const { closeReferral } = useV1ReferralsModel(); const referralCloseReasons = useRecoilValue(referralCloseReasonsData); - const [fields, setFields] = useState<{ - reason: string | null; - closedAtLocal: Date | null; - }>({ - reason: null, - closedAtLocal: null, - }); - - const [dateError, setDateError] = useState(false); - const { reason, closedAtLocal } = fields; - - const canSave = reason !== null && closedAtLocal !== null && !dateError; - - async function save() { - await closeReferral(referralId, reason!, closedAtLocal!); + async function closeCurrentReferral(reason: string, closedAtLocal: Date) { + await closeReferral(referralId, reason, closedAtLocal); onClose(); } return ( - -
- - - - Why is this Referral being closed? - - - - - - Reason for Closing: - - - setFields({ ...fields, reason: e.target.value }) - } - > - {referralCloseReasons.map((reasonOption) => ( - } - label={reasonOption} - /> - ))} - - - - - - setFields({ ...fields, closedAtLocal: date })} - onErrorChange={setDateError} - disableFuture - textFieldProps={{ fullWidth: true, required: true }} - /> - - - - - - - - -
-
+ onSave={closeCurrentReferral} + /> ); } diff --git a/swagger.json b/swagger.json index dc85065d3..511f302d9 100644 --- a/swagger.json +++ b/swagger.json @@ -1323,6 +1323,13 @@ "items": { "type": "string" } + }, + "caseCloseReasons": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } } } }, @@ -1976,6 +1983,20 @@ "type": "string", "format": "guid", "nullable": true + }, + "referralCloseReasons": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "caseCloseReasons": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } } } }, @@ -4082,12 +4103,8 @@ "nullable": true }, "closeReason": { - "nullable": true, - "oneOf": [ - { - "$ref": "#/components/schemas/V1CaseCloseReason" - } - ] + "type": "string", + "nullable": true }, "completedRequirements": { "type": "array", @@ -4144,24 +4161,6 @@ } } }, - "V1CaseCloseReason": { - "type": "integer", - "description": "", - "x-enumNames": [ - "NotAppropriate", - "NoCapacity", - "NoLongerNeeded", - "Resourced", - "NeedMet" - ], - "enum": [ - 0, - 1, - 2, - 3, - 4 - ] - }, "CompletedRequirementInfo": { "type": "object", "additionalProperties": false, @@ -8472,6 +8471,7 @@ "mapping": { "AssignStaffToV1Case": "#/components/schemas/AssignStaffToV1Case", "CloseReferral": "#/components/schemas/CloseReferral", + "CloseReferralWithReason": "#/components/schemas/CloseReferralWithReason", "CompleteReferralRequirement": "#/components/schemas/CompleteReferralRequirement", "CreateReferral": "#/components/schemas/CreateReferral", "ExemptReferralRequirement": "#/components/schemas/ExemptReferralRequirement", @@ -8553,6 +8553,48 @@ } ] }, + "V1CaseCloseReason": { + "type": "integer", + "description": "", + "x-enumNames": [ + "NotAppropriate", + "NoCapacity", + "NoLongerNeeded", + "Resourced", + "NeedMet" + ], + "enum": [ + 0, + 1, + 2, + 3, + 4 + ] + }, + "CloseReferralWithReason": { + "allOf": [ + { + "$ref": "#/components/schemas/V1CaseCommand" + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "closeReason", + "closedAtUtc" + ], + "properties": { + "closeReason": { + "type": "string" + }, + "closedAtUtc": { + "type": "string", + "format": "date-time" + } + } + } + ] + }, "CompleteReferralRequirement": { "allOf": [ { diff --git a/test/CareTogether.Core.Test/MembershipManagerTests.cs b/test/CareTogether.Core.Test/MembershipManagerTests.cs index 780dc42c7..3253c2724 100644 --- a/test/CareTogether.Core.Test/MembershipManagerTests.cs +++ b/test/CareTogether.Core.Test/MembershipManagerTests.cs @@ -69,6 +69,7 @@ public async Task GetUserAccessAsync_IgnoresAccountLocationsThatAreNotConfigured ), ImmutableList.Empty, ImmutableList.Empty, + ImmutableList.Empty, ImmutableList.Empty ) ); diff --git a/test/CareTogether.TestData/TestDataProvider.cs b/test/CareTogether.TestData/TestDataProvider.cs index 169e6c27d..2e236d246 100644 --- a/test/CareTogether.TestData/TestDataProvider.cs +++ b/test/CareTogether.TestData/TestDataProvider.cs @@ -2331,10 +2331,10 @@ .. new[] "Relative", "Droid", }, - ], - ["Crisis", "Assistance", "Respite"], - sourcePhoneNumbers, - [ + ], + ["Crisis", "Assistance", "Respite"], + sourcePhoneNumbers, + [ new AccessLevel( Guid.NewGuid(), "Staff Only", @@ -2358,10 +2358,10 @@ .. new[] "Relative", "Domestic Worker", }, - ], - ["Crisis", "Assistance", "Respite"], - sourcePhoneNumbers, - null + ], + ["Crisis", "Assistance", "Respite"], + sourcePhoneNumbers, + null ), ], [ @@ -2487,15 +2487,22 @@ .. new[] ), ], ["Community Organizer", "Community Co-Organizer"], - ImmutableList.Create( - "Not appropriate", - "No capacity", - "No longer needed", - "Resourced", - "Need met" - ) - ) - ); + ImmutableList.Create( + "Not appropriate", + "No capacity", + "No longer needed", + "Resourced", + "Need met" + ), + ImmutableList.Create( + "Not appropriate", + "No capacity", + "No longer needed", + "Resourced", + "Need met" + ) + ) + ); await organizationSecretsStore.UpsertAsync( guid1, From ad08c0281b0263b2b89158fe89f4a9f0e52cf879 Mon Sep 17 00:00:00 2001 From: Rabi <134292357+Rabi94@users.noreply.github.com> Date: Mon, 25 May 2026 11:19:22 -0300 Subject: [PATCH 2/2] Separate organization configuration from location definition --- .../Controllers/ConfigurationController.cs | 62 +++++--- src/caretogether-pwa/src/Api/Api.ts | 7 + src/caretogether-pwa/src/GeneratedClient.ts | 132 ++++++++++++++---- .../Locations/Tabs/BasicConfiguration.tsx | 14 +- swagger.json | 78 +++++++++-- 5 files changed, 233 insertions(+), 60 deletions(-) diff --git a/src/CareTogether.Api/Controllers/ConfigurationController.cs b/src/CareTogether.Api/Controllers/ConfigurationController.cs index f8f34a867..bbafa2618 100644 --- a/src/CareTogether.Api/Controllers/ConfigurationController.cs +++ b/src/CareTogether.Api/Controllers/ConfigurationController.cs @@ -24,9 +24,7 @@ bool FamilyScreenPageVersionSwitch public sealed record PutLocationPayload( LocationConfiguration locationConfiguration, - Guid? copyPoliciesFromLocationId, - ImmutableList? referralCloseReasons, - ImmutableList? caseCloseReasons + Guid? copyPoliciesFromLocationId ); [ApiController] @@ -319,19 +317,7 @@ [FromBody] PutLocationPayload newLocationPayload newLocationConfiguration ); - var updatedConfiguration = - newLocationPayload.referralCloseReasons != null - || newLocationPayload.caseCloseReasons != null - ? await policiesResource.UpdateOrganizationCloseReasonsAsync( - organizationId, - newLocationPayload.referralCloseReasons - ?? updatedLocation.OrganizationConfiguration.ReferralCloseReasons, - newLocationPayload.caseCloseReasons - ?? updatedLocation.OrganizationConfiguration.CaseCloseReasons - ) - : updatedLocation.OrganizationConfiguration; - - return Ok(updatedConfiguration); + return Ok(updatedLocation.OrganizationConfiguration); } if (copyPoliciesFromLocationId == Guid.Empty) @@ -459,6 +445,44 @@ public async Task> GetLocationFlags(Guid organ ) ); return Ok(result); - } - } -} + } + } + + public sealed record PutOrganizationConfigurationPayload( + ImmutableList? ReferralCloseReasons, + ImmutableList? CaseCloseReasons + ); + + [ApiController] + [Authorize( + Policies.ForbidAnonymous, + AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme + )] + public class OrganizationConfigurationController : ControllerBase + { + private readonly IPoliciesResource policiesResource; + + public OrganizationConfigurationController(IPoliciesResource policiesResource) + { + this.policiesResource = policiesResource; + } + + [HttpPut("/api/{organizationId:guid}/[controller]")] + public async Task> PutOrganizationConfiguration( + Guid organizationId, + [FromBody] PutOrganizationConfigurationPayload payload + ) + { + if (!User.IsInRole(SystemConstants.ORGANIZATION_ADMINISTRATOR)) + return Forbid(); + + var result = await policiesResource.UpdateOrganizationCloseReasonsAsync( + organizationId, + payload.ReferralCloseReasons, + payload.CaseCloseReasons + ); + + return Ok(result); + } + } +} diff --git a/src/caretogether-pwa/src/Api/Api.ts b/src/caretogether-pwa/src/Api/Api.ts index c46517bf8..112c541fb 100644 --- a/src/caretogether-pwa/src/Api/Api.ts +++ b/src/caretogether-pwa/src/Api/Api.ts @@ -2,6 +2,7 @@ import { CommunicationsClient, ConfigurationClient, FilesClient, + OrganizationConfigurationClient, RecordsClient, UsersClient, } from '../GeneratedClient'; @@ -17,6 +18,11 @@ const configurationClient = new ConfigurationClient( authenticatingFetch ); +const organizationConfigurationClient = new OrganizationConfigurationClient( + import.meta.env.VITE_APP_API_HOST, + authenticatingFetch +); + const recordsClient = new RecordsClient( import.meta.env.VITE_APP_API_HOST, authenticatingFetch @@ -35,6 +41,7 @@ const communicationsClient = new CommunicationsClient( export const api = { users: usersClient, configuration: configurationClient, + organizationConfiguration: organizationConfigurationClient, records: recordsClient, files: filesClient, communications: communicationsClient, diff --git a/src/caretogether-pwa/src/GeneratedClient.ts b/src/caretogether-pwa/src/GeneratedClient.ts index 08b1d852d..93bfbbf91 100644 --- a/src/caretogether-pwa/src/GeneratedClient.ts +++ b/src/caretogether-pwa/src/GeneratedClient.ts @@ -323,6 +323,58 @@ export class ConfigurationClient { } } +export class OrganizationConfigurationClient { + private http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; + private baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { + this.http = http ? http : window as any; + this.baseUrl = baseUrl ?? ""; + } + + putOrganizationConfiguration(organizationId: string, payload: PutOrganizationConfigurationPayload): Promise { + let url_ = this.baseUrl + "/api/{organizationId}/OrganizationConfiguration"; + if (organizationId === undefined || organizationId === null) + throw new Error("The parameter 'organizationId' must be defined."); + url_ = url_.replace("{organizationId}", encodeURIComponent("" + organizationId)); + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(payload); + + let options_: RequestInit = { + body: content_, + method: "PUT", + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processPutOrganizationConfiguration(_response); + }); + } + + protected processPutOrganizationConfiguration(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = OrganizationConfiguration.fromJS(resultData200); + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } +} + export class FilesClient { private http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; private baseUrl: string; @@ -2493,8 +2545,6 @@ export enum Permission { export class PutLocationPayload implements IPutLocationPayload { locationConfiguration!: LocationConfiguration; copyPoliciesFromLocationId?: string | undefined; - referralCloseReasons?: string[] | undefined; - caseCloseReasons?: string[] | undefined; constructor(data?: IPutLocationPayload) { if (data) { @@ -2512,16 +2562,6 @@ export class PutLocationPayload implements IPutLocationPayload { if (_data) { this.locationConfiguration = _data["locationConfiguration"] ? LocationConfiguration.fromJS(_data["locationConfiguration"]) : new LocationConfiguration(); this.copyPoliciesFromLocationId = _data["copyPoliciesFromLocationId"]; - if (Array.isArray(_data["referralCloseReasons"])) { - this.referralCloseReasons = [] as any; - for (let item of _data["referralCloseReasons"]) - this.referralCloseReasons!.push(item); - } - if (Array.isArray(_data["caseCloseReasons"])) { - this.caseCloseReasons = [] as any; - for (let item of _data["caseCloseReasons"]) - this.caseCloseReasons!.push(item); - } } } @@ -2536,16 +2576,6 @@ export class PutLocationPayload implements IPutLocationPayload { data = typeof data === 'object' ? data : {}; data["locationConfiguration"] = this.locationConfiguration ? this.locationConfiguration.toJSON() : undefined; data["copyPoliciesFromLocationId"] = this.copyPoliciesFromLocationId; - if (Array.isArray(this.referralCloseReasons)) { - data["referralCloseReasons"] = []; - for (let item of this.referralCloseReasons) - data["referralCloseReasons"].push(item); - } - if (Array.isArray(this.caseCloseReasons)) { - data["caseCloseReasons"] = []; - for (let item of this.caseCloseReasons) - data["caseCloseReasons"].push(item); - } return data; } } @@ -2553,8 +2583,6 @@ export class PutLocationPayload implements IPutLocationPayload { export interface IPutLocationPayload { locationConfiguration: LocationConfiguration; copyPoliciesFromLocationId?: string | undefined; - referralCloseReasons?: string[] | undefined; - caseCloseReasons?: string[] | undefined; } export class EffectiveLocationPolicy implements IEffectiveLocationPolicy { @@ -4430,6 +4458,62 @@ export interface ICurrentFeatureFlags { familyScreenPageVersionSwitch: boolean; } +export class PutOrganizationConfigurationPayload implements IPutOrganizationConfigurationPayload { + referralCloseReasons?: string[] | undefined; + caseCloseReasons?: string[] | undefined; + + constructor(data?: IPutOrganizationConfigurationPayload) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + if (Array.isArray(_data["referralCloseReasons"])) { + this.referralCloseReasons = [] as any; + for (let item of _data["referralCloseReasons"]) + this.referralCloseReasons!.push(item); + } + if (Array.isArray(_data["caseCloseReasons"])) { + this.caseCloseReasons = [] as any; + for (let item of _data["caseCloseReasons"]) + this.caseCloseReasons!.push(item); + } + } + } + + static fromJS(data: any): PutOrganizationConfigurationPayload { + data = typeof data === 'object' ? data : {}; + let result = new PutOrganizationConfigurationPayload(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + if (Array.isArray(this.referralCloseReasons)) { + data["referralCloseReasons"] = []; + for (let item of this.referralCloseReasons) + data["referralCloseReasons"].push(item); + } + if (Array.isArray(this.caseCloseReasons)) { + data["caseCloseReasons"] = []; + for (let item of this.caseCloseReasons) + data["caseCloseReasons"].push(item); + } + return data; + } +} + +export interface IPutOrganizationConfigurationPayload { + referralCloseReasons?: string[] | undefined; + caseCloseReasons?: string[] | undefined; +} + export class DocumentUploadInfo implements IDocumentUploadInfo { documentId!: string; valetUrl!: string; diff --git a/src/caretogether-pwa/src/Settings/Locations/Tabs/BasicConfiguration.tsx b/src/caretogether-pwa/src/Settings/Locations/Tabs/BasicConfiguration.tsx index 7831e2e03..39c8f2370 100644 --- a/src/caretogether-pwa/src/Settings/Locations/Tabs/BasicConfiguration.tsx +++ b/src/caretogether-pwa/src/Settings/Locations/Tabs/BasicConfiguration.tsx @@ -8,6 +8,7 @@ import { LocationConfiguration, PutLocationPayload, OrganizationConfiguration, + PutOrganizationConfigurationPayload, } from '../../../GeneratedClient'; import { organizationConfigurationEdited } from '../../../Model/ConfigurationModel'; import { useBackdrop } from '../../../Hooks/useBackdrop'; @@ -57,18 +58,25 @@ export default function BasicConfiguration({ withBackdrop(async () => { const { caseCloseReasons, referralCloseReasons, ...locationData } = data; - const updatedOrgConfig = await api.configuration.putLocationDefinition( + await api.configuration.putLocationDefinition( organizationId, new PutLocationPayload({ locationConfiguration: new LocationConfiguration({ ...currentLocationDefinition, ...locationData, }), - caseCloseReasons, - referralCloseReasons, }) ); + const updatedOrgConfig = + await api.organizationConfiguration.putOrganizationConfiguration( + organizationId, + new PutOrganizationConfigurationPayload({ + caseCloseReasons, + referralCloseReasons, + }) + ); + storeEdits(new OrganizationConfiguration(updatedOrgConfig)); }); }; diff --git a/swagger.json b/swagger.json index 511f302d9..85418f32b 100644 --- a/swagger.json +++ b/swagger.json @@ -321,6 +321,50 @@ } } }, + "/api/{organizationId}/OrganizationConfiguration": { + "put": { + "tags": [ + "OrganizationConfiguration" + ], + "operationId": "OrganizationConfiguration_PutOrganizationConfiguration", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + } + ], + "requestBody": { + "x-name": "payload", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PutOrganizationConfigurationPayload" + } + } + }, + "required": true, + "x-position": 2 + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationConfiguration" + } + } + } + } + } + } + }, "/api/{organizationId}/{locationId}/Files/family/{familyId}/{documentId}": { "get": { "tags": [ @@ -1983,20 +2027,6 @@ "type": "string", "format": "guid", "nullable": true - }, - "referralCloseReasons": { - "type": "array", - "nullable": true, - "items": { - "type": "string" - } - }, - "caseCloseReasons": { - "type": "array", - "nullable": true, - "items": { - "type": "string" - } } } }, @@ -2943,6 +2973,26 @@ } } }, + "PutOrganizationConfigurationPayload": { + "type": "object", + "additionalProperties": false, + "properties": { + "referralCloseReasons": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "caseCloseReasons": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + } + } + }, "DocumentUploadInfo": { "type": "object", "additionalProperties": false,