From f52ecddbfd49567d36fe774c09c56147c2d4c2cb Mon Sep 17 00:00:00 2001 From: PabloDinella Date: Tue, 1 Jul 2025 12:59:07 -0300 Subject: [PATCH 1/4] Add policy data retrieval to LocationEdit component --- .../src/Settings/Locations/LocationEdit.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/caretogether-pwa/src/Settings/Locations/LocationEdit.tsx b/src/caretogether-pwa/src/Settings/Locations/LocationEdit.tsx index a3dbbf699..136188f9a 100644 --- a/src/caretogether-pwa/src/Settings/Locations/LocationEdit.tsx +++ b/src/caretogether-pwa/src/Settings/Locations/LocationEdit.tsx @@ -10,7 +10,10 @@ import { } from '@mui/material'; import { useState, useEffect } 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'; @@ -39,6 +42,11 @@ export function LocationEdit() { (location) => location.id === locationId ); + const effectiveLocationPolicy = useRecoilValue(policyData); + + // TODO: Use this to implement the other tabs + console.log({ effectiveLocationPolicy }); + useScreenTitle(`Editing ${location?.name} configuration`); const showActionsTab = useFeatureFlagEnabled('actionDefinitionsTab'); From 09e042733b471d54ac545aafd4393c00a4fc260f Mon Sep 17 00:00:00 2001 From: Rabi <134292357+Rabi94@users.noreply.github.com> Date: Wed, 2 Jul 2025 17:50:16 -0300 Subject: [PATCH 2/4] WIP --- src/caretogether-pwa/package-lock.json | 6 + src/caretogether-pwa/package.json | 1 + src/caretogether-pwa/src/GeneratedClient.ts | 108 ++++++++++++ .../Locations/Tabs/ActionDefinitions.tsx | 154 +++++++++++++++- .../Locations/Tabs/AddActionDefinition.tsx | 164 ++++++++++++++++++ swagger.json | 88 ++++++++-- 6 files changed, 508 insertions(+), 13 deletions(-) create mode 100644 src/caretogether-pwa/src/Settings/Locations/Tabs/AddActionDefinition.tsx diff --git a/src/caretogether-pwa/package-lock.json b/src/caretogether-pwa/package-lock.json index ac0f45d53..4bc8a8aa2 100644 --- a/src/caretogether-pwa/package-lock.json +++ b/src/caretogether-pwa/package-lock.json @@ -28,6 +28,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.215.5", @@ -2973,6 +2974,11 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/src/caretogether-pwa/package.json b/src/caretogether-pwa/package.json index 70a01ecbb..14816e47c 100644 --- a/src/caretogether-pwa/package.json +++ b/src/caretogether-pwa/package.json @@ -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.215.5", diff --git a/src/caretogether-pwa/src/GeneratedClient.ts b/src/caretogether-pwa/src/GeneratedClient.ts index 5cc229df0..2e67f0658 100644 --- a/src/caretogether-pwa/src/GeneratedClient.ts +++ b/src/caretogether-pwa/src/GeneratedClient.ts @@ -1034,6 +1034,10 @@ export class MetadataClient { this.baseUrl = baseUrl ?? ""; } + /** + * Generates the OData $metadata document. + * @return The IEdmModel representing $metadata. + */ getMetadata(): Promise { let url_ = this.baseUrl + "/api/odata/live/$metadata"; url_ = url_.replace(/[?&]$/, ""); @@ -1068,6 +1072,10 @@ export class MetadataClient { return Promise.resolve(null as any); } + /** + * Generates the OData service document. + * @return The service document for the service. + */ getServiceDocument(): Promise { let url_ = this.baseUrl + "/api/odata/live"; url_ = url_.replace(/[?&]$/, ""); @@ -13031,12 +13039,19 @@ export interface IAccountLocationAccess { roles?: string[]; } +/** Semantic representation of an EDM model. */ export abstract class IEdmModel implements IIEdmModel { + /** Gets the collection of schema elements that are contained in this model. */ schemaElements?: IEdmSchemaElement[] | undefined; + /** Gets the collection of vocabulary annotations that are contained in this model. */ vocabularyAnnotations?: IEdmVocabularyAnnotation[] | undefined; + /** Gets the collection of models referred to by this model (mainly by the this.References). */ referencedModels?: IEdmModel[] | undefined; + /** Gets the collection of namespaces that schema elements use contained in this model. */ declaredNamespaces?: string[] | undefined; + /** Gets the model's annotations manager. */ directValueAnnotationsManager?: IEdmDirectValueAnnotationsManager | undefined; + /** Gets the only one entity container of the model. */ entityContainer?: IEdmEntityContainer | undefined; constructor(data?: IIEdmModel) { @@ -13108,17 +13123,27 @@ export abstract class IEdmModel implements IIEdmModel { } } +/** Semantic representation of an EDM model. */ export interface IIEdmModel { + /** Gets the collection of schema elements that are contained in this model. */ schemaElements?: IEdmSchemaElement[] | undefined; + /** Gets the collection of vocabulary annotations that are contained in this model. */ vocabularyAnnotations?: IEdmVocabularyAnnotation[] | undefined; + /** Gets the collection of models referred to by this model (mainly by the this.References). */ referencedModels?: IEdmModel[] | undefined; + /** Gets the collection of namespaces that schema elements use contained in this model. */ declaredNamespaces?: string[] | undefined; + /** Gets the model's annotations manager. */ directValueAnnotationsManager?: IEdmDirectValueAnnotationsManager | undefined; + /** Gets the only one entity container of the model. */ entityContainer?: IEdmEntityContainer | undefined; } +/** Common base interface for all named children of EDM schema. */ export abstract class IEdmSchemaElement implements IIEdmSchemaElement { + /** Gets the kind of this schema element. */ schemaElementKind?: EdmSchemaElementKind; + /** Gets the namespace this schema element belongs to. */ namespace?: string | undefined; constructor(data?: IIEdmSchemaElement) { @@ -13150,11 +13175,15 @@ export abstract class IEdmSchemaElement implements IIEdmSchemaElement { } } +/** Common base interface for all named children of EDM schema. */ export interface IIEdmSchemaElement { + /** Gets the kind of this schema element. */ schemaElementKind?: EdmSchemaElementKind; + /** Gets the namespace this schema element belongs to. */ namespace?: string | undefined; } +/** Defines EDM schema element types. */ export enum EdmSchemaElementKind { None = 0, TypeDefinition = 1, @@ -13164,10 +13193,15 @@ export enum EdmSchemaElementKind { Function = 5, } +/** Represents an EDM vocabulary annotation. */ export abstract class IEdmVocabularyAnnotation implements IIEdmVocabularyAnnotation { + /** Gets the qualifier used to discriminate between multiple bindings of the same property or type. */ qualifier?: string | undefined; + /** Gets the term bound by the annotation. */ term?: IEdmTerm | undefined; + /** Gets the element the annotation applies to. */ target?: IEdmVocabularyAnnotatable | undefined; + /** Gets the expression producing the value of the annotation. */ value?: IEdmExpression | undefined; constructor(data?: IIEdmVocabularyAnnotation) { @@ -13203,16 +13237,25 @@ export abstract class IEdmVocabularyAnnotation implements IIEdmVocabularyAnnotat } } +/** Represents an EDM vocabulary annotation. */ export interface IIEdmVocabularyAnnotation { + /** Gets the qualifier used to discriminate between multiple bindings of the same property or type. */ qualifier?: string | undefined; + /** Gets the term bound by the annotation. */ term?: IEdmTerm | undefined; + /** Gets the element the annotation applies to. */ target?: IEdmVocabularyAnnotatable | undefined; + /** Gets the expression producing the value of the annotation. */ value?: IEdmExpression | undefined; } +/** Represents an EDM term. */ export abstract class IEdmTerm implements IIEdmTerm { + /** Gets the type of this term. */ type?: IEdmTypeReference | undefined; + /** Gets the AppliesTo of this term. */ appliesTo?: string | undefined; + /** Gets the DefaultValue of this term. */ defaultValue?: string | undefined; constructor(data?: IIEdmTerm) { @@ -13246,14 +13289,21 @@ export abstract class IEdmTerm implements IIEdmTerm { } } +/** Represents an EDM term. */ export interface IIEdmTerm { + /** Gets the type of this term. */ type?: IEdmTypeReference | undefined; + /** Gets the AppliesTo of this term. */ appliesTo?: string | undefined; + /** Gets the DefaultValue of this term. */ defaultValue?: string | undefined; } +/** Represents a references to a type. */ export abstract class IEdmTypeReference implements IIEdmTypeReference { + /** Gets a value indicating whether this type is nullable. */ isNullable?: boolean; + /** Gets the definition to which this type refers. */ definition?: IEdmType | undefined; constructor(data?: IIEdmTypeReference) { @@ -13285,12 +13335,17 @@ export abstract class IEdmTypeReference implements IIEdmTypeReference { } } +/** Represents a references to a type. */ export interface IIEdmTypeReference { + /** Gets a value indicating whether this type is nullable. */ isNullable?: boolean; + /** Gets the definition to which this type refers. */ definition?: IEdmType | undefined; } +/** Represents the definition of an EDM type. */ export abstract class IEdmType implements IIEdmType { + /** Gets the kind of this type. */ typeKind?: EdmTypeKind; constructor(data?: IIEdmType) { @@ -13320,10 +13375,13 @@ export abstract class IEdmType implements IIEdmType { } } +/** Represents the definition of an EDM type. */ export interface IIEdmType { + /** Gets the kind of this type. */ typeKind?: EdmTypeKind; } +/** Defines EDM metatypes. */ export enum EdmTypeKind { None = 0, Primitive = 1, @@ -13337,6 +13395,7 @@ export enum EdmTypeKind { Path = 9, } +/** Represents an element that can be targeted by Vocabulary Annotations */ export abstract class IEdmVocabularyAnnotatable implements IIEdmVocabularyAnnotatable { constructor(data?: IIEdmVocabularyAnnotatable) { @@ -13362,10 +13421,13 @@ export abstract class IEdmVocabularyAnnotatable implements IIEdmVocabularyAnnota } } +/** Represents an element that can be targeted by Vocabulary Annotations */ export interface IIEdmVocabularyAnnotatable { } +/** Represents an EDM expression. */ export abstract class IEdmExpression implements IIEdmExpression { + /** Gets the kind of this expression. */ expressionKind?: EdmExpressionKind; constructor(data?: IIEdmExpression) { @@ -13395,10 +13457,13 @@ export abstract class IEdmExpression implements IIEdmExpression { } } +/** Represents an EDM expression. */ export interface IIEdmExpression { + /** Gets the kind of this expression. */ expressionKind?: EdmExpressionKind; } +/** Defines EDM expression kinds. */ export enum EdmExpressionKind { None = 0, BinaryConstant = 1, @@ -13428,6 +13493,7 @@ export enum EdmExpressionKind { AnnotationPath = 25, } +/** Manages getting and setting direct annotations on EDM elements. */ export abstract class IEdmDirectValueAnnotationsManager implements IIEdmDirectValueAnnotationsManager { constructor(data?: IIEdmDirectValueAnnotationsManager) { @@ -13453,10 +13519,13 @@ export abstract class IEdmDirectValueAnnotationsManager implements IIEdmDirectVa } } +/** Manages getting and setting direct annotations on EDM elements. */ export interface IIEdmDirectValueAnnotationsManager { } +/** Represents an EDM entity container. */ export abstract class IEdmEntityContainer implements IIEdmEntityContainer { + /** Gets a collection of the elements of this entity container. */ elements?: IEdmEntityContainerElement[] | undefined; constructor(data?: IIEdmEntityContainer) { @@ -13494,12 +13563,17 @@ export abstract class IEdmEntityContainer implements IIEdmEntityContainer { } } +/** Represents an EDM entity container. */ export interface IIEdmEntityContainer { + /** Gets a collection of the elements of this entity container. */ elements?: IEdmEntityContainerElement[] | undefined; } +/** Represents the common elements of all EDM entity container elements. */ export abstract class IEdmEntityContainerElement implements IIEdmEntityContainerElement { + /** Gets the kind of element of this container element. */ containerElementKind?: EdmContainerElementKind; + /** Gets the container that contains this element. */ container?: IEdmEntityContainer | undefined; constructor(data?: IIEdmEntityContainerElement) { @@ -13531,11 +13605,15 @@ export abstract class IEdmEntityContainerElement implements IIEdmEntityContainer } } +/** Represents the common elements of all EDM entity container elements. */ export interface IIEdmEntityContainerElement { + /** Gets the kind of element of this container element. */ containerElementKind?: EdmContainerElementKind; + /** Gets the container that contains this element. */ container?: IEdmEntityContainer | undefined; } +/** Defines EDM container element types. */ export enum EdmContainerElementKind { None = 0, EntitySet = 1, @@ -13544,7 +13622,9 @@ export enum EdmContainerElementKind { Singleton = 4, } +/** Base class for all annotatable types in OData library. */ export abstract class ODataAnnotatable implements IODataAnnotatable { + /** The annotation for storing @odata.type. */ typeAnnotation?: ODataTypeAnnotation | undefined; constructor(data?: IODataAnnotatable) { @@ -13574,13 +13654,19 @@ export abstract class ODataAnnotatable implements IODataAnnotatable { } } +/** Base class for all annotatable types in OData library. */ export interface IODataAnnotatable { + /** The annotation for storing @odata.type. */ typeAnnotation?: ODataTypeAnnotation | undefined; } +/** Class representing the a service document. */ export class ODataServiceDocument extends ODataAnnotatable implements IODataServiceDocument { + /** Gets or sets the set of entity sets in the service document. */ entitySets?: ODataEntitySetInfo[] | undefined; + /** Gets or sets the set of singletons in the service document. */ singletons?: ODataSingletonInfo[] | undefined; + /** Gets or sets the set of function imports in the service document. */ functionImports?: ODataFunctionImportInfo[] | undefined; constructor(data?: IODataServiceDocument) { @@ -13637,15 +13723,23 @@ export class ODataServiceDocument extends ODataAnnotatable implements IODataServ } } +/** Class representing the a service document. */ export interface IODataServiceDocument extends IODataAnnotatable { + /** Gets or sets the set of entity sets in the service document. */ entitySets?: ODataEntitySetInfo[] | undefined; + /** Gets or sets the set of singletons in the service document. */ singletons?: ODataSingletonInfo[] | undefined; + /** Gets or sets the set of function imports in the service document. */ functionImports?: ODataFunctionImportInfo[] | undefined; } +/** Abstract class representing an element (EntitySet, Singleton) in a service document. */ export abstract class ODataServiceDocumentElement extends ODataAnnotatable implements IODataServiceDocumentElement { + /** Gets or sets the URI representing the Unified Resource Locator (URL) to the element. */ url?: string | undefined; + /** Gets or sets the name of the element; this is the entity set or singleton name in JSON and the HREF in Atom. */ name?: string | undefined; + /** Gets or sets the title of the element; this is the title in JSON. */ title?: string | undefined; constructor(data?: IODataServiceDocumentElement) { @@ -13676,12 +13770,17 @@ export abstract class ODataServiceDocumentElement extends ODataAnnotatable imple } } +/** Abstract class representing an element (EntitySet, Singleton) in a service document. */ export interface IODataServiceDocumentElement extends IODataAnnotatable { + /** Gets or sets the URI representing the Unified Resource Locator (URL) to the element. */ url?: string | undefined; + /** Gets or sets the name of the element; this is the entity set or singleton name in JSON and the HREF in Atom. */ name?: string | undefined; + /** Gets or sets the title of the element; this is the title in JSON. */ title?: string | undefined; } +/** Class representing a entity set in a service document. */ export class ODataEntitySetInfo extends ODataServiceDocumentElement implements IODataEntitySetInfo { constructor(data?: IODataEntitySetInfo) { @@ -13706,10 +13805,13 @@ export class ODataEntitySetInfo extends ODataServiceDocumentElement implements I } } +/** Class representing a entity set in a service document. */ export interface IODataEntitySetInfo extends IODataServiceDocumentElement { } +/** Annotation which stores the EDM type information of a value. */ export class ODataTypeAnnotation implements IODataTypeAnnotation { + /** Gets the type name to serialize, for the annotated item. */ typeName?: string | undefined; constructor(data?: IODataTypeAnnotation) { @@ -13741,10 +13843,13 @@ export class ODataTypeAnnotation implements IODataTypeAnnotation { } } +/** Annotation which stores the EDM type information of a value. */ export interface IODataTypeAnnotation { + /** Gets the type name to serialize, for the annotated item. */ typeName?: string | undefined; } +/** Class representing a singleton in a service document. */ export class ODataSingletonInfo extends ODataServiceDocumentElement implements IODataSingletonInfo { constructor(data?: IODataSingletonInfo) { @@ -13769,9 +13874,11 @@ export class ODataSingletonInfo extends ODataServiceDocumentElement implements I } } +/** Class representing a singleton in a service document. */ export interface IODataSingletonInfo extends IODataServiceDocumentElement { } +/** Class representing a function Import in a service document. */ export class ODataFunctionImportInfo extends ODataServiceDocumentElement implements IODataFunctionImportInfo { constructor(data?: IODataFunctionImportInfo) { @@ -13796,6 +13903,7 @@ export class ODataFunctionImportInfo extends ODataServiceDocumentElement impleme } } +/** Class representing a function Import in a service document. */ export interface IODataFunctionImportInfo extends IODataServiceDocumentElement { } diff --git a/src/caretogether-pwa/src/Settings/Locations/Tabs/ActionDefinitions.tsx b/src/caretogether-pwa/src/Settings/Locations/Tabs/ActionDefinitions.tsx index 8b809971e..bfe711c69 100644 --- a/src/caretogether-pwa/src/Settings/Locations/Tabs/ActionDefinitions.tsx +++ b/src/caretogether-pwa/src/Settings/Locations/Tabs/ActionDefinitions.tsx @@ -1,5 +1,155 @@ -import { Typography } from '@mui/material'; +import { + Box, + Button, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + Paper, + Link, + TablePagination, + Drawer, +} from '@mui/material'; +import { useRecoilValue } from 'recoil'; +import { policyData } from '../../../Model/ConfigurationModel'; +import { useState } from 'react'; +import { AddActionDefinition } from './AddActionDefinition'; +import SearchIcon from '@mui/icons-material/Search'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import InputAdornment from '@mui/material/InputAdornment'; + +dayjs.extend(duration); +dayjs.extend(relativeTime); export default function ActionDefinitions() { - return Action Definitions; + 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); + + if (!actionDefinitions || Object.keys(actionDefinitions).length === 0) { + return ( + No action definitions found. + ); + } + + const rows = Object.entries(actionDefinitions).filter(([name]) => + name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + const paginatedRows = rows.slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage + ); + + return ( + + + Action definitions + + + Actions can not be delete, nor have its name changed. All the other + properties can be changed. + + + + + + ), + }} + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + /> + + + + + + Name + Document + Note + Instructions + Info link + Validity + + + + {paginatedRows.map(([name, definition]) => ( + + {name} + {definition.documentLink || '-'} + {definition.noteEntry || '-'} + {definition.instructions || '-'} + + {definition.infoLink ? ( + + {definition.infoLink} + + ) : ( + '-' + )} + + + {definition.validity + ? dayjs.duration(definition.validity).humanize() + : '-'} + + + ))} + +
+
+ + setPage(newPage)} + rowsPerPage={rowsPerPage} + rowsPerPageOptions={[]} + /> + + + + + + setOpenDrawer(false)} + > + + setOpenDrawer(false)} /> + + +
+ ); } diff --git a/src/caretogether-pwa/src/Settings/Locations/Tabs/AddActionDefinition.tsx b/src/caretogether-pwa/src/Settings/Locations/Tabs/AddActionDefinition.tsx new file mode 100644 index 000000000..f02d5deb6 --- /dev/null +++ b/src/caretogether-pwa/src/Settings/Locations/Tabs/AddActionDefinition.tsx @@ -0,0 +1,164 @@ +import { + Button, + TextField, + Typography, + Radio, + RadioGroup, + FormControl, + FormLabel, + FormControlLabel, + FormHelperText, + Stack, + Box, +} from '@mui/material'; +import { useForm, Controller } from 'react-hook-form'; + +interface DrawerProps { + onClose: () => void; +} + +interface AddActionDefinitionFormValues { + name: string; + documentRequirement: 'None' | 'Allowed' | 'Required'; + noteRequirement: 'None' | 'Allowed' | 'Required'; + instructions: string; + infoLink: string; +} + +export function AddActionDefinition({ onClose }: DrawerProps) { + const { + handleSubmit, + control, + formState: { errors, isValid }, + } = useForm({ + mode: 'onChange', + defaultValues: { + name: '', + documentRequirement: 'Allowed', + noteRequirement: 'Allowed', + instructions: '', + infoLink: '', + }, + }); + + const onSubmit = () => { + onClose(); + }; + + return ( +
+ + Add new action definition + + ( + + )} + /> + + + Document + ( + + + {['Allowed', 'Required', 'None'].map((value) => ( + + } + label={value} + /> + + {value === 'Allowed' && 'Document is optional'} + {value === 'Required' && 'Always require a document'} + {value === 'None' && 'Don’t ask for a document'} + + + ))} + + + )} + /> + + + + Note + ( + + + {['Allowed', 'Required', 'None'].map((value) => ( + + } + label={value} + /> + + {value === 'Allowed' && 'Note is optional'} + {value === 'Required' && 'Always require a note'} + {value === 'None' && 'Don’t ask for a note'} + + + ))} + + + )} + /> + + + ( + + )} + /> + + ( + + )} + /> + + + + + + +
+ ); +} diff --git a/swagger.json b/swagger.json index 623ca284b..725e87b51 100644 --- a/swagger.json +++ b/swagger.json @@ -1067,10 +1067,11 @@ "tags": [ "Metadata" ], + "summary": "Generates the OData $metadata document.", "operationId": "Metadata_GetMetadata", "responses": { "200": { - "description": "", + "description": "The IEdmModel representing $metadata.", "content": { "application/json": { "schema": { @@ -1087,10 +1088,11 @@ "tags": [ "Metadata" ], + "summary": "Generates the OData service document.", "operationId": "Metadata_GetServiceDocument", "responses": { "200": { - "description": "", + "description": "The service document for the service.", "content": { "application/json": { "schema": { @@ -7331,11 +7333,13 @@ }, "IEdmModel": { "type": "object", + "description": "Semantic representation of an EDM model.", "x-abstract": true, "additionalProperties": false, "properties": { "schemaElements": { "type": "array", + "description": "Gets the collection of schema elements that are contained in this model.", "nullable": true, "items": { "$ref": "#/components/schemas/IEdmSchemaElement" @@ -7343,6 +7347,7 @@ }, "vocabularyAnnotations": { "type": "array", + "description": "Gets the collection of vocabulary annotations that are contained in this model.", "nullable": true, "items": { "$ref": "#/components/schemas/IEdmVocabularyAnnotation" @@ -7350,6 +7355,7 @@ }, "referencedModels": { "type": "array", + "description": "Gets the collection of models referred to by this model (mainly by the this.References).", "nullable": true, "items": { "$ref": "#/components/schemas/IEdmModel" @@ -7357,12 +7363,14 @@ }, "declaredNamespaces": { "type": "array", + "description": "Gets the collection of namespaces that schema elements use contained in this model.", "nullable": true, "items": { "type": "string" } }, "directValueAnnotationsManager": { + "description": "Gets the model's annotations manager.", "nullable": true, "oneOf": [ { @@ -7371,6 +7379,7 @@ ] }, "entityContainer": { + "description": "Gets the only one entity container of the model.", "nullable": true, "oneOf": [ { @@ -7382,21 +7391,28 @@ }, "IEdmSchemaElement": { "type": "object", + "description": "Common base interface for all named children of EDM schema.", "x-abstract": true, "additionalProperties": false, "properties": { "schemaElementKind": { - "$ref": "#/components/schemas/EdmSchemaElementKind" + "description": "Gets the kind of this schema element.", + "oneOf": [ + { + "$ref": "#/components/schemas/EdmSchemaElementKind" + } + ] }, "namespace": { "type": "string", + "description": "Gets the namespace this schema element belongs to.", "nullable": true } } }, "EdmSchemaElementKind": { "type": "integer", - "description": "", + "description": "Defines EDM schema element types.", "x-enumNames": [ "None", "TypeDefinition", @@ -7416,14 +7432,17 @@ }, "IEdmVocabularyAnnotation": { "type": "object", + "description": "Represents an EDM vocabulary annotation.", "x-abstract": true, "additionalProperties": false, "properties": { "qualifier": { "type": "string", + "description": "Gets the qualifier used to discriminate between multiple bindings of the same property or type.", "nullable": true }, "term": { + "description": "Gets the term bound by the annotation.", "nullable": true, "oneOf": [ { @@ -7432,6 +7451,7 @@ ] }, "target": { + "description": "Gets the element the annotation applies to.", "nullable": true, "oneOf": [ { @@ -7440,6 +7460,7 @@ ] }, "value": { + "description": "Gets the expression producing the value of the annotation.", "nullable": true, "oneOf": [ { @@ -7451,10 +7472,12 @@ }, "IEdmTerm": { "type": "object", + "description": "Represents an EDM term.", "x-abstract": true, "additionalProperties": false, "properties": { "type": { + "description": "Gets the type of this term.", "nullable": true, "oneOf": [ { @@ -7464,23 +7487,28 @@ }, "appliesTo": { "type": "string", + "description": "Gets the AppliesTo of this term.", "nullable": true }, "defaultValue": { "type": "string", + "description": "Gets the DefaultValue of this term.", "nullable": true } } }, "IEdmTypeReference": { "type": "object", + "description": "Represents a references to a type.", "x-abstract": true, "additionalProperties": false, "properties": { "isNullable": { - "type": "boolean" + "type": "boolean", + "description": "Gets a value indicating whether this type is nullable." }, "definition": { + "description": "Gets the definition to which this type refers.", "nullable": true, "oneOf": [ { @@ -7492,17 +7520,23 @@ }, "IEdmType": { "type": "object", + "description": "Represents the definition of an EDM type.", "x-abstract": true, "additionalProperties": false, "properties": { "typeKind": { - "$ref": "#/components/schemas/EdmTypeKind" + "description": "Gets the kind of this type.", + "oneOf": [ + { + "$ref": "#/components/schemas/EdmTypeKind" + } + ] } } }, "EdmTypeKind": { "type": "integer", - "description": "", + "description": "Defines EDM metatypes.", "x-enumNames": [ "None", "Primitive", @@ -7530,22 +7564,29 @@ }, "IEdmVocabularyAnnotatable": { "type": "object", + "description": "Represents an element that can be targeted by Vocabulary Annotations", "x-abstract": true, "additionalProperties": false }, "IEdmExpression": { "type": "object", + "description": "Represents an EDM expression.", "x-abstract": true, "additionalProperties": false, "properties": { "expressionKind": { - "$ref": "#/components/schemas/EdmExpressionKind" + "description": "Gets the kind of this expression.", + "oneOf": [ + { + "$ref": "#/components/schemas/EdmExpressionKind" + } + ] } } }, "EdmExpressionKind": { "type": "integer", - "description": "", + "description": "Defines EDM expression kinds.", "x-enumNames": [ "None", "BinaryConstant", @@ -7605,16 +7646,19 @@ }, "IEdmDirectValueAnnotationsManager": { "type": "object", + "description": "Manages getting and setting direct annotations on EDM elements.", "x-abstract": true, "additionalProperties": false }, "IEdmEntityContainer": { "type": "object", + "description": "Represents an EDM entity container.", "x-abstract": true, "additionalProperties": false, "properties": { "elements": { "type": "array", + "description": "Gets a collection of the elements of this entity container.", "nullable": true, "items": { "$ref": "#/components/schemas/IEdmEntityContainerElement" @@ -7624,13 +7668,20 @@ }, "IEdmEntityContainerElement": { "type": "object", + "description": "Represents the common elements of all EDM entity container elements.", "x-abstract": true, "additionalProperties": false, "properties": { "containerElementKind": { - "$ref": "#/components/schemas/EdmContainerElementKind" + "description": "Gets the kind of element of this container element.", + "oneOf": [ + { + "$ref": "#/components/schemas/EdmContainerElementKind" + } + ] }, "container": { + "description": "Gets the container that contains this element.", "nullable": true, "oneOf": [ { @@ -7642,7 +7693,7 @@ }, "EdmContainerElementKind": { "type": "integer", - "description": "", + "description": "Defines EDM container element types.", "x-enumNames": [ "None", "EntitySet", @@ -7665,10 +7716,12 @@ }, { "type": "object", + "description": "Class representing the a service document.", "additionalProperties": false, "properties": { "entitySets": { "type": "array", + "description": "Gets or sets the set of entity sets in the service document.", "nullable": true, "items": { "$ref": "#/components/schemas/ODataEntitySetInfo" @@ -7676,6 +7729,7 @@ }, "singletons": { "type": "array", + "description": "Gets or sets the set of singletons in the service document.", "nullable": true, "items": { "$ref": "#/components/schemas/ODataSingletonInfo" @@ -7683,6 +7737,7 @@ }, "functionImports": { "type": "array", + "description": "Gets or sets the set of function imports in the service document.", "nullable": true, "items": { "$ref": "#/components/schemas/ODataFunctionImportInfo" @@ -7699,6 +7754,7 @@ }, { "type": "object", + "description": "Class representing a entity set in a service document.", "additionalProperties": false } ] @@ -7710,20 +7766,24 @@ }, { "type": "object", + "description": "Abstract class representing an element (EntitySet, Singleton) in a service document.", "x-abstract": true, "additionalProperties": false, "properties": { "url": { "type": "string", + "description": "Gets or sets the URI representing the Unified Resource Locator (URL) to the element.", "format": "uri", "nullable": true }, "name": { "type": "string", + "description": "Gets or sets the name of the element; this is the entity set or singleton name in JSON and the HREF in Atom.", "nullable": true }, "title": { "type": "string", + "description": "Gets or sets the title of the element; this is the title in JSON.", "nullable": true } } @@ -7732,10 +7792,12 @@ }, "ODataAnnotatable": { "type": "object", + "description": "Base class for all annotatable types in OData library.", "x-abstract": true, "additionalProperties": false, "properties": { "typeAnnotation": { + "description": "The annotation for storing @odata.type.", "nullable": true, "oneOf": [ { @@ -7747,10 +7809,12 @@ }, "ODataTypeAnnotation": { "type": "object", + "description": "Annotation which stores the EDM type information of a value.", "additionalProperties": false, "properties": { "typeName": { "type": "string", + "description": "Gets the type name to serialize, for the annotated item. ", "nullable": true } } @@ -7762,6 +7826,7 @@ }, { "type": "object", + "description": "Class representing a singleton in a service document.", "additionalProperties": false } ] @@ -7773,6 +7838,7 @@ }, { "type": "object", + "description": "Class representing a function Import in a service document.", "additionalProperties": false } ] From aa92de920718107490d89e0b1cf4333fef9f610a Mon Sep 17 00:00:00 2001 From: Rabi <134292357+Rabi94@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:31:21 -0300 Subject: [PATCH 3/4] Add Action Definition creation drawer and EffectiveLocationPolicy PUT endpoint --- .vscode/launch.json | 2 +- .../Controllers/ConfigurationController.cs | 19 ++ .../Resources/Policies/PoliciesResource.cs | 5 +- src/caretogether-pwa/src/GeneratedClient.ts | 44 ++++ .../src/Settings/Locations/LocationEdit.tsx | 6 + .../Locations/Tabs/ActionDefinitions.tsx | 108 +++++---- .../Locations/Tabs/AddActionDefinition.tsx | 215 +++++++++++------- swagger.json | 52 +++++ .../CareTogether.TestData/TestDataProvider.cs | 2 +- 9 files changed, 320 insertions(+), 133 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d1478c8b4..86e52d266 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -50,4 +50,4 @@ "stopAll": true } ] -} \ No newline at end of file +} diff --git a/src/CareTogether.Api/Controllers/ConfigurationController.cs b/src/CareTogether.Api/Controllers/ConfigurationController.cs index 0768ec570..8f8bc1791 100644 --- a/src/CareTogether.Api/Controllers/ConfigurationController.cs +++ b/src/CareTogether.Api/Controllers/ConfigurationController.cs @@ -432,6 +432,25 @@ Guid locationId return Ok(result); } + [HttpPut("/api/{organizationId:guid}/{locationId:guid}/[controller]/policy")] +public async Task> 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> GetLocationFlags(Guid organizationId) { diff --git a/src/CareTogether.Core/Resources/Policies/PoliciesResource.cs b/src/CareTogether.Core/Resources/Policies/PoliciesResource.cs index d0822bfed..09aa494da 100644 --- a/src/CareTogether.Core/Resources/Policies/PoliciesResource.cs +++ b/src/CareTogether.Core/Resources/Policies/PoliciesResource.cs @@ -218,9 +218,8 @@ af with .ToImmutableList(), }, }; - - return effectivePolicy; - } + return effectivePolicy; +} public async Task GetOrganizationSecretsAsync(Guid organizationId) { diff --git a/src/caretogether-pwa/src/GeneratedClient.ts b/src/caretogether-pwa/src/GeneratedClient.ts index b02df1337..4553a05d3 100644 --- a/src/caretogether-pwa/src/GeneratedClient.ts +++ b/src/caretogether-pwa/src/GeneratedClient.ts @@ -282,6 +282,50 @@ export class ConfigurationClient { return Promise.resolve(null as any); } + putEffectiveLocationPolicy(organizationId: string, locationId: string, policy: EffectiveLocationPolicy): Promise { + 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 { + 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(null as any); + } + getLocationFlags(organizationId: string, locationId: string): Promise { let url_ = this.baseUrl + "/api/{organizationId}/{locationId}/Configuration/flags"; if (organizationId === undefined || organizationId === null) diff --git a/src/caretogether-pwa/src/Settings/Locations/LocationEdit.tsx b/src/caretogether-pwa/src/Settings/Locations/LocationEdit.tsx index c8b04b85d..8837f9203 100644 --- a/src/caretogether-pwa/src/Settings/Locations/LocationEdit.tsx +++ b/src/caretogether-pwa/src/Settings/Locations/LocationEdit.tsx @@ -229,6 +229,12 @@ export function LocationEdit() { )} + {activeTab === 'actions' && ( + + + + )} + {activeTab === 'accessLevels' && ( diff --git a/src/caretogether-pwa/src/Settings/Locations/Tabs/ActionDefinitions.tsx b/src/caretogether-pwa/src/Settings/Locations/Tabs/ActionDefinitions.tsx index bfe711c69..a27d720d0 100644 --- a/src/caretogether-pwa/src/Settings/Locations/Tabs/ActionDefinitions.tsx +++ b/src/caretogether-pwa/src/Settings/Locations/Tabs/ActionDefinitions.tsx @@ -14,53 +14,63 @@ import { 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 { useState } from 'react'; import { AddActionDefinition } from './AddActionDefinition'; -import SearchIcon from '@mui/icons-material/Search'; -import dayjs from 'dayjs'; -import duration from 'dayjs/plugin/duration'; -import relativeTime from 'dayjs/plugin/relativeTime'; -import InputAdornment from '@mui/material/InputAdornment'; -dayjs.extend(duration); -dayjs.extend(relativeTime); +const requirementLabel: Record = { + 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() { 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); - if (!actionDefinitions || Object.keys(actionDefinitions).length === 0) { - return ( - No action definitions found. - ); - } + const entries = Object.entries(actionDefinitions ?? {}); - const rows = Object.entries(actionDefinitions).filter(([name]) => - name.toLowerCase().includes(searchTerm.toLowerCase()) + const filtered = useMemo( + () => + entries.filter(([name]) => + name.toLowerCase().includes(searchTerm.toLowerCase()) + ), + [entries, searchTerm] ); - const paginatedRows = rows.slice( + + const paginated = filtered.slice( page * rowsPerPage, page * rowsPerPage + rowsPerPage ); + if (!actionDefinitions) { + return No action definitions found.; + } + return ( - Action definitions - - - Actions can not be delete, nor have its name changed. All the other - properties can be changed. + Action Definitions setSearchTerm(e.target.value)} /> - + Name - Document + Document Link Note Instructions - Info link + Info Link Validity + - {paginatedRows.map(([name, definition]) => ( + {paginated.map(([name, def]) => ( - {name} - {definition.documentLink || '-'} - {definition.noteEntry || '-'} - {definition.instructions || '-'} - {definition.infoLink ? ( + {name} + + {def.alternateNames && def.alternateNames.length > 0 && ( + + {def.alternateNames.join(', ')} + + )} + + + {requirementLabel[def.documentLink]} + {requirementLabel[def.noteEntry]} + {truncate(def.instructions)} + + {def.infoLink ? ( - {definition.infoLink} + {def.infoLink} ) : ( '-' )} - - {definition.validity - ? dayjs.duration(definition.validity).humanize() - : '-'} - + {formatValidity(def.validity)} ))} @@ -124,7 +144,7 @@ export default function ActionDefinitions() { setPage(newPage)} rowsPerPage={rowsPerPage} @@ -132,12 +152,8 @@ export default function ActionDefinitions() { /> - diff --git a/src/caretogether-pwa/src/Settings/Locations/Tabs/AddActionDefinition.tsx b/src/caretogether-pwa/src/Settings/Locations/Tabs/AddActionDefinition.tsx index f02d5deb6..db9af0d44 100644 --- a/src/caretogether-pwa/src/Settings/Locations/Tabs/AddActionDefinition.tsx +++ b/src/caretogether-pwa/src/Settings/Locations/Tabs/AddActionDefinition.tsx @@ -1,17 +1,9 @@ -import { - Button, - TextField, - Typography, - Radio, - RadioGroup, - FormControl, - FormLabel, - FormControlLabel, - FormHelperText, - Stack, - Box, -} from '@mui/material'; +import { Button, TextField, Typography, Stack, MenuItem } from '@mui/material'; import { useForm, Controller } from 'react-hook-form'; +import { useRecoilValue } from 'recoil'; +import { selectedLocationContextState } from '../../../Model/Data'; +import { api } from '../../../Api/Api'; +import { EffectiveLocationPolicy } from '../../../GeneratedClient'; interface DrawerProps { onClose: () => void; @@ -19,13 +11,25 @@ interface DrawerProps { interface AddActionDefinitionFormValues { name: string; - documentRequirement: 'None' | 'Allowed' | 'Required'; - noteRequirement: 'None' | 'Allowed' | 'Required'; + alternateNames: string; instructions: string; infoLink: string; + documentRequirement: 0 | 1 | 2; + noteRequirement: 0 | 1 | 2; + validityInDays: number | null; } +const requirementOptions = [ + { label: 'None', value: 0 }, + { label: 'Allowed', value: 1 }, + { label: 'Required', value: 2 }, +]; + export function AddActionDefinition({ onClose }: DrawerProps) { + const { organizationId, locationId } = useRecoilValue( + selectedLocationContextState + ); + const { handleSubmit, control, @@ -34,21 +38,76 @@ export function AddActionDefinition({ onClose }: DrawerProps) { mode: 'onChange', defaultValues: { name: '', - documentRequirement: 'Allowed', - noteRequirement: 'Allowed', + alternateNames: '', instructions: '', infoLink: '', + documentRequirement: 0, + noteRequirement: 0, + validityInDays: null, }, }); - const onSubmit = () => { + const convertToBackend = (data: AddActionDefinitionFormValues) => { + const parsedAlternateNames = data.alternateNames + ? data.alternateNames.split(',').map((x) => x.trim()) + : []; + + const validity = + data.validityInDays && data.validityInDays > 0 + ? `${data.validityInDays}.00:00:00` + : null; + + return { + name: data.name, + alternateNames: parsedAlternateNames, + instructions: data.instructions || null, + infoLink: data.infoLink || null, + documentLink: data.documentRequirement, + noteEntry: data.noteRequirement, + validity, + canView: null, + canEdit: null, + }; + }; + + const onSubmit = async (values: AddActionDefinitionFormValues) => { + const newAction = convertToBackend(values); + + const currentPolicy = await api.configuration.getEffectiveLocationPolicy( + organizationId, + locationId + ); + + const updatedPolicy = { + ...currentPolicy, + actionDefinitions: { + ...currentPolicy.actionDefinitions, + [newAction.name]: { + documentLink: newAction.documentLink, + noteEntry: newAction.noteEntry, + instructions: newAction.instructions, + infoLink: newAction.infoLink, + validity: newAction.validity, + canView: null, + canEdit: null, + alternateNames: newAction.alternateNames, + }, + }, + }; + + await api.configuration.putEffectiveLocationPolicy( + organizationId, + locationId, + updatedPolicy as unknown as EffectiveLocationPolicy + ); + onClose(); }; return (
- - Add new action definition + + Add New Action Definition ( )} /> - - Document - ( - - - {['Allowed', 'Required', 'None'].map((value) => ( - - } - label={value} - /> - - {value === 'Allowed' && 'Document is optional'} - {value === 'Required' && 'Always require a document'} - {value === 'None' && 'Don’t ask for a document'} - - - ))} - - - )} - /> - - - - Note - ( - - - {['Allowed', 'Required', 'None'].map((value) => ( - - } - label={value} - /> - - {value === 'Allowed' && 'Note is optional'} - {value === 'Required' && 'Always require a note'} - {value === 'None' && 'Don’t ask for a note'} - - - ))} - - - )} - /> - + ( + + )} + /> )} /> @@ -145,14 +154,56 @@ export function AddActionDefinition({ onClose }: DrawerProps) { )} /> - -