fix(intake): classify owner dropdown options
This commit is contained in:
parent
cec4b449ad
commit
30b1c5740d
@ -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 },
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 },
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ export async function GET(request: NextRequest) {
|
||||
request,
|
||||
async (ctx) => {
|
||||
const names =
|
||||
await listIntakeOwnerOptionNamesByDepartmentScope("brandPlanning");
|
||||
await listIntakeOwnerOptionNamesByDepartmentScope("merchandisePlanning");
|
||||
|
||||
return jsonSuccess(
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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: [],
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user