Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@
"stopAll": true
}
]
}
}
19 changes: 19 additions & 0 deletions src/CareTogether.Api/Controllers/ConfigurationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,25 @@ Guid locationId
return Ok(result);
}

[HttpPut("/api/{organizationId:guid}/{locationId:guid}/[controller]/policy")]
public async Task<ActionResult<EffectiveLocationPolicy>> PutEffectiveLocationPolicy(
Guid organizationId,
Guid locationId,
[FromBody] EffectiveLocationPolicy policy
)
{
if (!User.IsInRole(SystemConstants.ORGANIZATION_ADMINISTRATOR))
return Forbid();

var result = await policiesResource.UpsertEffectiveLocationPolicyAsync(
organizationId,
locationId,
policy
);

return Ok(result);
}

[HttpGet("/api/{organizationId:guid}/{locationId:guid}/[controller]/flags")]
public async Task<ActionResult<CurrentFeatureFlags>> GetLocationFlags(Guid organizationId)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,8 @@ af with
.ToImmutableList(),
},
};

return effectivePolicy;
}
return effectivePolicy;
}

public async Task<OrganizationSecrets> GetOrganizationSecretsAsync(Guid organizationId)
{
Expand Down
6 changes: 6 additions & 0 deletions src/caretogether-pwa/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/caretogether-pwa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@testing-library/react": "^14.3.1",
"@testing-library/user-event": "^14.5.2",
"date-fns": "^3.6.0",
"dayjs": "^1.11.13",
"dexie": "^3.2.7",
"dexie-react-hooks": "^1.1.7",
"posthog-js": "^1.260.1",
Expand Down
44 changes: 44 additions & 0 deletions src/caretogether-pwa/src/GeneratedClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,50 @@ export class ConfigurationClient {
return Promise.resolve<EffectiveLocationPolicy>(null as any);
}

putEffectiveLocationPolicy(organizationId: string, locationId: string, policy: EffectiveLocationPolicy): Promise<EffectiveLocationPolicy> {
let url_ = this.baseUrl + "/api/{organizationId}/{locationId}/Configuration/policy";
if (organizationId === undefined || organizationId === null)
throw new Error("The parameter 'organizationId' must be defined.");
url_ = url_.replace("{organizationId}", encodeURIComponent("" + organizationId));
if (locationId === undefined || locationId === null)
throw new Error("The parameter 'locationId' must be defined.");
url_ = url_.replace("{locationId}", encodeURIComponent("" + locationId));
url_ = url_.replace(/[?&]$/, "");

const content_ = JSON.stringify(policy);

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.processPutEffectiveLocationPolicy(_response);
});
}

protected processPutEffectiveLocationPolicy(response: Response): Promise<EffectiveLocationPolicy> {
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 = EffectiveLocationPolicy.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<EffectiveLocationPolicy>(null as any);
}

getLocationFlags(organizationId: string, locationId: string): Promise<CurrentFeatureFlags> {
let url_ = this.baseUrl + "/api/{organizationId}/{locationId}/Configuration/flags";
if (organizationId === undefined || organizationId === null)
Expand Down
12 changes: 10 additions & 2 deletions src/caretogether-pwa/src/Model/ConfigurationModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
OrganizationConfiguration,
RequirementStage,
VolunteerFamilyRequirementScope,
EffectiveLocationPolicy,
} from '../GeneratedClient';
import { useLoadable } from '../Hooks/useLoadable';
import { api } from '../Api/Api';
Expand All @@ -15,6 +16,12 @@ export const organizationConfigurationEdited =
default: null,
});

export const effectiveLocationPolicyEdited =
atom<EffectiveLocationPolicy | null>({
key: 'effectiveLocationPolicyEdited',
default: null,
});

