Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4ba6b37
adds new autocomplete editable field
DarrellRoberts Aug 7, 2026
77fe7e4
Merge branch 'develop' into darrell/fix/operator-agent-org-details
need4deed Aug 10, 2026
41bb5b3
Merge branch 'develop' into darrell/fix/operator-agent-org-details
need4deed Aug 10, 2026
a4f9e4d
adds hint to autocomplete & reverts initvalues for operator
DarrellRoberts Aug 11, 2026
ff2442b
Merge branch 'darrell/fix/operator-agent-org-details' of github.com:n…
DarrellRoberts Aug 11, 2026
545cf82
Merge branch 'develop' into darrell/fix/operator-agent-org-details
need4deed Aug 19, 2026
b75dbfe
Merge branch 'develop' into darrell/fix/operator-agent-org-details
arturasmckwcz Aug 26, 2026
e072dcd
fix: remove error appreciations and gitignore claude local state
arturasmckwcz Aug 26, 2026
34d87e1
Merge branch 'develop' into darrell/fix/operator-agent-org-details
arturasmckwcz Aug 27, 2026
546c861
resolves merge conflicts
DarrellRoberts Aug 27, 2026
c233c0f
adds additional lowercase guard & changes onclick handler
DarrellRoberts Aug 27, 2026
de765c2
Merge branch 'darrell/fix/operator-agent-org-details' of github.com:n…
DarrellRoberts Aug 27, 2026
0d93c00
Merge branch 'develop' into darrell/fix/operator-agent-org-details
arturasmckwcz Aug 31, 2026
5f6c94b
Merge branch 'develop' into darrell/fix/operator-agent-org-details
arturasmckwcz Sep 2, 2026
15cb761
adds onblur, local val & removes strict zod validation
DarrellRoberts Sep 2, 2026
847825c
Merge branch 'darrell/fix/operator-agent-org-details' of github.com:n…
DarrellRoberts Sep 2, 2026
e18b9ac
Merge branch 'develop' into darrell/fix/operator-agent-org-details
need4deed Sep 2, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,8 @@ dev/
dev.*
.env.local

# claude code local worktree scratch
.claude/worktrees/

# prettier
.prettierignore
6 changes: 6 additions & 0 deletions public/locales/de/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1250,8 +1250,14 @@
"validation": {
"required": "Dieses Feld ist erforderlich",
"websiteInvalid": "Webseite muss mit http:// oder https:// beginnen",
"operatorRequired": "Es sind mindestens 3 Zeichen erforderlich.",
"operatorHint": "Geben Sie die ersten drei Buchstaben ein, damit die Liste angezeigt wird, und wählen Sie aus der Liste aus. Wenn die Liste nicht erscheint, ist der Träger ungültig.",
"operatorInvalid": "Ungültiger Träger. Bitte wählen Sie aus der Liste oder versuchen Sie es mit einem anderen Träger.",
"clientLanguagesRequired": "Mindestens eine Sprache ist erforderlich"
},
"placeholders": {
"operatorPlaceholder": "Geben Sie die ersten drei Buchstaben des Trägers ein"
},
"addressStreet": "Straße",
"addressPostcode": "PLZ"
},
Expand Down
6 changes: 6 additions & 0 deletions public/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1238,8 +1238,14 @@
"validation": {
"required": "This field is required",
"websiteInvalid": "Website must start with http:// or https://",
"operatorRequired": "A minimum of 3 characters is needed",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New i18n keys operatorRequired and operatorInvalid (and their de counterparts) are added but never referenced by any t(...) call outside the translation files. This copy looks like it was meant to surface the exact failure described in the operator-save finding, but it was never wired into the schema or the EditableField error path — translators now maintain strings nothing displays.

"operatorHint": "Type first three letters for the list to appear and select from the list. If the list doesn't appear, Operator is invalid.",
"operatorInvalid": "Operator invalid. Either select from the list or try another Operator",
"clientLanguagesRequired": "At least one language is required"
},
"placeholders": {
"operatorPlaceholder": "Type the first three letters of the operator"
},
"addressStreet": "Street",
"addressPostcode": "Postcode"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { apiLanguagesToFormValues, toLanguagesForForm } from "./formatters";
import { OrganisationDetailsDisplay } from "./OrganisationDetailsDisplay";
import { OrganisationDetailsEdit } from "./OrganisationDetailsEdit";
import { createOrganisationDetailsSchema, OrganisationDetailsFormData } from "./organisationDetailsSchema";
import { useGetOrganization } from "@/hooks/useGetOrganization";

