fix(intake): classify owner dropdown options

This commit is contained in:
WuYongJie 2026-07-07 14:07:04 +08:00
parent cec4b449ad
commit 30b1c5740d
14 changed files with 301 additions and 53 deletions

View File

@ -0,0 +1,23 @@
import { NextRequest } from "next/server";
import { handleRequest, jsonSuccess } from "@/server/core/response";
import { listIntakeOwnerOptionNamesByDepartmentScope } from "@/server/services/intake-owner-options.service";
export async function GET(request: NextRequest) {
return handleRequest(
request,
async (ctx) => {
const names =
await listIntakeOwnerOptionNamesByDepartmentScope("brandPlanning");
return jsonSuccess(
{
names,
},
200,
ctx.requestId,
);
},
{ requireAuth: true },
);
}

View File

@ -0,0 +1,23 @@
import { NextRequest } from "next/server";
import { handleRequest, jsonSuccess } from "@/server/core/response";
import { listIntakeOwnerOptionNamesByDepartmentScope } from "@/server/services/intake-owner-options.service";
export async function GET(request: NextRequest) {
return handleRequest(
request,
async (ctx) => {
const names =
await listIntakeOwnerOptionNamesByDepartmentScope("externalIntake");
return jsonSuccess(
{
names,
},
200,
ctx.requestId,
);
},
{ requireAuth: true },
);
}

View File

