Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete family #727

Merged
merged 6 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/CareTogether.Api/Controllers/RecordsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ public async Task<ActionResult<IEnumerable<RecordsAggregate>>> ListVisibleAggreg
}

[HttpPost("atomicRecordsCommand")]
public async Task<ActionResult<RecordsAggregate>> SubmitAtomicRecordsCommandAsync(Guid organizationId, Guid locationId,
public async Task<ActionResult<RecordsAggregate?>> SubmitAtomicRecordsCommandAsync(Guid organizationId, Guid locationId,
[FromBody] AtomicRecordsCommand command)
{
var result = await recordsManager.ExecuteAtomicRecordsCommandAsync(organizationId, locationId, User, command);
return Ok(result);
}

[HttpPost("compositeRecordsCommand")]
public async Task<ActionResult<RecordsAggregate>> SubmitCompositeRecordsCommandAsync(Guid organizationId, Guid locationId,
public async Task<ActionResult<RecordsAggregate?>> SubmitCompositeRecordsCommandAsync(Guid organizationId, Guid locationId,
[FromBody] CompositeRecordsCommand command)
{
var result = await recordsManager.ExecuteCompositeRecordsCommand(organizationId, locationId, User, command);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ public async Task<bool> AuthorizeFamilyCommandAsync(
return permissions.Contains(command switch
{
CreateFamily => Permission.EditFamilyInfo,
UndoCreateFamily => Permission.EditFamilyInfo,
AddAdultToFamily => Permission.EditFamilyInfo,
AddChildToFamily => Permission.EditFamilyInfo,
ConvertChildToAdult => Permission.EditFamilyInfo,
Expand Down
14 changes: 13 additions & 1 deletion src/CareTogether.Core/Managers/CombinedFamilyInfoFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public CombinedFamilyInfoFormatter(IPolicyEvaluationEngine policyEvaluationEngin
}


public async Task<CombinedFamilyInfo> RenderCombinedFamilyInfoAsync(Guid organizationId, Guid locationId,
public async Task<CombinedFamilyInfo?> RenderCombinedFamilyInfoAsync(Guid organizationId, Guid locationId,
Guid familyId, ClaimsPrincipal user)
{
var locationPolicy = await policiesResource.GetCurrentPolicy(organizationId, locationId);
Expand All @@ -52,6 +52,18 @@ public async Task<CombinedFamilyInfo> RenderCombinedFamilyInfoAsync(Guid organiz
if (family == null)
throw new InvalidOperationException("The specified family ID was not found.");

// Exclude soft-deleted families and individuals (i.e., those marked as 'inactive').
// Note that this is different from the 'inactive' role removal reason.
// A potential 'undelete' feature could be implemented that involves checking for a
// special "View Deleted" permission to bypass this step.
if (!family.Active)
return null;
family = family with
{
Adults = family.Adults.Where(adult => adult.Item1.Active).ToImmutableList(),
Children = family.Children.Where(child => child.Active).ToImmutableList()
};

var missingCustomFamilyFields = locationPolicy.CustomFamilyFields
.Where(customField => !family.CompletedCustomFields.Any(completed => completed.CustomFieldName == customField.Name))
.Select(customField => customField.Name)
Expand Down
16 changes: 8 additions & 8 deletions src/CareTogether.Core/Managers/Membership/MembershipManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public async Task<UserAccess> GetUserAccessAsync(ClaimsPrincipal user)
var organizationsAccess = await Task.WhenAll(account.Organizations.Select(async organization =>
{
var organizationId = organization.OrganizationId;

var locationsAccess = await Task.WhenAll(organization.Locations
.Select(async location =>
{
Expand All @@ -65,7 +65,7 @@ public async Task<UserAccess> GetUserAccessAsync(ClaimsPrincipal user)

return new UserAccess(user.UserId(), organizationsAccess.ToImmutableList());
}

public async Task<FamilyRecordsAggregate> ChangePersonRolesAsync(ClaimsPrincipal user,
Guid organizationId, Guid locationId, Guid personId, ImmutableList<string> roles)
{
Expand All @@ -81,14 +81,14 @@ public async Task<FamilyRecordsAggregate> ChangePersonRolesAsync(ClaimsPrincipal
//NOTE: This invariant could be revisited, but that would split 'Person' and 'Family' into separate aggregates.
if (personFamily == null)
throw new Exception("CareTogether currently assumes that all people should (always) belong to a family record.");

var result = await accountsResource.ExecutePersonAccessCommandAsync(
organizationId, locationId, command, user.UserId());

var familyResult = await combinedFamilyInfoFormatter.RenderCombinedFamilyInfoAsync(
organizationId, locationId, personFamily.Id, user);

return new FamilyRecordsAggregate(familyResult);
return new FamilyRecordsAggregate(familyResult!);
}

public async Task<byte[]> GenerateUserInviteNonceAsync(ClaimsPrincipal user,
Expand All @@ -100,7 +100,7 @@ public async Task<byte[]> GenerateUserInviteNonceAsync(ClaimsPrincipal user,

var result = await accountsResource.GenerateUserInviteNonceAsync(
organizationId, locationId, personId, user.UserId());

return result;
}

Expand All @@ -112,10 +112,10 @@ public async Task<byte[]> GenerateUserInviteNonceAsync(ClaimsPrincipal user,

var locationAccess = await accountsResource.TryLookupUserInviteNoncePersonIdAsync(
organizationId, locationId, nonce);

if (locationAccess == null)
return null;

var family = await directoryResource.FindPersonFamilyAsync(organizationId, locationId, locationAccess.PersonId);
if (family == null)
return null;
Expand All @@ -135,7 +135,7 @@ public async Task<byte[]> GenerateUserInviteNonceAsync(ClaimsPrincipal user,
{
var result = await accountsResource.TryRedeemUserInviteNonceAsync(
organizationId, locationId, user.UserId(), nonce);

return result;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/CareTogether.Core/Managers/Records/IRecordsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ Task<ImmutableList<RecordsAggregate>> ListVisibleAggregatesAsync(
ClaimsPrincipal user, Guid organizationId, Guid locationId);

//TODO: Support returning *multiple* aggregates to upsert
Task<RecordsAggregate> ExecuteCompositeRecordsCommand(Guid organizationId, Guid locationId,
Task<RecordsAggregate?> ExecuteCompositeRecordsCommand(Guid organizationId, Guid locationId,
ClaimsPrincipal user, CompositeRecordsCommand command);

//TODO: Support returning *multiple* aggregates to upsert
Task<RecordsAggregate> ExecuteAtomicRecordsCommandAsync(Guid organizationId, Guid locationId,
Task<RecordsAggregate?> ExecuteAtomicRecordsCommandAsync(Guid organizationId, Guid locationId,
ClaimsPrincipal user, AtomicRecordsCommand command);

Task<Uri> GetFamilyDocumentReadValetUrl(Guid organizationId, Guid locationId,
Expand Down
21 changes: 13 additions & 8 deletions src/CareTogether.Core/Managers/Records/RecordsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using CareTogether.Resources.Notes;
using CareTogether.Resources.Referrals;
using Nito.AsyncEx;
using Nito.Disposables.Internals;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
Expand Down Expand Up @@ -63,14 +64,18 @@ public async Task<ImmutableList<RecordsAggregate>> ListVisibleAggregatesAsync(Cl
.Select(x => x.family)
.ToImmutableList();

var renderedFamilies = await visibleFamilies
var renderedFamilies = (await visibleFamilies
.Select(async family =>
{
var renderedFamily = await combinedFamilyInfoFormatter.RenderCombinedFamilyInfoAsync(
organizationId, locationId, family.Id, user);
if (renderedFamily == null)
return null;
return new FamilyRecordsAggregate(renderedFamily);
})
.WhenAll();
.WhenAll())
.WhereNotNull()
.ToImmutableList();

var communities = await communitiesResource.ListLocationCommunitiesAsync(organizationId, locationId);

Expand Down Expand Up @@ -101,7 +106,7 @@ public async Task<ImmutableList<RecordsAggregate>> ListVisibleAggregatesAsync(Cl
.ToImmutableList();
}

public async Task<RecordsAggregate> ExecuteCompositeRecordsCommand(Guid organizationId, Guid locationId,
public async Task<RecordsAggregate?> ExecuteCompositeRecordsCommand(Guid organizationId, Guid locationId,
ClaimsPrincipal user, CompositeRecordsCommand command)
{
var atomicCommands = GenerateAtomicCommandsForCompositeCommand(command).ToImmutableList();
Expand All @@ -116,10 +121,10 @@ public async Task<RecordsAggregate> ExecuteCompositeRecordsCommand(Guid organiza
var familyResult = await combinedFamilyInfoFormatter.RenderCombinedFamilyInfoAsync(
organizationId, locationId, command.FamilyId, user);

return new FamilyRecordsAggregate(familyResult);
return familyResult == null ? null : new FamilyRecordsAggregate(familyResult);
}

public async Task<RecordsAggregate> ExecuteAtomicRecordsCommandAsync(Guid organizationId, Guid locationId,
public async Task<RecordsAggregate?> ExecuteAtomicRecordsCommandAsync(Guid organizationId, Guid locationId,
ClaimsPrincipal user, AtomicRecordsCommand command)
{
if (!await AuthorizeCommandAsync(organizationId, locationId, user, command))
Expand All @@ -141,7 +146,7 @@ public async Task<Uri> GetFamilyDocumentReadValetUrl(Guid organizationId, Guid l

var valetUrl = await directoryResource.GetFamilyDocumentReadValetUrl(organizationId, locationId,
familyId, documentId);

return valetUrl;
}

Expand Down Expand Up @@ -323,7 +328,7 @@ private Task ExecuteCommandAsync(Guid organizationId, Guid locationId,
$"The command type '{command.GetType().FullName}' has not been implemented.")
};

private async Task<RecordsAggregate> RenderCommandResultAsync(Guid organizationId, Guid locationId,
private async Task<RecordsAggregate?> RenderCommandResultAsync(Guid organizationId, Guid locationId,
ClaimsPrincipal user, AtomicRecordsCommand command)
{
if (command is CommunityRecordsCommand c)
Expand All @@ -348,7 +353,7 @@ private async Task<RecordsAggregate> RenderCommandResultAsync(Guid organizationI
var familyResult = await combinedFamilyInfoFormatter.RenderCombinedFamilyInfoAsync(
organizationId, locationId, familyId, user);

return new FamilyRecordsAggregate(familyResult);
return familyResult == null ? null : new FamilyRecordsAggregate(familyResult);
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/CareTogether.Core/Resources/Directory/DirectoryModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public sealed record PersonCommandExecuted(Guid UserId, DateTime TimestampUtc,

public sealed class DirectoryModel
{
internal record FamilyEntry(Guid Id, Guid PrimaryFamilyContactPersonId,
internal record FamilyEntry(Guid Id, bool Active, Guid PrimaryFamilyContactPersonId,
ImmutableDictionary<Guid, FamilyAdultRelationshipInfo> AdultRelationships,
ImmutableList<Guid> Children,
ImmutableDictionary<(Guid ChildId, Guid AdultId), CustodialRelationshipType> CustodialRelationships,
Expand All @@ -26,7 +26,7 @@ internal record FamilyEntry(Guid Id, Guid PrimaryFamilyContactPersonId,
ImmutableList<Activity> History)
{
internal Family ToFamily(ImmutableDictionary<Guid, PersonEntry> people) =>
new(Id, PrimaryFamilyContactPersonId,
new(Id, Active, PrimaryFamilyContactPersonId,
AdultRelationships.Select(ar => (people[ar.Key].ToPerson(), ar.Value)).ToImmutableList(),
Children.Select(c => people[c].ToPerson()).ToImmutableList(),
CustodialRelationships.Select(cr => new CustodialRelationship(cr.Key.ChildId, cr.Key.AdultId, cr.Value)).ToImmutableList(),
Expand Down Expand Up @@ -73,7 +73,7 @@ public static async Task<DirectoryModel> InitializeAsync(
{
var familyEntryToUpsert = command switch
{
CreateFamily c => new FamilyEntry(c.FamilyId, c.PrimaryFamilyContactPersonId,
CreateFamily c => new FamilyEntry(c.FamilyId, Active: true, c.PrimaryFamilyContactPersonId,
AdultRelationships: ImmutableDictionary<Guid, FamilyAdultRelationshipInfo>.Empty.AddRange(
c.Adults?.Select(a => new KeyValuePair<Guid, FamilyAdultRelationshipInfo>(a.Item1, a.Item2))
?? new List<KeyValuePair<Guid, FamilyAdultRelationshipInfo>>()),
Expand All @@ -88,6 +88,7 @@ public static async Task<DirectoryModel> InitializeAsync(
_ => families.TryGetValue(command.FamilyId, out var familyEntry)
? command switch
{
UndoCreateFamily c => familyEntry with { Active = false },
//TODO: Error if key already exists
//TODO: Error if person is not found
AddAdultToFamily c => familyEntry with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace CareTogether.Resources.Directory
{
public sealed record Family(Guid Id, Guid PrimaryFamilyContactPersonId,
public sealed record Family(Guid Id, bool Active, Guid PrimaryFamilyContactPersonId,
ImmutableList<(Person, FamilyAdultRelationshipInfo)> Adults,
ImmutableList<Person> Children,
ImmutableList<CustodialRelationship> CustodialRelationships,
Expand Down Expand Up @@ -50,6 +50,8 @@ public sealed record CreateFamily(Guid FamilyId, Guid PrimaryFamilyContactPerson
ImmutableList<Guid> Children,
ImmutableList<CustodialRelationship> CustodialRelationships)
: FamilyCommand(FamilyId);
public sealed record UndoCreateFamily(Guid FamilyId)
: FamilyCommand(FamilyId);
public sealed record AddAdultToFamily(Guid FamilyId, Guid AdultPersonId,
FamilyAdultRelationshipInfo RelationshipToFamily)
: FamilyCommand(FamilyId);
Expand Down
Binary file added src/caretogether-pwa/src/Engagement/waldo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions src/caretogether-pwa/src/Families/DeleteFamilyDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useDirectoryModel, useFamilyLookup } from '../Model/DirectoryModel';
import { DialogHandle } from '../Hooks/useDialogHandle';
import { UpdateDialog } from '../Generic/UpdateDialog';
import { familyNameString } from './FamilyName';
import waldoUrl from '../Engagement/waldo.png';
import { useAppNavigate } from '../Hooks/useAppNavigate';

interface DeleteFamilyDialogProps {
familyId: string,
handle: DialogHandle
}

export function DeleteFamilyDialog({ familyId, handle }: DeleteFamilyDialogProps) {
const directoryModel = useDirectoryModel();
const familyLookup = useFamilyLookup();
const appNavigate = useAppNavigate();

const family = familyLookup(familyId);

async function save() {
appNavigate.dashboard();
await directoryModel.undoCreateFamily(familyId);
}

return (
<UpdateDialog title={`Are you sure you want to delete the ${familyNameString(family)}?`}
onClose={handle.closeDialog} onSave={save}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<img src={waldoUrl} alt="Waldo" style={{ width: '15%', position: 'absolute', bottom: 0, left: '25%' }} />
</div>
</UpdateDialog>
);
}
2 changes: 1 addition & 1 deletion src/caretogether-pwa/src/Families/EditAdultDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function EditAdultDialog({ handle, adult }: EditAdultDialogProps) {
const permissions = useFamilyIdPermissions(familyId!);

return (
<Dialog open={handle.open} onClose={(event: object | undefined, reason: string) => !isBackdropClick(reason) ? handle.closeDialog : ({})}
<Dialog open={handle.open} onClose={(_event: object | undefined, reason: string) => !isBackdropClick(reason) ? handle.closeDialog : ({})}
fullWidth scroll='body' aria-labelledby="edit-adult-title">
<DialogTitle id="edit-adult-title">
Edit Adult
Expand Down
29 changes: 20 additions & 9 deletions src/caretogether-pwa/src/Families/FamilyScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import { FamilyCustomField } from './FamilyCustomField';
import { useFilterMenu } from '../Generic/useFilterMenu';
import { FilterMenu } from '../Generic/FilterMenu';
import { isBackdropClick } from '../Utilities/handleBackdropClick';
import { DeleteFamilyDialog } from './DeleteFamilyDialog';
import { useDialogHandle } from '../Hooks/useDialogHandle';

const sortArrangementsByStartDateDescThenCreateDateDesc = (a: Arrangement, b: Arrangement) => {
return ((b.startedAtUtc ?? new Date()).getTime() - (a.startedAtUtc ?? new Date()).getTime()) ||
Expand Down Expand Up @@ -69,6 +71,8 @@ export function FamilyScreen() {
const [addChildDialogOpen, setAddChildDialogOpen] = useState(false);
const [addNoteDialogOpen, setAddNoteDialogOpen] = useState(false);

const deleteFamilyDialogHandle = useDialogHandle();

const [familyMoreMenuAnchor, setFamilyMoreMenuAnchor] = useState<Element | null>(null);

const participatingFamilyRoles =
Expand Down Expand Up @@ -179,10 +183,11 @@ export function FamilyScreen() {
startIcon={<AddCircleIcon />}>
Note
</Button>}
{permissions(Permission.EditVolunteerRoleParticipation) &&
{((permissions(Permission.EditVolunteerRoleParticipation) &&
(participatingFamilyRoles.length > 0 ||
(family.volunteerFamilyInfo?.roleRemovals &&
family.volunteerFamilyInfo.roleRemovals.length > 0)) &&
family.volunteerFamilyInfo.roleRemovals.length > 0))) || (
permissions(Permission.EditFamilyInfo))) &&
<IconButton
onClick={(event) => setFamilyMoreMenuAnchor(event.currentTarget)}
size="large">
Expand All @@ -207,23 +212,29 @@ export function FamilyScreen() {
<ListItemText primary={`Reset ${removedRole.roleName} participation`} />
</MenuItem>
))}
{permissions(Permission.EditFamilyInfo) &&
<MenuItem onClick={deleteFamilyDialogHandle.openDialog}>
<ListItemText primary="Delete family" />
</MenuItem>}
</MenuList>
</Menu>
{uploadDocumentDialogOpen && <UploadFamilyDocumentsDialog family={family}
onClose={() => setUploadDocumentDialogOpen(false)} />}
{addAdultDialogOpen && <AddAdultDialog
onClose={(event: object | undefined, reason: string) => !isBackdropClick(reason) ? setAddAdultDialogOpen(false) : ({})}
></AddAdultDialog>
}
{addChildDialogOpen && <AddChildDialog
onClose={(event: object | undefined, reason: string) => !isBackdropClick(reason) ? setAddChildDialogOpen(false) : ({})}
/>}
{addAdultDialogOpen && <AddAdultDialog
onClose={(_event: object | undefined, reason: string) => !isBackdropClick(reason) ? setAddAdultDialogOpen(false) : ({})}
></AddAdultDialog>
}
{addChildDialogOpen && <AddChildDialog
onClose={(_event: object | undefined, reason: string) => !isBackdropClick(reason) ? setAddChildDialogOpen(false) : ({})}
/>}
{addNoteDialogOpen && <AddEditNoteDialog familyId={family.family!.id!} onClose={() => setAddNoteDialogOpen(false)} />}
{(removeRoleParameter && <RemoveFamilyRoleDialog volunteerFamilyId={familyId} role={removeRoleParameter.role}
onClose={() => setRemoveRoleParameter(null)} />) || null}
{(resetRoleParameter && <ResetFamilyRoleDialog volunteerFamilyId={familyId} role={resetRoleParameter.role}
removalReason={resetRoleParameter.removalReason} removalAdditionalComments={resetRoleParameter.removalAdditionalComments}
onClose={() => setResetRoleParameter(null)} />) || null}
{deleteFamilyDialogHandle.open && <DeleteFamilyDialog key={deleteFamilyDialogHandle.key}
handle={deleteFamilyDialogHandle} familyId={familyId} />}
</Toolbar>
<Grid container spacing={0}>
<Grid item container xs={12} md={4} spacing={0}>
Expand Down
Loading
Loading