type Props = {
agent: ApiAgentProfileGet;
Expand All @@ -35,8 +36,10 @@ export const OrganisationDetails = forwardRef<EditableSectionRef, Props>(functio
const { data: apiLanguages } = useApiLanguages();
const { data: apiAgentTypes = [] } = useApiAgentTypes();
const { data: apiServices = [] } = useApiServices();
const { data: organizations = [] } = useGetOrganization();
const agentTypeMapping = useMemo(() => createMapping(apiAgentTypes), [apiAgentTypes]);
const serviceMapping = useMemo(() => createMapping(apiServices), [apiServices]);
const organizationMapping = useMemo(() => createMapping(organizations), [organizations]);

const details = agent.agentDetails;
const languagesForForm = toLanguagesForForm(apiLanguages, i18n.language);
Expand Down Expand Up @@ -96,6 +99,7 @@ export const OrganisationDetails = forwardRef<EditableSectionRef, Props>(functio
const serviceIds = values.services
.map((title) => serviceMapping.titleToId[title])
.filter((id): id is number => id !== undefined);
const organizationId = organizationMapping.titleToId[values.operator];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONFIRMED] Operator save silently no-ops when the typed text doesn't exactly match an organization title.

If the user types an operator name but doesn't click a dropdown row (case/whitespace differs, or they ignore the list and hit Save), organizationMapping.titleToId[values.operator] (exact-case lookup) returns undefined, so ...(organizationId !== undefined && { organizationId }) (line 113) omits organizationId from the PATCH entirely. ApiAgentPatch has no raw operator string field, so nothing about the operator is sent at all. organisationDetailsSchema.ts only validates operator: z.string().min(1, required), so the form is considered valid and Save succeeds anyway — onSuccess calls reset(values); setIsEditing(false), and OrganisationDetailsDisplay renders the typed (unsaved) text via watch(). The user believes their change saved, but the backend operator/organizationId is untouched — silent data loss.


updateOrganization(
{
Expand All @@ -106,6 +110,7 @@ export const OrganisationDetails = forwardRef<EditableSectionRef, Props>(functio
addressPostcode: values.addressPostcode,
...(typeId !== undefined && { typeId }),
serviceIds,
...(organizationId !== undefined && { organizationId }),
},
{
onSuccess: () => {
Expand All @@ -124,6 +129,7 @@ export const OrganisationDetails = forwardRef<EditableSectionRef, Props>(functio
languagesForForm={languagesForForm}
organizationTypeOptions={apiAgentTypes.map((agentType) => agentType.title)}
servicesOptions={apiServices.map((service) => service.title)}
operatorOptions={organizations?.map((org) => org.title)}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant optional chaining: const { data: organizations = [] } = useGetOrganization(); already guarantees organizations is never nullish, so organizations?.map((org) => org.title) carries a defensive ?. that can never trigger.

onCancel={handleCancel}
onSubmit={handleSubmit(onSubmit)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const OrganisationDetailsDisplay = ({ rawClientLanguages, address }: Prop
const { t } = useTranslation();
const { watch } = useFormContext<OrganisationDetailsFormData>();
const values = watch();

return (
<FormDetails data-testid="organisation-details-display">
<EditableField
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { FormButtonRow, FormDetails } from "../shared/styles";
import { OrganisationDetailsFormData } from "./organisationDetailsSchema";
import { useCallback } from "react";

const i18nPrefix = "dashboard.agentProfile.organisationDetails";

type Props = {
languagesForForm: Option[];
organizationTypeOptions: string[];
servicesOptions: string[];
operatorOptions: string[];
onCancel: () => void;
onSubmit: () => void;
};
Expand All @@ -22,6 +24,7 @@ export const OrganisationDetailsEdit = ({
languagesForForm,
organizationTypeOptions,
servicesOptions,
operatorOptions,
onCancel,
onSubmit,
}: Props) => {
Expand All @@ -31,6 +34,16 @@ export const OrganisationDetailsEdit = ({
formState: { errors, isDirty, isValid },
} = useFormContext<OrganisationDetailsFormData>();

const displayOperators = useCallback(
(value: string) => {
if (value?.length >= 3) {
return operatorOptions.filter((op) => op.toLowerCase().includes(value.toLowerCase()));
} else {
return [];
}
},
[operatorOptions],
);
return (
<>
<FormDetails data-testid="organisation-details-edit">
Expand Down Expand Up @@ -123,14 +136,19 @@ export const OrganisationDetailsEdit = ({
name="operator"
control={control}
render={({ field }) => (
<EditableField
mode="edit"
type="text"
label={t(`${i18nPrefix}.operator`)}
value={field.value}
setValue={field.onChange}
errorMessage={errors.operator?.message}
/>
<>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The operator Controller's render prop wraps a single <EditableField> in an unnecessary <>...</> fragment — no sibling element is added, and every other Controller in this file returns the EditableField directly.

<EditableField
mode="edit"
type="autocomplete"
label={t(`${i18nPrefix}.operator`)}
value={field.value}
setValue={field.onChange}
errorMessage={errors.operator?.message}
options={displayOperators(field.value)}
placeholder={t(`${i18nPrefix}.placeholders.operatorPlaceholder`)}
hint={t(`dashboard.agentProfile.organisationDetails.validation.operatorHint`)}
/>
</>
)}
/>
<Controller
Expand Down
2 changes: 1 addition & 1 deletion src/components/Dashboard/common/statusMaps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type React from "react";
import {
BinocularsIcon,
ChartLineIcon,
Expand All @@ -12,6 +11,7 @@ import {
StopCircleIcon,
} from "@phosphor-icons/react";
import { AgentEngagementStatusType, AgentTrustType, AgentVolunteerSearchType } from "need4deed-sdk";
import type React from "react";

export type StatusValue = AgentEngagementStatusType | AgentVolunteerSearchType | AgentTrustType;

Expand Down
68 changes: 67 additions & 1 deletion src/components/EditableField/EditableField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ const StepperValue = styled.span`
user-select: none;
`;

type EditableFieldType = "text" | "textarea" | "number" | "stepper" | "checkbox-list" | "radio-list";
type EditableFieldType = "text" | "textarea" | "number" | "stepper" | "checkbox-list" | "radio-list" | "autocomplete";

export interface EditableFieldRef<T> {
getValue: () => T;
Expand Down Expand Up @@ -559,6 +559,72 @@ export const EditableField = forwardRef(function EditableField<T extends string
)}
</DropdownWrapper>
)}

{type === "autocomplete" && (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplification: This new autocomplete block duplicates the existing checkbox-list/radio-list dropdown structure (DropdownWrapper/DropdownButton/DropdownList/OptionRow) almost line-for-line instead of reusing it.

~55 lines at 506-561 and ~55 lines at 563-620 both implement the same wrapper/button/list/option-row pattern, differing only in the button's inner content and single- vs multi-select. Any future change to dropdown behavior (styling, keyboard nav, outside-click handling) now has to be made twice and can drift out of sync.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

originally I wanted to reuse the checkbox-list/radio-list but I found it was easier to create a new type, as otherwise you are complicating this checkbox/radio-lists with extra props and conditions (also risks accidentally mutating their existing behaviour).

Also now they have different behaviours and slightly different appearences (now no chevron arrow for autocomplete as it's no longer needed)

I think also if there is a future change to the checkbox-list/radio-list it shouldn't neccessarily mean that the same change should apply to the autocomplete?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplification: The new type === "autocomplete" branch fully duplicates the DropdownWrapper/DropdownButton/DropdownList/OptionRow scaffold already used by the checkbox-list/radio-list branch instead of extending it, and has already silently diverged from it in two ways (see follow-up comment below).

Failure scenario: radio-list's button toggles open/closed, autocomplete's only opens and never closes on click; radio-list's list renders on open alone while autocomplete additionally gates on options.length > 0. Any future fix to shared dropdown behavior (keyboard nav, a11y, styling) has to be applied to both blocks or they drift further, as already happened here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new autocomplete branch duplicates the text-input+clear-button markup and the dropdown-list/option-row markup already implemented for the text and checkbox-list/radio-list branches (~65 lines re-authoring InputWrapper/ClearButton/Boolean(localValue) from the text branch and DropdownWrapper/DropdownList/OptionRow from checkbox-list/radio-list). A fix to one copy (e.g. the outside-click handling, or the clear-button stopPropagation flagged above) won't propagate to the other, and future edits must be made twice.

<DropdownWrapper ref={wrapperRef}>
<DropdownButton
$hasError={!!errorMessage}
onClick={() => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONFIRMED] The autocomplete dropdown closes on a plain click inside its own input, not just on option selection or outside click.

With the dropdown open and a non-empty value, clicking inside the <input> to reposition the cursor or select text bubbles up (the input has no onClick/stopPropagation, unlike ClearButton which does call e.stopPropagation()) to the wrapping DropdownButton, whose handler is onClick={() => { if (localValue) setOpen((o) => !o); }} — toggling open from true to false and closing the suggestion list mid-edit.

if (localValue) setOpen((o) => !o);
}}
>
<InputWrapper>
<input

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: The new autocomplete <input> wires neither onBlur nor onKeyDown, unlike every other edit-mode input type (text/textarea/number) in this shared component, which all support submit-on-blur and Enter-to-submit.

Failure scenario: Dormant today since no current EditableField caller passes a submit prop with type="autocomplete", but a future caller adding it following the existing text-input pattern will find blur-to-save and Enter-to-save silently do nothing, and Enter won't even preventDefault() if the field is ever placed inside a real <form>.

type="text"
value={localValue}
placeholder={placeholder}
onChange={(e) => {
const v = e.target.value as T;
setLocalValue(v);
setValue(v);
setOpen(true);
}}
onBlur={handleSubmit}
onKeyDown={handleKeyDown}
style={{ border: "none", padding: 0 }}
/>
{Boolean(localValue) && (
<ClearButton
type="button"
onClick={(e) => {
e.stopPropagation();
const v = "" as T;
setLocalValue(v);
setValue(v);
setOpen(false);
}}
style={{ alignSelf: "flex-start", marginTop: "0px", marginRight: "0px" }}
>
<XCircleIcon size={20} weight="bold" />
</ClearButton>
)}
</InputWrapper>
</DropdownButton>

{open && (
<DropdownList>
{options.map((option) => {
const isSelected = localValue === option;
return (
<OptionRow
key={option}
$isSelected={isSelected}
onClick={() => {
const v = option as T;
setLocalValue(v);
setValue(v);
setOpen(false);
}}
>
<input type="radio" name={label} value={option} checked={isSelected} onChange={() => {}} />
<Text>{option}</Text>
</OptionRow>
);
})}
</DropdownList>
)}
</DropdownWrapper>
)}
</FieldWrapper>
{error && (
<p style={{ color: "var(--editableField-error-color)", paddingLeft: "var(--editableField-error-paddingLeft)" }}>
Expand Down
19 changes: 19 additions & 0 deletions src/hooks/useGetOrganization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ApiOrganizationGetList } from "need4deed-sdk";
import { useGetQuery } from "./useGetQuery";
import { apiPathOrganization, cacheTTL } from "@/config/constants";

export const useGetOrganization = () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Efficiency: useGetOrganization omits addLang: false, unlike every other comparable hook fetching non-translated reference data, so the UI language becomes part of the request and the react-query cache key for data that doesn't depend on it.

Failure scenario: Backend GET /organization/ never reads request.query.language, and organization titles are explicitly documented as untranslated. Toggling UI language between en/de causes react-query to treat the two languages as separate cache entries, triggering a redundant refetch and duplicate cached copies of identical data.

const { data, isLoading, isError, error } = useGetQuery<ApiOrganizationGetList[]>({
queryKey: ["organization"],
apiPath: `${apiPathOrganization}`,
staleTime: cacheTTL,
addLang: false,
});

return {
data,
isLoading,
isError,
error,
};
};
Loading