Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -230,14 +230,18 @@
"contactNumber": "Contact no.",
"website": "Website URL",
"address": "Address",
"industry": "Industry"
"industry": "Industry",
"addNewIndustry": "Add \"{{name}}\" as a new industry"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Claude · 🟣 Nit

addNewIndustry is an action/CTA string with interpolation but it is placed under labels, which everywhere else in this namespace holds static form-field labels (name, contactNumber, website, ...). Move it under buttons (or a new actions group) so the key path reflects its role, consistent with the module.section.key convention.

},
"placeholders": {
"name": "Enter company name",
"contactNumber": "Enter contact no.",
"website": "Enter website",
"address": "Enter address",
"industry": "Select industry"
"industry": "Search or add industry"
},
"emptyStates": {
"noIndustries": "No industries found"
},
"buttons": {
"save": "Save",
Expand All @@ -249,6 +253,7 @@
"website": "Website",
"address": "Address",
"industry": "Industry",
"clearIndustry": "Clear selected industry",
"save": "Save",
"cancel": "Cancel"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export enum characterLengths {
NAME_LENGTH = 50,
CHARACTER_LENGTH = 50,
COMPANY_NAME_LENGTH = 30,
INDUSTRY_NAME_LENGTH = 100,
ORGANIZATION_NAME_LENGTH = 100,
EMPLOYEE_ID_LENGTH = 20,
LEAVE_TYPE_LENGTH = 20,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { FC } from "react";

interface AddIndustryOptionProps {
label: string;
}

const AddIndustryOption: FC<AddIndustryOptionProps> = ({ label }) => (
<span className="-mx-4 -my-2 flex items-center gap-2 rounded bg-primary-background px-4 py-2 text-primary-text">
<span aria-hidden="true">+</span>
{label}
</span>
);

export default AddIndustryOption;
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import {
ButtonV2,
CloseIcon,
Dropdown,
InputField
} from "@rootcodelabs/skapp-ui";
import { ButtonV2, CloseIcon, InputField } from "@rootcodelabs/skapp-ui";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Claude · 🔴 Important

📁 File-level observation

SidePanelCompanyHeader.tsx (lines 22-26, 79) still resolves the industry for display via useTranslator("crmModule", "companies", "industryOptions") and translateText([industry]). Once industry becomes a numeric id created from free-form user input, that lookup can never match a key in the industryOptions block and i18next will render the raw key (e.g. "12") in the side panel. Update that consumer to resolve the name from the industries store/record, and decide whether the now-legacy companies.industryOptions translation block should be retired.

import { FormikProps } from "formik";
import { FC, useMemo } from "react";
import { ChangeEvent, FC, useMemo, useState } from "react";
import { useShallow } from "zustand/react/shallow";

import { SearchableDropdownItem } from "~community/common/components/molecules/SearchableDropdown/SearchableDropdown";
import { SEARCH_DEBOUNCE_DELAY } from "~community/common/constants/commonConstants";
import { characterLengths } from "~community/common/constants/stringConstants";
import useDebounce from "~community/common/hooks/useDebounce";
import { useTranslator } from "~community/common/hooks/useTranslator";
import { TranslatorFunctionType } from "~community/common/types/CommonTypes";
import SelectableSearchField from "~community/crm/components/molecules/SelectableSearchField/SelectableSearchField";
import { useCheckCompanyNameExists } from "~community/crm/v2/api/CompanyApi";
import { CrmIndustryEnum } from "~community/crm/v2/enums/common";
import AddIndustryOption from "~community/crm/v2/components/atoms/AddIndustryOption/AddIndustryOption";
import { ADD_NEW_INDUSTRY_OPTION_ID } from "~community/crm/v2/constants/commonConstants";
import { useCrmStoreV2 } from "~community/crm/v2/store/store";
import { CrmCompanyEntity } from "~community/crm/v2/types/CrmCommonTypes";
import {
CrmIndustryOption,
getIndustryDisplayName,
getIndustryOptions
} from "~community/crm/v2/utils/companyUtil";

interface CompanyModalFormProps {
formik: FormikProps<CrmCompanyEntity>;
Expand All @@ -31,22 +36,16 @@ const CompanyModalForm: FC<CompanyModalFormProps> = ({
originalName,
onCancel
}) => {
const translateIndustryOptions = useTranslator(
"crmModule",
"companies",
"industryOptions"
);
const translateCompanies = useTranslator("crmModule", "companies");

const industryOptions = useMemo(
() =>
Object.values(CrmIndustryEnum).map((industry) => ({
id: industry,
label: translateIndustryOptions([industry]),
value: industry
})),
[translateIndustryOptions]
const { industries } = useCrmStoreV2(
useShallow((store) => ({
industries: store.industries

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Claude · 🔴 Important

store.industries (here) and store.setIndustries (AddIndustryOption.tsx:24-25) do not exist on the CRM v2 store. crmDataSlice.ts defines only companies/contacts/deals/board/tasks/owners/stages/taskTypes, and SliceTypes.ts:CrmDataSliceTypes declares no industries or setIndustries. This is a TypeScript error, and at runtime industries would be undefined, so Object.values(industries) on line 44 throws a TypeError and the whole company modal white-screens. Add industries: CrmIndustryRecord and setIndustries: (industries: CrmIndustryRecord) => void to CrmDataSliceTypes and initialise/implement them in CrmDataSlice the same way taskTypes/setTaskTypes are.

}))
);

const [industrySearchTerm, setIndustrySearchTerm] = useState("");

const {
values,
errors,
Expand Down Expand Up @@ -76,8 +75,68 @@ const CompanyModalForm: FC<CompanyModalFormProps> = ({
? translateText(["validations", "companyExists"])
: errors.name;

const handleIndustryChange = (value: string) => {
setFieldValue("industry", value);
const industryOptions = useMemo(
() =>
getIndustryOptions(
industries,
translateCompanies,
industrySearchTerm,
true
),
[industries, translateCompanies, industrySearchTerm]
);

const renderIndustryOptionContent = (option: CrmIndustryOption) => {
if (option.id === ADD_NEW_INDUSTRY_OPTION_ID) {
return (
<AddIndustryOption
label={translateText(["labels", "addNewIndustry"], {
name: option.name
})}
/>
);
}

return option.name;
};

const industryDropdownItems: SearchableDropdownItem[] = industryOptions.map(
(option) => ({
id: option.id,
content: renderIndustryOptionContent(option)
})
);

const selectedIndustry =
values.industryId != null ? industries[values.industryId] : undefined;

const selectedIndustryLabel =
values.industryName ??
(selectedIndustry
? getIndustryDisplayName(selectedIndustry, translateCompanies)
: "");

const handleIndustrySearchChange = (e: ChangeEvent<HTMLInputElement>) => {
setIndustrySearchTerm(e.target.value);
};

const handleIndustrySelect = (item: SearchableDropdownItem) => {
if (item.id === ADD_NEW_INDUSTRY_OPTION_ID) {
setFieldValue("industryId", null);
setFieldValue("industryName", industrySearchTerm.trim());
setIndustrySearchTerm("");
return;
}

setFieldValue("industryId", Number(item.id));
setFieldValue("industryName", undefined);
setIndustrySearchTerm("");
};

const handleClearIndustry = () => {
setFieldValue("industryId", null);
setFieldValue("industryName", undefined);
setIndustrySearchTerm("");
};

return (
Expand Down Expand Up @@ -136,15 +195,19 @@ const CompanyModalForm: FC<CompanyModalFormProps> = ({
fullWidth
/>

<Dropdown
options={industryOptions}
value={values.industry}
onChange={handleIndustryChange}
<SelectableSearchField
id="company-industry-search"
label={translateText(["labels", "industry"])}
className="rounded-lg"
variant="primary"
ariaLabel={translateText(["ariaLabels", "industry"])}
width="100%"
placeholder={translateText(["placeholders", "industry"])}
selectedValue={selectedIndustryLabel}
onClear={handleClearIndustry}
clearAriaLabel={translateText(["ariaLabels", "clearIndustry"])}
fieldAriaLabel={translateText(["ariaLabels", "industry"])}
searchValue={industrySearchTerm}
onSearchChange={handleIndustrySearchChange}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Claude · 🔴 Important

Clearing the search box closes the dropdown and it cannot be reopened without blurring and refocusing. SearchableDropdown.handleInputChange does setIsOpen(e.target.value.trim().length > 0), and the onFocus handler that would reopen it is on the wrapper div, so it will not fire again while the input is already focused. Every other consumer of SelectableSearchField (e.g. TaskModalForm owner/contact/deal) is backed by a server lookup that returns nothing for an empty term, so this never mattered before; this field is the first one where the empty term is expected to show the full list. Fix by making the dropdown stay open when items.length > 0 on an empty term — e.g. pass an explicit open handler, or change handleInputChange to setIsOpen(true) and let isDropdownOpen gate on items/emptyMessage as it already does.

items={industryDropdownItems}
onSelect={handleIndustrySelect}
emptyMessage={translateText(["emptyStates", "noIndustries"])}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Claude · 🟡 Suggestion

The new emptyStates.noIndustries message is effectively unreachable in the scenario it was written for. items is only empty when the search term is empty AND the store holds no industries; as soon as the user types anything that matches nothing, the add-industry row is pushed so items.length === 1 and SearchableDropdown renders the list instead of emptyMessage. So 'No industries found' shows only on an empty search box with an empty store, which reads as a bug rather than an empty state. Either drop the prop or render the message as a non-selectable header above the add row.

/>

<div className="flex flex-row justify-end py-[0.85rem] gap-[1rem]">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,21 @@ import {
} from "@rootcodelabs/skapp-ui";
import { FC } from "react";

import { useTranslator } from "~community/common/hooks/useTranslator";
import { IconName } from "~community/common/types/IconTypes";
import { openInNewTab } from "~community/common/utils/commonUtil";
import SidePanelHeaderInfoItem from "~community/crm/v2/components/molecules/SidePanelHeaderInfoItem/SidePanelHeaderInfoItem";
import { CrmCompanyEntity } from "~community/crm/v2/types/CrmCommonTypes";

interface SidePanelCompanyHeaderProps {
company: CrmCompanyEntity;
industryName: string;
}

const SidePanelCompanyHeader: FC<SidePanelCompanyHeaderProps> = ({
company
company,
industryName
}) => {
const translateText = useTranslator(
"crmModule",
"companies",
"industryOptions"
);

const { website, contactNumber, address, industry } = company;
const { website, contactNumber, address } = company;

return (
<div className="flex items-center gap-12 flex-wrap">
Expand Down Expand Up @@ -67,18 +62,16 @@ const SidePanelCompanyHeader: FC<SidePanelCompanyHeaderProps> = ({
value={address}
/>
)}
{industry && (
<SidePanelHeaderInfoItem
icon={
<OfficeIcon
width="20"
height="20"
fill="var(--color-secondary-icon)"
/>
}
value={translateText([industry])}
/>
)}
<SidePanelHeaderInfoItem
icon={
<OfficeIcon
width="20"
height="20"
fill="var(--color-secondary-icon)"
/>
}
value={industryName}
/>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ import SidePanelCompanyHeader from "~community/crm/v2/components/molecules/SideP
import SidePanelMetricCards from "~community/crm/v2/components/molecules/SidePanelMetricCards/SidePanelMetricCards";
import SidePanelHeaderActionsSkeleton from "~community/crm/v2/components/molecules/SidePanelSkeleton/SidePanelHeaderActionsSkeleton";
import SidePanelHeaderSkeleton from "~community/crm/v2/components/molecules/SidePanelSkeleton/SidePanelHeaderSkeleton";
import { CrmSidePanelTabEnum } from "~community/crm/v2/enums/common";
import {
CrmIndustryEnum,
CrmSidePanelTabEnum
} from "~community/crm/v2/enums/common";
import { useCrmStoreV2 } from "~community/crm/v2/store/store";
import {
CrmModalTypes,
CrmSidePanelTypes
} from "~community/crm/v2/types/CrmTypes";
import {
getCompanyMetricItems,
getIndustryDisplayName,
updateCompany
} from "~community/crm/v2/utils/companyUtil";

Expand All @@ -38,7 +42,7 @@ interface CompanySidePanelProps {
}

const CompanySidePanel: FC<CompanySidePanelProps> = ({ companyId }) => {
const translateText = useTranslator("crmModule", "companies", "sidePanel");
const translateText = useTranslator("crmModule", "companies");
const { isCrmSalesManager } = useSessionData();

const [activeTab, setActiveTab] = useState<CrmSidePanelTabEnum>(
Expand All @@ -47,6 +51,7 @@ const CompanySidePanel: FC<CompanySidePanelProps> = ({ companyId }) => {

const {
companies,
industries,
isCrmSidePanelOpen,
crmSidePanelType,
setCompanies,
Expand All @@ -57,6 +62,7 @@ const CompanySidePanel: FC<CompanySidePanelProps> = ({ companyId }) => {
} = useCrmStoreV2(
useShallow((store) => ({
companies: store.companies,
industries: store.industries,
isCrmSidePanelOpen: store.isCrmSidePanelOpen,
crmSidePanelType: store.crmSidePanelType,
setCompanies: store.setCompanies,
Expand Down Expand Up @@ -86,6 +92,12 @@ const CompanySidePanel: FC<CompanySidePanelProps> = ({ companyId }) => {
}, [fetchedCompany, fetchedMetrics]);

const company = companies[companyId];
const industry =
company?.industryId != null ? industries[company.industryId] : undefined;

const industryName = industry
? getIndustryDisplayName(industry, translateText)
: translateText(["industryOptions", CrmIndustryEnum.NONE]);

const isOpen =
isCrmSidePanelOpen &&
Expand All @@ -100,7 +112,7 @@ const CompanySidePanel: FC<CompanySidePanelProps> = ({ companyId }) => {
() => [
{
id: "edit",
label: translateText(["editCompany"]),
label: translateText(["sidePanel", "editCompany"]),
icon: { start: <EditIcon width="16px" height="16px" /> },
onClick: () => {
setCompanyModalType(CrmModalTypes.EDIT_COMPANY_MODAL);
Expand All @@ -109,7 +121,7 @@ const CompanySidePanel: FC<CompanySidePanelProps> = ({ companyId }) => {
},
{
id: "delete",
label: translateText(["deleteCompany"]),
label: translateText(["sidePanel", "deleteCompany"]),
icon: {
start: (
<DeleteButtonIcon
Expand All @@ -133,15 +145,15 @@ const CompanySidePanel: FC<CompanySidePanelProps> = ({ companyId }) => {
const tabs: TabItem[] = [
{
id: CrmSidePanelTabEnum.TASKS,
label: translateText(["tabs", "tasks"])
label: translateText(["sidePanel", "tabs", "tasks"])
},
{
id: CrmSidePanelTabEnum.DEALS,
label: translateText(["tabs", "deals"])
label: translateText(["sidePanel", "tabs", "deals"])
},
{
id: CrmSidePanelTabEnum.CONTACTS,
label: translateText(["tabs", "contacts"])
label: translateText(["sidePanel", "tabs", "contacts"])
}
];

Expand Down Expand Up @@ -173,7 +185,10 @@ const CompanySidePanel: FC<CompanySidePanelProps> = ({ companyId }) => {
<CompanySidePanelSkeleton />
) : (
<>
<SidePanelCompanyHeader company={company} />
<SidePanelCompanyHeader
company={company}
industryName={industryName}
/>

<SidePanelMetricCards
metrics={getCompanyMetricItems(company, translateText)}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/community/crm/v2/constants/commonConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export const COMPANY_PAGE_SIZE = 10;
export const DEAL_PAGE_SIZE = 15;
export const CONTACT_PAGE_SIZE = 10;
export const DEFAULT_LOOKUP_PAGE_SIZE = 50;

export const ADD_NEW_INDUSTRY_OPTION_ID = "ADD_NEW_INDUSTRY";
3 changes: 2 additions & 1 deletion frontend/src/community/crm/v2/types/CrmCommonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
export interface CrmCompanyEntity {
id?: number;
name?: string;
industry?: CrmIndustryEnum;
industryId?: number | null;
industryName?: string;
website?: string;
address?: string;
contactNumber?: string;
Expand Down
Loading
Loading