export type ExtendedOrganizationConfiguration = OrganizationConfiguration & {
availableTimeZones?: string[];
ethnicities?: string[];
Expand Down Expand Up @@ -70,12 +77,13 @@ export const adultFamilyRelationshipsData = selector({
export const policyData = selector({
key: 'policyData',
get: async ({ get }) => {
const edited = get(effectiveLocationPolicyEdited);
if (edited) return edited;
const { organizationId, locationId } = get(selectedLocationContextState);
const dataResponse = await api.configuration.getEffectiveLocationPolicy(
return await api.configuration.getEffectiveLocationPolicy(
organizationId,
locationId
);
return dataResponse;
},
});

Expand Down
16 changes: 15 additions & 1 deletion src/caretogether-pwa/src/Settings/Locations/LocationEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
} from '@mui/material';
import { useState, useEffect, useMemo } from 'react';
import { useLoadable } from '../../Hooks/useLoadable';
import { organizationConfigurationQuery } from '../../Model/ConfigurationModel';
import {
organizationConfigurationQuery,
policyData,
} from '../../Model/ConfigurationModel';
import { ProgressBackdrop } from '../../Shell/ProgressBackdrop';
import { useScreenTitle } from '../../Shell/ShellScreenTitle';
import { useRecoilValue } from 'recoil';
Expand Down Expand Up @@ -41,6 +44,11 @@ export function LocationEdit() {
(location) => location.id === editingLocationId
);

const effectiveLocationPolicy = useRecoilValue(policyData);

// TODO: Use this to implement the other tabs
console.log({ effectiveLocationPolicy });

useScreenTitle(`Editing ${location?.name} configuration`);

const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
Expand Down Expand Up @@ -221,6 +229,12 @@ export function LocationEdit() {
</Box>
)}

{activeTab === 'actions' && (
<Box key="actions">
<ActionDefinitions />
</Box>
)}

{activeTab === 'accessLevels' && (
<Box key="accessLevels">
<AccessLevels locationConfiguration={location} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,171 @@
import { Typography } from '@mui/material';
import {
Box,
Button,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Typography,
Paper,
Link,
TablePagination,
Drawer,
InputAdornment,
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import { useState, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { policyData } from '../../../Model/ConfigurationModel';
import { AddActionDefinition } from './AddActionDefinition';

const requirementLabel: Record<number, string> = {
0: 'None',
1: 'Allowed',
2: 'Required',
};

function truncate(text?: string | null, length = 40) {
if (!text) return '-';
return text.length <= length ? text : text.slice(0, length) + '…';
}

function formatValidity(value?: string | null) {
if (!value) return '-';
const [days] = value.split('.');
return `${days} days`;
}

export default function ActionDefinitions() {
return <Typography variant="body1">Action Definitions</Typography>;
const effectiveLocationPolicy = useRecoilValue(policyData);
const actionDefinitions = effectiveLocationPolicy?.actionDefinitions;

const [searchTerm, setSearchTerm] = useState('');
const [page, setPage] = useState(0);
const rowsPerPage = 5;
const [openDrawer, setOpenDrawer] = useState(false);

const entries = Object.entries(actionDefinitions ?? {});

const filtered = useMemo(
() =>
entries.filter(([name]) =>
name.toLowerCase().includes(searchTerm.toLowerCase())
),
[entries, searchTerm]
);

const paginated = filtered.slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
);

if (!actionDefinitions) {
return <Typography>No action definitions found.</Typography>;
}

return (
<Box>
<Typography variant="h6" sx={{ mb: 1 }}>
Action Definitions
</Typography>

<TextField
placeholder="Search..."
size="small"
fullWidth
sx={{ maxWidth: 300 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>

<TableContainer component={Paper} sx={{ mt: 2 }}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Document Link</TableCell>
<TableCell>Note</TableCell>
<TableCell>Instructions</TableCell>
<TableCell>Info Link</TableCell>
<TableCell>Validity</TableCell>
</TableRow>
</TableHead>

<TableBody>
{paginated.map(([name, def]) => (
<TableRow key={name}>
<TableCell>
{name}

{def.alternateNames && def.alternateNames.length > 0 && (
<Typography
variant="body2"
color="text.secondary"
sx={{ display: 'block', mt: 0.5 }}
>
{def.alternateNames.join(', ')}
</Typography>
)}
</TableCell>

<TableCell>{requirementLabel[def.documentLink]}</TableCell>
<TableCell>{requirementLabel[def.noteEntry]}</TableCell>
<TableCell>{truncate(def.instructions)}</TableCell>
<TableCell>
{def.infoLink ? (
<Link
href={def.infoLink}
target="_blank"
rel="noopener noreferrer"
>
{def.infoLink}
</Link>
) : (
'-'
)}
</TableCell>
<TableCell>{formatValidity(def.validity)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>

<TablePagination
component="div"
count={filtered.length}
page={page}
onPageChange={(_, newPage) => setPage(newPage)}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[]}
/>

<Stack direction="row" spacing={2} sx={{ mt: 2 }}>
<Button variant="contained" onClick={() => setOpenDrawer(true)}>
Add new action definition
</Button>
</Stack>

<Drawer
anchor="right"
open={openDrawer}
onClose={() => setOpenDrawer(false)}
>
<Box sx={{ width: 500, padding: 3, pt: 7 }}>
<AddActionDefinition onClose={() => setOpenDrawer(false)} />
</Box>
</Drawer>
</Box>
);
}
Loading
Loading