@ -7,7 +7,7 @@ export async function GET(request: NextRequest) {
request,
async (ctx) => {
const names =
await listIntakeOwnerOptionNamesByDepartmentScope("brandPlanning");
await listIntakeOwnerOptionNamesByDepartmentScope("merchandisePlanning");
return jsonSuccess(
{

View File

@ -636,7 +636,9 @@ export default function NonIntegratedDetailClient({
onProductSavedRef.current = onProductSaved;
}, [onProductSaved]);
const {
brandPlanningNameOptions,
designerNameOptions,
externalIntakeNameOptions,
planningOwnerNameOptions,
spaceDesignerNameOptions,
operationOwnerNameOptions,
@ -1389,6 +1391,7 @@ export default function NonIntegratedDetailClient({
setActiveDownstreamSkuBatch(batch);
}, []);
const {
basicInfoBrandPlanningOptions,
basicInfoDesignerOptions,
basicInfoOperationOwnerOptions,
basicInfoPlanningOwnerOptions,
@ -1397,7 +1400,9 @@ export default function NonIntegratedDetailClient({
basicInfoSecondaryCategoryOptions,
basicInfoSpaceDesignerOptions,
} = buildNonIntegratedBasicInfoOptions({
brandPlanningNameOptions,
designerNameOptions,
externalIntakeNameOptions,
formValues,
openingReference,
operationOwnerNameOptions,
@ -5066,6 +5071,9 @@ export default function NonIntegratedDetailClient({
actionGroupClassName={
DOCUMENT_WORKSPACE_ACTION_GROUP_CLASS
}
basicInfoBrandPlanningOptions={
basicInfoBrandPlanningOptions
}
basicInfoDesignerOptions={basicInfoDesignerOptions}
basicInfoOperationOwnerOptions={
basicInfoOperationOwnerOptions

View File

@ -5,6 +5,7 @@ import {
InlineSelect,
InlineSuggestionInput,
SearchableInlineSelect,
type InlineSelectOption,
} from "./InlineControls";
type BasicFieldType = "text" | "textarea" | "select";
@ -51,7 +52,7 @@ export function BasicField({
required?: boolean;
type?: BasicFieldType;
placeholder?: string;
options?: readonly string[];
options?: readonly InlineSelectOption[];
value: string;
readOnly?: boolean;
compact?: boolean;
@ -61,6 +62,10 @@ export function BasicField({
onCommit?: () => void;
onChange: (value: string) => void;
}) {
const suggestionOptions = (options ?? []).filter(
(option): option is string => typeof option === "string",
);
if (type === "select") {
return (
<div
@ -79,6 +84,7 @@ export function BasicField({
placeholder={placeholder}
readOnly={readOnly}
includeEmptyOption={includeEmptyOption}
maxVisibleOptions={10}
onChange={onChange}
inputClassName={`h-9 w-full rounded-apple-lg border border-apple-separator px-3 pr-9 text-sm text-apple-label-primary outline-none disabled:cursor-not-allowed ${
readOnly
@ -148,14 +154,14 @@ export function BasicField({
<InlineSuggestionInput
ariaLabel={label}
value={value}
options={options}
options={suggestionOptions}
placeholder={placeholder}
readOnly={readOnly}
filterOptions={Boolean((options ?? []).length)}
filterOptions={suggestionOptions.length > 0}
onCommit={onCommit}
onChange={onChange}
inputClassName={`h-9 w-full rounded-apple-lg border border-apple-separator px-3 text-sm text-apple-label-primary outline-none ${
!readOnly && (options ?? []).length > 0 ? "pr-9" : ""
!readOnly && suggestionOptions.length > 0 ? "pr-9" : ""
} ${
readOnly
? "bg-apple-bg-primary text-apple-label-secondary"

View File

@ -287,15 +287,26 @@ export type InlineSelectOption =
value: string;
label?: string;
disabled?: boolean;
keywords?: readonly string[];
};
function normalizeInlineSelectOptions(
options: readonly InlineSelectOption[] | undefined,
): { value: string; label: string; disabled?: boolean }[] {
): {
value: string;
label: string;
disabled?: boolean;
keywords?: readonly string[];
}[] {
return (options ?? []).map((option) =>
typeof option === "string"
? { value: option, label: option }
: { value: option.value, label: option.label ?? option.value, disabled: option.disabled },
: {
value: option.value,
label: option.label ?? option.value,
disabled: option.disabled,
keywords: option.keywords,
},
);
}
@ -536,16 +547,19 @@ export function SearchableInlineSelect({
if (!shouldFilterOptions) return true;
return (
option.label.toLowerCase().includes(normalizedQuery) ||
option.value.toLowerCase().includes(normalizedQuery)
option.value.toLowerCase().includes(normalizedQuery) ||
(option.keywords ?? []).some((keyword) =>
keyword.toLowerCase().includes(normalizedQuery),
)
);
});
const normalizedMaxVisibleOptions =
typeof maxVisibleOptions === "number" && Number.isFinite(maxVisibleOptions)
? Math.max(1, Math.floor(maxVisibleOptions))
: null;
const visibleOptions = normalizedMaxVisibleOptions
? filteredOptions.slice(0, normalizedMaxVisibleOptions)
: filteredOptions;
const panelMaxHeight = normalizedMaxVisibleOptions
? normalizedMaxVisibleOptions * 28 + 8
: null;
const hasOptions =
!readOnly && (selectOptions.length > 0 || Boolean(onQueryChange));
const hasVisiblePanel = hasOptions && selectOptions.length > 0;
@ -625,7 +639,7 @@ export function SearchableInlineSelect({
}
if (event.key === "Enter") {
event.preventDefault();
const firstEnabledOption = visibleOptions.find(
const firstEnabledOption = filteredOptions.find(
(option) => !option.disabled,
);
if (firstEnabledOption) {
@ -673,12 +687,14 @@ export function SearchableInlineSelect({
top: floatingPosition.top,
minWidth: floatingPosition.width,
maxWidth: floatingPosition.maxWidth,
maxHeight: floatingPosition.maxHeight,
maxHeight: panelMaxHeight
? Math.min(floatingPosition.maxHeight, panelMaxHeight)
: floatingPosition.maxHeight,
zIndex: 9999,
}}
>
{visibleOptions.length > 0 ? (
visibleOptions.map((option) => (
{filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<button
key={`${option.value}-${option.label}`}
type="button"

View File

@ -8,7 +8,9 @@ describe("buildNonIntegratedBasicInfoOptions", () => {
formValues.salesCategory = "架类";
const options = buildNonIntegratedBasicInfoOptions({
brandPlanningNameOptions: [],
designerNameOptions: [],
externalIntakeNameOptions: [],
formValues,
openingReference: createEmptyNonIntegratedOpeningReference(),
operationOwnerNameOptions: [],
@ -27,7 +29,9 @@ describe("buildNonIntegratedBasicInfoOptions", () => {
formValues.productCategory = "自定义类目";
const options = buildNonIntegratedBasicInfoOptions({
brandPlanningNameOptions: [],
designerNameOptions: [],
externalIntakeNameOptions: [],
formValues,
openingReference: createEmptyNonIntegratedOpeningReference(),
operationOwnerNameOptions: [],

View File

@ -7,7 +7,9 @@ import type { FormValues } from "./productFormValueHelpers";
import { getUniqueNonEmptyStrings } from "./detailValueHelpers";
export function buildNonIntegratedBasicInfoOptions({
brandPlanningNameOptions,
designerNameOptions,
externalIntakeNameOptions,
formValues,
openingReference,
operationOwnerNameOptions,
@ -15,7 +17,9 @@ export function buildNonIntegratedBasicInfoOptions({
purchasingOwnerNameOptions,
spaceDesignerNameOptions,
}: {
brandPlanningNameOptions: string[];
designerNameOptions: string[];
externalIntakeNameOptions: string[];
formValues: FormValues;
openingReference: NonIntegratedOpeningReference;
operationOwnerNameOptions: string[];
@ -45,15 +49,21 @@ export function buildNonIntegratedBasicInfoOptions({
basicInfoPlanningOwnerOptions: getUniqueNonEmptyStrings([
...planningOwnerNameOptions,
formValues.productPlanning,
formValues.brandPlanning,
openingReference.planningOwner,
]),
basicInfoBrandPlanningOptions: getUniqueNonEmptyStrings([
...brandPlanningNameOptions,
formValues.brandPlanning,
]),
basicInfoOperationOwnerOptions: getUniqueNonEmptyStrings([
...operationOwnerNameOptions,
formValues.spuOwner,
formValues.operator,
formValues.operationsOwner,
]),
basicInfoExternalIntakeOptions: getUniqueNonEmptyStrings([
...externalIntakeNameOptions,
]),
basicInfoSpaceDesignerOptions: getUniqueNonEmptyStrings([
...spaceDesignerNameOptions,
formValues.visualPlanning,

View File

@ -0,0 +1,23 @@
import type { InlineSelectOption } from "./components/InlineControls";
import { getUniqueNonEmptyStrings } from "./detailValueHelpers";
const PERSON_NAME_ALIASES: Record<string, string> = {
宁柯: "宁珂",
许褚: "许储",
};
export function buildPersonSelectOptions(
names: readonly string[],
): InlineSelectOption[] {
const normalizedNames = getUniqueNonEmptyStrings(names);
return normalizedNames.map((name) => {
const aliases = Object.entries(PERSON_NAME_ALIASES)
.filter(([, standardName]) => standardName === name)
.map(([alias]) => alias);
return aliases.length > 0
? { value: name, label: name, keywords: aliases }
: name;
});
}

View File

@ -29,6 +29,9 @@ export function useNonIntegratedDirectoryOptions() {
const [planningOwnerNameOptions, setPlanningOwnerNameOptions] = useState<
string[]
>([]);
const [brandPlanningNameOptions, setBrandPlanningNameOptions] = useState<
string[]
>([]);
const [spaceDesignerNameOptions, setSpaceDesignerNameOptions] = useState<
string[]
>([]);
@ -38,6 +41,9 @@ export function useNonIntegratedDirectoryOptions() {
const [purchasingOwnerNameOptions, setPurchasingOwnerNameOptions] = useState<
string[]
>([]);
const [externalIntakeNameOptions, setExternalIntakeNameOptions] = useState<
string[]
>([]);
const [supplierDirectoryOptions, setSupplierDirectoryOptions] =
useState<SupplierDirectoryEntry[]>(SUPPLIER_DIRECTORY);
const [warehouseDirectoryOptions, setWarehouseDirectoryOptions] = useState<
@ -54,23 +60,29 @@ export function useNonIntegratedDirectoryOptions() {
void Promise.all([
fetchNameOptions("/api/intake/designer-options"),
fetchNameOptions("/api/intake/planning-options"),
fetchNameOptions("/api/intake/brand-planning-options"),
fetchNameOptions("/api/intake/space-options"),
fetchNameOptions("/api/intake/operation-options"),
fetchNameOptions("/api/intake/purchasing-options"),
fetchNameOptions("/api/intake/external-intake-options"),
]).then(
([
nextDesignerNameOptions,
nextPlanningOwnerNameOptions,
nextBrandPlanningNameOptions,
nextSpaceDesignerNameOptions,
nextOperationOwnerNameOptions,
nextPurchasingOwnerNameOptions,
nextExternalIntakeNameOptions,
]) => {
if (cancelled) return;
setDesignerNameOptions(nextDesignerNameOptions);
setPlanningOwnerNameOptions(nextPlanningOwnerNameOptions);
setBrandPlanningNameOptions(nextBrandPlanningNameOptions);
setSpaceDesignerNameOptions(nextSpaceDesignerNameOptions);
setOperationOwnerNameOptions(nextOperationOwnerNameOptions);
setPurchasingOwnerNameOptions(nextPurchasingOwnerNameOptions);
setExternalIntakeNameOptions(nextExternalIntakeNameOptions);
},
);
@ -137,9 +149,11 @@ export function useNonIntegratedDirectoryOptions() {
return {
designerNameOptions,
planningOwnerNameOptions,
brandPlanningNameOptions,
spaceDesignerNameOptions,
operationOwnerNameOptions,
purchasingOwnerNameOptions,
externalIntakeNameOptions,
supplierDirectoryOptions,
warehouseDirectoryOptions,
supplierNameOptions,

View File

@ -10,6 +10,7 @@ import {
import { UploadField } from "../../components/UploadControls";
import { SectionCard } from "../../components/WorkspaceShell";
import type { NonIntegratedOpeningReference } from "../../nonIntegratedStorage";
import { buildPersonSelectOptions } from "../../personNameOptions";
import {
getAutoProductYear,
OPENING_REFERENCE_DEMAND_TYPE_OPTIONS,
@ -112,6 +113,10 @@ export function BasicInfoCreateSection({
onUpdateField: (key: keyof FormValues, value: string) => void;
onUpdateManualSeriesProductSpu: (value: string) => void;
}) {
const designerOptions = buildPersonSelectOptions(basicInfoDesignerOptions);
const planningOwnerOptions = buildPersonSelectOptions(
basicInfoPlanningOwnerOptions,
);
const handlePreview = () =>
formValues.previewImage ? onPreviewProductImage() : undefined;
const openingCodeLookupMessageClassName =
@ -179,8 +184,10 @@ export function BasicInfoCreateSection({
<BasicField
label="外观设计师"
required
type="select"
searchableSelect
placeholder="请输入外观设计师"
options={basicInfoDesignerOptions}
options={designerOptions}
value={openingReference.designer}
readOnly={isReadOnly}
compact
@ -270,8 +277,10 @@ export function BasicInfoCreateSection({
<BasicField
label="外观设计师"
required={!isSeriesComponentCreate}
type="select"
searchableSelect
placeholder="请输入外观设计师"
options={basicInfoDesignerOptions}
options={designerOptions}
value={openingReference.designer}
readOnly={isReadOnly}
compact
@ -444,8 +453,10 @@ export function BasicInfoCreateSection({
<BasicField
label="商品企划"
required={!isSeriesComponentCreate}
type="select"
searchableSelect
placeholder="请输入商品企划"
options={basicInfoPlanningOwnerOptions}
options={planningOwnerOptions}
value={openingReference.planningOwner}
readOnly={isReadOnly}
compact

View File

@ -17,6 +17,7 @@ import {
NON_INTEGRATED_PRODUCT_TAG_OPTIONS,
type NonIntegratedOpeningReference,
} from "../../nonIntegratedStorage";
import { buildPersonSelectOptions } from "../../personNameOptions";
import {
BASIC_INFO_CORE_FIELDS,
BRAND_OPTIONS,
@ -24,8 +25,21 @@ import {
} from "./basicInfoConfig";
import type { FormValues } from "./productFormValueHelpers";
function isPersonFieldKey(key: keyof FormValues) {
return (
key === "designer" ||
key === "spuOwner" ||
key === "operator" ||
key === "productPlanning" ||
key === "brandPlanning" ||
key === "purchaseOwner" ||
key === "visualPlanning"
);
}
export function BasicInfoDetailSection({
actionGroupClassName,
basicInfoBrandPlanningOptions,
basicInfoDesignerOptions,
basicInfoOperationOwnerOptions,
basicInfoPlanningOwnerOptions,
@ -52,6 +66,7 @@ export function BasicInfoDetailSection({
onUpdateField,
}: {
actionGroupClassName: string;
basicInfoBrandPlanningOptions: string[];
basicInfoDesignerOptions: string[];
basicInfoOperationOwnerOptions: string[];
basicInfoPlanningOwnerOptions: string[];
@ -77,6 +92,23 @@ export function BasicInfoDetailSection({
onStructureDesignerChange: (value: string) => void;
onUpdateField: (key: keyof FormValues, value: string) => void;
}) {
const brandPlanningOptions = buildPersonSelectOptions(
basicInfoBrandPlanningOptions,
);
const designerOptions = buildPersonSelectOptions(basicInfoDesignerOptions);
const operationOwnerOptions = buildPersonSelectOptions(
basicInfoOperationOwnerOptions,
);
const planningOwnerOptions = buildPersonSelectOptions(
basicInfoPlanningOwnerOptions,
);
const purchasingOwnerOptions = buildPersonSelectOptions(
basicInfoPurchasingOwnerOptions,
);
const spaceDesignerOptions = buildPersonSelectOptions(
basicInfoSpaceDesignerOptions,
);
return (
<SectionCard
title={developmentDetailTitle}
@ -154,7 +186,8 @@ export function BasicInfoDetailSection({
<CreateFieldShell>
<BasicField
label={field.label}
type={field.type}
type={isPersonFieldKey(field.key) ? "select" : field.type}
searchableSelect={isPersonFieldKey(field.key)}
placeholder={field.placeholder}
options={
field.type === "select"
@ -164,17 +197,18 @@ export function BasicInfoDetailSection({
: field.key === "productCategory"
? basicInfoProductCategoryOptions
: field.key === "designer"
? basicInfoDesignerOptions
? designerOptions
: field.key === "visualPlanning"
? basicInfoSpaceDesignerOptions
: field.key === "productPlanning" ||
field.key === "brandPlanning"
? basicInfoPlanningOwnerOptions
? spaceDesignerOptions
: field.key === "productPlanning"
? planningOwnerOptions
: field.key === "brandPlanning"
? brandPlanningOptions
: field.key === "spuOwner" ||
field.key === "operator"
? basicInfoOperationOwnerOptions
? operationOwnerOptions
: field.key === "purchaseOwner"
? basicInfoPurchasingOwnerOptions
? purchasingOwnerOptions
: field.options
}
value={
@ -198,7 +232,9 @@ export function BasicInfoDetailSection({
<CreateFieldShell>
<BasicField
label="结构设计师"
options={basicInfoDesignerOptions}
type="select"
searchableSelect
options={designerOptions}
value={openingReference.structureDesigner}
readOnly={isReadOnly}
onChange={onStructureDesignerChange}

View File

@ -8,6 +8,10 @@ import {
CopyProductDialog,
ImagePreviewDialog,
} from "@/app/intake/components/IntakeSharedPanels";
import {
SearchableInlineSelect,
type InlineSelectOption,
} from "@/app/intake/non-integrated/components/InlineControls";
import { SpuLabel } from "@/app/intake/components/SpuLabel";
import { HoverImagePreview } from "@/app/intake/non-integrated/components/HoverImagePreview";
import { DocumentStatusMark } from "@/app/intake/product-development/DocumentStatusMark";
@ -82,6 +86,8 @@ import {
canEditNonIntegratedProduct,
canOpenNonIntegratedProductForEdit,
} from "@/app/intake/product-development/nonIntegratedProductAccess";
import { buildPersonSelectOptions } from "@/app/intake/non-integrated/personNameOptions";
import { useNonIntegratedDirectoryOptions } from "@/app/intake/non-integrated/useNonIntegratedDirectoryOptions";
function createRestoredNonIntegratedProduct(
product: NonIntegratedProductRecord,
): NonIntegratedProductRecord {
@ -115,6 +121,10 @@ const TABLE_HEADER_CELL_CLASS =
const TABLE_HEADER_CENTER_CELL_CLASS =
"sticky top-0 z-10 bg-apple-bg-primary px-3 py-2.5 text-center font-apple-medium whitespace-nowrap";
function buildPersonFilterOptions(names: readonly string[]): InlineSelectOption[] {
return buildPersonSelectOptions(names);
}
function normalizeCategoryFilterValue(value: string, options: readonly string[]) {
const trimmedValue = value.trim();
if (!trimmedValue) return "";
@ -247,6 +257,13 @@ export function NonIntegratedPendingDetail({
const skipNextListStatePersistRef = useRef(false);
const initialListRequestHandledRef = useRef(false);
const shouldUseSharedNonIntegratedApi = process.env.NODE_ENV !== "test";
const {
designerNameOptions,
operationOwnerNameOptions,
planningOwnerNameOptions,
purchasingOwnerNameOptions,
spaceDesignerNameOptions,
} = useNonIntegratedDirectoryOptions();
const effectiveProductKind = fixedProductKind ?? appliedQuery.productKind;
const hasCreateAction = Boolean(
onOpenNewNonIntegratedProductWorkspace ||
@ -844,7 +861,8 @@ export function NonIntegratedPendingDetail({
label: ReactNode;
ariaLabel: string;
placeholder: string;
options?: Array<{ value: string; label: string }>;
options?: readonly InlineSelectOption[];
searchable?: boolean;
}> = [
{ key: "spu", label: <SpuLabel />, ariaLabel: "SPU", placeholder: "搜索 SPU" },
{
@ -852,36 +870,48 @@ export function NonIntegratedPendingDetail({
label: "外观设计师",
ariaLabel: "外观设计师",
placeholder: "搜索外观设计师",
options: buildPersonFilterOptions(designerNameOptions),
searchable: true,
},
{
key: "structureDesigner",
label: "结构设计师",
ariaLabel: "结构设计师",
placeholder: "搜索结构设计师",
options: buildPersonFilterOptions(designerNameOptions),
searchable: true,
},
{
key: "spuOwner",
label: "SPU负责人",
ariaLabel: "SPU负责人",
placeholder: "搜索SPU负责人",
options: buildPersonFilterOptions(operationOwnerNameOptions),
searchable: true,
},
{
key: "planningOwner",
label: "商品企划",
ariaLabel: "商品企划",
placeholder: "搜索商品企划",
options: buildPersonFilterOptions(planningOwnerNameOptions),
searchable: true,
},
{
key: "spaceDesigner",
label: "空间设计师",
ariaLabel: "空间设计师",
placeholder: "搜索空间设计师",
options: buildPersonFilterOptions(spaceDesignerNameOptions),
searchable: true,
},
{
key: "purchaseOwner",
label: "采购负责人",
ariaLabel: "采购负责人",
placeholder: "搜索采购负责人",
options: buildPersonFilterOptions(purchasingOwnerNameOptions),
searchable: true,
},
];
const stockFilterFields: typeof productFilterFields = [
@ -1055,7 +1085,22 @@ export function NonIntegratedPendingDetail({
className="min-w-[112px] space-y-1.5 text-xs text-apple-label-secondary max-md:space-y-0 md:min-w-0"
>
<span className="max-md:hidden">{filter.label}</span>
{filter.options ? (
{filter.options && filter.searchable ? (
<SearchableInlineSelect
ariaLabel={filter.ariaLabel}
value={queryDraft[filter.key]}
options={filter.options}
placeholder={filter.placeholder}
includeEmptyOption
onChange={(value) =>
setQueryDraft((current) => ({
...current,
[filter.key]: value,
}))
}
inputClassName="h-8 w-full rounded-apple-md border border-apple-separator bg-apple-bg-secondary px-2.5 pr-8 text-xs text-apple-label-primary outline-none max-md:h-7 max-md:px-2 max-md:pr-8 max-md:text-[11px]"
/>
) : filter.options ? (
<select
aria-label={filter.ariaLabel}
value={queryDraft[filter.key]}
@ -1067,11 +1112,21 @@ export function NonIntegratedPendingDetail({
}
className="h-8 w-full rounded-apple-md border border-apple-separator bg-apple-bg-secondary px-2.5 text-xs text-apple-label-primary outline-none max-md:h-7 max-md:px-2 max-md:text-[11px]"
>
{filter.options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
{filter.options.map((option) => {
const normalizedOption =
typeof option === "string"
? { value: option, label: option }
: option;
return (
<option
key={normalizedOption.value}
value={normalizedOption.value}
>
{normalizedOption.label}
</option>
))}
);
})}
</select>
) : (
<input

View File

@ -2,10 +2,12 @@ import { prisma } from "@/lib/prisma";
type IntakeOwnerDepartmentScope =
| "productResearch"
| "merchandisePlanning"
| "brandPlanning"
| "spaceVisual"
| "operation"
| "purchasing";
| "purchasing"
| "externalIntake";
function getUniqueNames(values: Array<string | null | undefined>) {
return Array.from(
@ -33,7 +35,7 @@ function departmentTextMatchesScope(
return text.includes("产品研发部");
}
if (scope === "brandPlanning") {
if (scope === "merchandisePlanning") {
return text.includes("品牌中心") && text.includes("商品企划部");
}
@ -41,11 +43,23 @@ function departmentTextMatchesScope(
return text.includes("品牌中心") && text.includes("空间视觉部");
}
if (scope === "brandPlanning") {
return (
text.includes("品牌中心") &&
!text.includes("商品企划部") &&
!text.includes("空间视觉部")
);
}
if (scope === "operation") {
return text.includes("运营中心");
}
return text.includes("供应链中心");
if (scope === "purchasing") {
return text.includes("供应链中心");
}
return false;
});
}
@ -58,6 +72,7 @@ export async function listIntakeOwnerOptionNamesByDepartmentScope(
},
select: {
name: true,
businessRoles: true,
department: {
select: {
name: true,
@ -81,20 +96,24 @@ export async function listIntakeOwnerOptionNamesByDepartmentScope(
},
});
return getUniqueNames(
users
.filter((user) =>
departmentTextMatchesScope(
[
user.department?.name,
user.department?.fullPath,
user.team?.name,
user.team?.department?.name,
user.team?.department?.fullPath,
],
scope,
),
)
.map((user) => user.name),
);
const scopedNames = users
.filter((user) => {
if (scope === "externalIntake") {
return user.businessRoles.includes("EXTERNAL_INTAKE");
}
return departmentTextMatchesScope(
[
user.department?.name,
user.department?.fullPath,
user.team?.name,
user.team?.department?.name,
user.team?.department?.fullPath,
],
scope,
);
})
.map((user) => user.name);
return getUniqueNames(scopedNames);
}