feat(intake): 完善品牌报备模板与已报备流程
This commit is contained in:
parent
f178e9cceb
commit
be49070962
@ -16,6 +16,10 @@ import {
|
||||
type BrandFilingPlanningLineItem,
|
||||
type BrandFilingPlanningRecord,
|
||||
} from "@/app/intake/brand-filing/brandFilingTypes";
|
||||
import {
|
||||
buildRequiredFieldsMessage,
|
||||
validateBrandFilingPlanningRequiredFields,
|
||||
} from "@/app/intake/brand-filing/brandFilingRequiredValidation";
|
||||
import type { NonIntegratedProductRecord } from "@/app/intake/non-integrated/nonIntegratedStorage";
|
||||
|
||||
export function BrandFilingPlanningTableDialog({
|
||||
@ -156,6 +160,23 @@ function BrandFilingPlanningTableDialogContent({
|
||||
);
|
||||
};
|
||||
|
||||
const getMissingRequiredFields = () =>
|
||||
validateBrandFilingPlanningRequiredFields(form, lineItems);
|
||||
|
||||
const handleSaveDraft = () => {
|
||||
onSave(form, lineItems, completed && getMissingRequiredFields().length === 0);
|
||||
};
|
||||
|
||||
const handleSaveAndConfirm = () => {
|
||||
const missingFields = getMissingRequiredFields();
|
||||
if (missingFields.length > 0) {
|
||||
window.alert(buildRequiredFieldsMessage("企划表", missingFields));
|
||||
return;
|
||||
}
|
||||
|
||||
onSave(form, lineItems, true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
@ -221,7 +242,7 @@ function BrandFilingPlanningTableDialogContent({
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={() => onSave(form, lineItems, completed)}
|
||||
onClick={handleSaveDraft}
|
||||
className="inline-flex h-8 items-center rounded-[6px] border border-[var(--obs-shell-border)] bg-white px-4 text-[12px] font-apple-semibold text-[var(--obs-shell-control-text)] transition hover:bg-[var(--obs-shell-hover-bg)] hover:text-[var(--obs-shell-title)] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
保存
|
||||
@ -229,7 +250,7 @@ function BrandFilingPlanningTableDialogContent({
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={() => onSave(form, lineItems, true)}
|
||||
onClick={handleSaveAndConfirm}
|
||||
className="inline-flex h-8 items-center rounded-[6px] border border-[var(--obs-shell-action-border)] bg-[var(--obs-shell-action-bg)] px-4 text-[12px] font-apple-semibold text-[var(--obs-shell-action-text)] transition hover:bg-[var(--obs-shell-action-hover-bg)] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
保存并确认
|
||||
|
||||
@ -33,6 +33,15 @@ function SectionBand({ title }: { title: string }) {
|
||||
return <div className={SECTION_BAND_CLASS}>{title}</div>;
|
||||
}
|
||||
|
||||
function RequiredFieldLabel({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<span className="inline-flex items-center justify-center gap-0.5">
|
||||
<span>{children}</span>
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function LabelCell({
|
||||
children,
|
||||
className = "",
|
||||
@ -159,6 +168,16 @@ function PlanSectionRow({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<InputCell
|
||||
value={displayRow.brandItemCode}
|
||||
onChange={(event) =>
|
||||
onLineItemChange(
|
||||
displayRow.itemCode,
|
||||
"brandItemCode",
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
{productHasSkuRows ? (
|
||||
<ReadonlyValueCell value={displayRow.itemCode} />
|
||||
) : (
|
||||
@ -252,6 +271,16 @@ function PromotionSectionRow({
|
||||
onLineItemChange(displayRow.itemCode, "seriesName", event.target.value)
|
||||
}
|
||||
/>
|
||||
<InputCell
|
||||
value={displayRow.brandItemCode}
|
||||
onChange={(event) =>
|
||||
onLineItemChange(
|
||||
displayRow.itemCode,
|
||||
"brandItemCode",
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ReadonlyValueCell value={displayRow.itemCode} />
|
||||
<ReadonlyValueCell value={displayRow.annualSalesTarget} />
|
||||
<InputCell
|
||||
@ -364,7 +393,9 @@ export function BrandFilingPlanningTemplateSheet({
|
||||
|
||||
<SectionBand title="基本信息" />
|
||||
<div className="grid grid-cols-[1.5fr_1.1fr_1.35fr_1.15fr_1.45fr_1.05fr]">
|
||||
<LabelCell>企划产品线</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>企划产品线</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<InputCell
|
||||
value={form.planningProductLine}
|
||||
onChange={(event) =>
|
||||
@ -373,14 +404,18 @@ export function BrandFilingPlanningTemplateSheet({
|
||||
listId="planning-product-line-options"
|
||||
options={planningProductLineOptions}
|
||||
/>
|
||||
<LabelCell>产品线</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>产品线</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<InputCell
|
||||
value={form.productLine}
|
||||
onChange={(event) => onChange("productLine", event.target.value)}
|
||||
listId="product-line-options"
|
||||
options={productLineOptions}
|
||||
/>
|
||||
<LabelCell>是否年度规划内</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>是否年度规划内</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<InputCell
|
||||
value={form.annualPlanStatus}
|
||||
onChange={(event) =>
|
||||
@ -391,7 +426,9 @@ export function BrandFilingPlanningTemplateSheet({
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-[1.5fr_1.1fr_1.35fr_1.15fr_1.45fr_1.05fr]">
|
||||
<LabelCell>开发方式</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>开发方式</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<InputCell
|
||||
value={form.developmentMode}
|
||||
onChange={(event) =>
|
||||
@ -400,12 +437,16 @@ export function BrandFilingPlanningTemplateSheet({
|
||||
listId="development-mode-options"
|
||||
options={developmentModeOptions}
|
||||
/>
|
||||
<LabelCell>产品系列</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>产品系列</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<InputCell
|
||||
value={form.productSeries}
|
||||
onChange={(event) => onChange("productSeries", event.target.value)}
|
||||
/>
|
||||
<LabelCell>开发类别</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>开发类别</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<InputCell
|
||||
value={form.developmentCategory}
|
||||
onChange={(event) =>
|
||||
@ -416,13 +457,17 @@ export function BrandFilingPlanningTemplateSheet({
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-[1.55fr_1.55fr_1fr_0.95fr_1.55fr_1.55fr_1.45fr_1.45fr]">
|
||||
<LabelCell>买采供应商名称</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>买采供应商名称</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<InputCell
|
||||
value={form.supplierName}
|
||||
onChange={(event) => onChange("supplierName", event.target.value)}
|
||||
inputClassName={INPUT_LEFT_CLASS}
|
||||
/>
|
||||
<LabelCell>是否入仓</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>是否入仓</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<InputCell
|
||||
value={form.warehousingStatus}
|
||||
onChange={(event) =>
|
||||
@ -431,12 +476,16 @@ export function BrandFilingPlanningTemplateSheet({
|
||||
listId="warehousing-status-options"
|
||||
options={warehousingStatusOptions}
|
||||
/>
|
||||
<LabelCell>需求上市时间</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>需求上市时间</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<InputCell
|
||||
value={form.launchDate}
|
||||
onChange={(event) => onChange("launchDate", event.target.value)}
|
||||
/>
|
||||
<LabelCell>是否活动款</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>是否活动款</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<InputCell
|
||||
value={form.activityStyleStatus}
|
||||
onChange={(event) =>
|
||||
@ -449,10 +498,12 @@ export function BrandFilingPlanningTemplateSheet({
|
||||
|
||||
<SectionBand title="企划背景介绍" />
|
||||
<div className="grid grid-cols-[190px_minmax(0,1fr)]">
|
||||
<LabelCell className="min-h-[200px] text-[22px]">图片</LabelCell>
|
||||
<LabelCell className="min-h-[200px] text-[22px]">
|
||||
<RequiredFieldLabel>图片</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<ImageCell imageUrl={previewImage} />
|
||||
<LabelCell className="min-h-[200px] text-[18px]">
|
||||
产品需求分析
|
||||
<RequiredFieldLabel>产品需求分析</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<TextareaCell
|
||||
value={form.productDemandAnalysis}
|
||||
@ -464,13 +515,26 @@ export function BrandFilingPlanningTemplateSheet({
|
||||
</div>
|
||||
|
||||
<SectionBand title="企划方案" />
|
||||
<div className="grid grid-cols-[1.1fr_1.8fr_1.5fr_1.6fr_1.7fr_2fr]">
|
||||
<LabelCell>货号</LabelCell>
|
||||
<LabelCell>零售价需求</LabelCell>
|
||||
<LabelCell>采购价需求</LabelCell>
|
||||
<LabelCell>尺寸要求</LabelCell>
|
||||
<LabelCell>材质/颜色</LabelCell>
|
||||
<LabelCell>特殊说明(是否老款迭代)</LabelCell>
|
||||
<div className="grid grid-cols-[1.05fr_1.05fr_1.55fr_1.45fr_1.55fr_1.7fr_2fr]">
|
||||
<LabelCell>品牌货号</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>货号</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>零售价需求</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>采购价需求</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>尺寸要求</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>材质/颜色</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>特殊说明(是否老款迭代)</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
{displayRows.map((displayRow, index) => (
|
||||
<Fragment key={`plan-${displayRow.itemCode || index}`}>
|
||||
<PlanSectionRow
|
||||
@ -485,20 +549,41 @@ export function BrandFilingPlanningTemplateSheet({
|
||||
</div>
|
||||
|
||||
<SectionBand title="推广计划" />
|
||||
<div className="grid grid-cols-[1.35fr_1.35fr_1.2fr_0.95fr_0.9fr_0.9fr_0.9fr_1fr_1.25fr_1.4fr] auto-rows-[58px]">
|
||||
<LabelCell className="row-span-2">系列</LabelCell>
|
||||
<LabelCell className="row-span-2">货号</LabelCell>
|
||||
<div className="grid grid-cols-[1.25fr_1.2fr_1.25fr_1.1fr_0.9fr_0.85fr_0.85fr_0.85fr_0.95fr_1fr_1.25fr] auto-rows-[58px]">
|
||||
<LabelCell className="row-span-2">
|
||||
<RequiredFieldLabel>系列</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<LabelCell className="row-span-2">品牌货号</LabelCell>
|
||||
<LabelCell className="row-span-2">
|
||||
<RequiredFieldLabel>货号</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<LabelCell className="col-span-2">上市计划</LabelCell>
|
||||
<LabelCell className="col-span-6">上市季度销量</LabelCell>
|
||||
|
||||
<LabelCell className="whitespace-nowrap">全年销售总量</LabelCell>
|
||||
<LabelCell>渠道</LabelCell>
|
||||
<LabelCell>第1个月</LabelCell>
|
||||
<LabelCell>第2个月</LabelCell>
|
||||
<LabelCell>第3个月</LabelCell>
|
||||
<LabelCell>第4-6月</LabelCell>
|
||||
<LabelCell>第7-9月</LabelCell>
|
||||
<LabelCell>第10-12月</LabelCell>
|
||||
<LabelCell className="whitespace-nowrap">
|
||||
<RequiredFieldLabel>全年销售总量</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>渠道</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>第1个月</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>第2个月</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>第3个月</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>第4-6月</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>第7-9月</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>第10-12月</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
|
||||
{displayRows.map((displayRow, index) => (
|
||||
<Fragment key={`promotion-${displayRow.itemCode || index}`}>
|
||||
@ -512,26 +597,34 @@ export function BrandFilingPlanningTemplateSheet({
|
||||
|
||||
<SectionBand title="会签" />
|
||||
<div className="grid grid-cols-[1.15fr_2.2fr_1fr_2.2fr_1fr_2.25fr]">
|
||||
<LabelCell>产品经理</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>产品经理</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<InputCell
|
||||
value={form.productManager}
|
||||
onChange={(event) => onChange("productManager", event.target.value)}
|
||||
listId="product-manager-options"
|
||||
options={productManagerOptions}
|
||||
/>
|
||||
<LabelCell>企划负责人</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>企划负责人</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<InputCell
|
||||
value={form.planningOwner}
|
||||
onChange={(event) => onChange("planningOwner", event.target.value)}
|
||||
/>
|
||||
<LabelCell>采购负责人</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>采购负责人</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<InputCell
|
||||
value={form.purchaseOwner}
|
||||
onChange={(event) => onChange("purchaseOwner", event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-[1.15fr_5.45fr]">
|
||||
<LabelCell>渠道</LabelCell>
|
||||
<LabelCell>
|
||||
<RequiredFieldLabel>渠道</RequiredFieldLabel>
|
||||
</LabelCell>
|
||||
<ReadonlyValueCell
|
||||
value={channelSummary}
|
||||
emptyPlaceholder=""
|
||||
|
||||
@ -10,6 +10,7 @@ import { type BrandFilingBrand } from "@/app/intake/brand-filing/brandFilingConf
|
||||
import { getBrandFilingPlanningRecord } from "@/app/intake/brand-filing/brandFilingProductData";
|
||||
import {
|
||||
buildBrandFilingSequenceMap,
|
||||
getBrandFilingEnteredAt,
|
||||
getBrandFilingProducts,
|
||||
isBrandFilingBasicInfoCurrentlyConfirmed,
|
||||
} from "@/app/intake/brand-filing/brandFilingSequence";
|
||||
@ -20,11 +21,14 @@ export type BrandFilingRow = {
|
||||
product: NonIntegratedProductRecord;
|
||||
planningRecord?: BrandFilingPlanningRecord;
|
||||
sequenceNumber?: number;
|
||||
enteredAt: string;
|
||||
quotationCompleted: boolean;
|
||||
infoConfirmationCompleted: boolean;
|
||||
planningCompleted: boolean;
|
||||
brandFilingCompleted: boolean;
|
||||
basicInfoConfirmed: boolean;
|
||||
reportedAt: string;
|
||||
reported: boolean;
|
||||
displayName: string;
|
||||
spu: string;
|
||||
productName: string;
|
||||
@ -41,6 +45,7 @@ export function buildBrandFilingRows(
|
||||
const sequenceMap = buildBrandFilingSequenceMap(products, brand);
|
||||
|
||||
return getBrandFilingProducts(products, brand).map((product) => {
|
||||
const brandData = product.brandFilingData?.[brand];
|
||||
const people = getNonIntegratedListPeople(product);
|
||||
const previewImage =
|
||||
product.openingReference?.productImage?.trim() || product.previewImage;
|
||||
@ -59,10 +64,13 @@ export function buildBrandFilingRows(
|
||||
product,
|
||||
planningRecord,
|
||||
sequenceNumber: sequenceMap.get(product.id),
|
||||
enteredAt: getBrandFilingEnteredAt(product, brand),
|
||||
quotationCompleted,
|
||||
infoConfirmationCompleted,
|
||||
planningCompleted,
|
||||
basicInfoConfirmed,
|
||||
reportedAt: brandData?.reportedAt?.trim() ?? "",
|
||||
reported: Boolean(brandData?.reportedAt?.trim()),
|
||||
brandFilingCompleted:
|
||||
brand === "gujia"
|
||||
? quotationCompleted && infoConfirmationCompleted && planningCompleted
|
||||
@ -112,6 +120,15 @@ export function formatBrandFilingSequenceNumber(value: number | undefined) {
|
||||
return value && value > 0 ? String(value) : "-";
|
||||
}
|
||||
|
||||
export function formatBrandFilingEntryDate(value: string | undefined) {
|
||||
if (!value) return "-";
|
||||
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return "-";
|
||||
|
||||
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
|
||||
}
|
||||
|
||||
export function getBrandFilingPlanningStatusLabel(completed: boolean) {
|
||||
return completed ? "已完成" : "待填写";
|
||||
}
|
||||
|
||||
@ -12,12 +12,17 @@ import {
|
||||
import {
|
||||
BrandFilingPlanningTableDialog,
|
||||
} from "@/app/intake/brand-filing/BrandFilingPlanningTableDialog";
|
||||
import {
|
||||
BrandFilingReportConfirmationDialog,
|
||||
} from "@/app/intake/brand-filing/BrandFilingReportConfirmationDialog";
|
||||
import {
|
||||
BrandFilingProductTableToolbar,
|
||||
type BrandFilingReportView,
|
||||
} from "@/app/intake/brand-filing/BrandFilingProductTableToolbar";
|
||||
import { downloadGujiaBrandFilingPackage } from "@/app/intake/brand-filing/gujiaBrandFilingPackageDownload";
|
||||
import { downloadBrandFilingPlanningTables } from "@/app/intake/brand-filing/brandFilingPlanningTableDownload";
|
||||
import type { BrandFilingDownloadProgress } from "@/app/intake/brand-filing/brandFilingDownloadProgress";
|
||||
import { markBrandFilingReported } from "@/app/intake/brand-filing/brandFilingReportStatus";
|
||||
import {
|
||||
ensureBrandFilingPlanningRecord,
|
||||
getBrandFilingPlanningRecord,
|
||||
@ -26,6 +31,7 @@ import {
|
||||
} from "@/app/intake/brand-filing/brandFilingProductData";
|
||||
import {
|
||||
buildBrandFilingRows,
|
||||
formatBrandFilingEntryDate,
|
||||
formatBrandFilingSequenceNumber,
|
||||
getBrandFilingProductsByIds,
|
||||
getBrandFilingTemplateStatusLabel,
|
||||
@ -66,6 +72,7 @@ const TEXT = {
|
||||
productCountUnit: "个产品",
|
||||
rowUnit: "条",
|
||||
index: "序号",
|
||||
entryDate: "日期",
|
||||
quotationStatus: "报价表状态",
|
||||
infoConfirmationStatus: "信息确认函状态",
|
||||
planningStatus: "企划表状态",
|
||||
@ -90,6 +97,11 @@ type ActiveDownloadProgress = BrandFilingDownloadProgress & {
|
||||
title: string;
|
||||
};
|
||||
|
||||
type PendingReportConfirmation = {
|
||||
productIds: string[];
|
||||
count: number;
|
||||
};
|
||||
|
||||
function getDownloadProgressDescription(progress: ActiveDownloadProgress) {
|
||||
if (progress.phase === "preparing") return "正在生成文件";
|
||||
if (progress.phase === "saving") return "正在保存文件";
|
||||
@ -217,6 +229,8 @@ export function BrandFilingProductTable({
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageJumpInput, setPageJumpInput] = useState("1");
|
||||
const [isPageJumpEditing, setIsPageJumpEditing] = useState(false);
|
||||
const [reportView, setReportView] =
|
||||
useState<BrandFilingReportView>("unreported");
|
||||
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
||||
const [activePlanningProductId, setActivePlanningProductId] = useState<
|
||||
string | null
|
||||
@ -240,6 +254,9 @@ export function BrandFilingProductTable({
|
||||
isDownloadingGujiaTemplatePackage,
|
||||
setIsDownloadingGujiaTemplatePackage,
|
||||
] = useState(false);
|
||||
const [isConfirmingReport, setIsConfirmingReport] = useState(false);
|
||||
const [pendingReportConfirmation, setPendingReportConfirmation] =
|
||||
useState<PendingReportConfirmation | null>(null);
|
||||
const [downloadProgress, setDownloadProgress] =
|
||||
useState<ActiveDownloadProgress | null>(null);
|
||||
const selectAllRef = useRef<HTMLInputElement | null>(null);
|
||||
@ -301,12 +318,23 @@ export function BrandFilingProductTable({
|
||||
.map(({ sourceIndex: _sourceIndex, ...row }) => row),
|
||||
[rows],
|
||||
);
|
||||
const unreportedRows = useMemo(
|
||||
() => orderedRows.filter((row) => !row.reported),
|
||||
[orderedRows],
|
||||
);
|
||||
const reportedRows = useMemo(
|
||||
() => orderedRows.filter((row) => row.reported),
|
||||
[orderedRows],
|
||||
);
|
||||
const visibleRows = reportView === "reported" ? reportedRows : unreportedRows;
|
||||
|
||||
useEffect(() => {
|
||||
const rowsNeedingSequencePersistence = orderedRows.filter(
|
||||
(row) =>
|
||||
row.basicInfoConfirmed &&
|
||||
row.sequenceNumber &&
|
||||
!row.product.brandFilingData?.[brand]?.sequenceNumber,
|
||||
(!row.product.brandFilingData?.[brand]?.sequenceNumber ||
|
||||
!row.product.brandFilingData?.[brand]?.enteredAt),
|
||||
);
|
||||
if (rowsNeedingSequencePersistence.length === 0) return;
|
||||
|
||||
@ -347,21 +375,18 @@ export function BrandFilingProductTable({
|
||||
};
|
||||
}, [applySavedProduct, brand, orderedRows]);
|
||||
const validRowIdSet = useMemo(
|
||||
() => new Set(orderedRows.map((row) => row.id)),
|
||||
[orderedRows],
|
||||
() => new Set(visibleRows.map((row) => row.id)),
|
||||
[visibleRows],
|
||||
);
|
||||
const rowMap = useMemo(
|
||||
() => new Map(orderedRows.map((row) => [row.id, row])),
|
||||
[orderedRows],
|
||||
);
|
||||
const totalRows = orderedRows.length;
|
||||
const completedRows = orderedRows.filter(
|
||||
(row) => row.brandFilingCompleted,
|
||||
).length;
|
||||
const totalRows = visibleRows.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalRows / PAGE_SIZE));
|
||||
const safeCurrentPage = Math.min(currentPage, totalPages);
|
||||
const pageStartIndex = (safeCurrentPage - 1) * PAGE_SIZE;
|
||||
const pagedRows = orderedRows.slice(pageStartIndex, pageStartIndex + PAGE_SIZE);
|
||||
const pagedRows = visibleRows.slice(pageStartIndex, pageStartIndex + PAGE_SIZE);
|
||||
const pageRowIdSet = useMemo(
|
||||
() => new Set(pagedRows.map((row) => row.id)),
|
||||
[pagedRows],
|
||||
@ -379,13 +404,17 @@ export function BrandFilingProductTable({
|
||||
[normalizedSelectedRowIds, pageRowIdSet],
|
||||
);
|
||||
const selectedRows = useMemo(
|
||||
() => orderedRows.filter((row) => selectedRowSet.has(row.id)),
|
||||
[orderedRows, selectedRowSet],
|
||||
() => visibleRows.filter((row) => selectedRowSet.has(row.id)),
|
||||
[visibleRows, selectedRowSet],
|
||||
);
|
||||
const selectedRowsNeedingBasicInfoConfirmation = useMemo(
|
||||
() => selectedRows.filter((row) => !row.basicInfoConfirmed),
|
||||
[selectedRows],
|
||||
);
|
||||
const selectedRowsNeedingTemplateCompletion = useMemo(
|
||||
() => selectedRows.filter((row) => !row.brandFilingCompleted),
|
||||
[selectedRows],
|
||||
);
|
||||
const hasSelectedRowsNeedingBasicInfoConfirmation =
|
||||
selectedRowsNeedingBasicInfoConfirmation.length > 0;
|
||||
const brandLabel = getBrandFilingBrandLabel(brand);
|
||||
@ -406,7 +435,8 @@ export function BrandFilingProductTable({
|
||||
isSavingGujiaQuotationTable ||
|
||||
isSavingGujiaProductInfoConfirmation ||
|
||||
isDownloadingPlanningTable ||
|
||||
isDownloadingGujiaTemplatePackage;
|
||||
isDownloadingGujiaTemplatePackage ||
|
||||
isConfirmingReport;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectAllRef.current) {
|
||||
@ -435,6 +465,13 @@ export function BrandFilingProductTable({
|
||||
);
|
||||
};
|
||||
|
||||
const handleReportViewChange = (view: BrandFilingReportView) => {
|
||||
setReportView(view);
|
||||
setCurrentPage(1);
|
||||
setPageJumpInput("1");
|
||||
setSelectedRowIds([]);
|
||||
};
|
||||
|
||||
const createDownloadProgressHandler = (title: string) => {
|
||||
return (progress: BrandFilingDownloadProgress) => {
|
||||
setDownloadProgress({
|
||||
@ -661,6 +698,10 @@ export function BrandFilingProductTable({
|
||||
warnBasicInfoConfirmationRequired();
|
||||
return;
|
||||
}
|
||||
if (selectedRowsNeedingTemplateCompletion.length > 0) {
|
||||
toast.warning("请先完成并确认企划表、信息确认函和产品报价表后再下载全套表");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDownloadingGujiaTemplatePackage(true);
|
||||
try {
|
||||
@ -673,6 +714,10 @@ export function BrandFilingProductTable({
|
||||
onProgress: createDownloadProgressHandler("顾家全套报备表"),
|
||||
});
|
||||
toast.success("顾家全套报备表压缩包已开始下载");
|
||||
setPendingReportConfirmation({
|
||||
productIds: latestSelectedProducts.map((product) => product.id),
|
||||
count: latestSelectedProducts.length,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "顾家全套报备表下载失败,请稍后重试",
|
||||
@ -683,6 +728,48 @@ export function BrandFilingProductTable({
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmGujiaTemplatePackageReported = async () => {
|
||||
if (!pendingReportConfirmation) return;
|
||||
|
||||
const productsToReport = getBrandFilingProductsByIds(
|
||||
tableProductsRef.current,
|
||||
pendingReportConfirmation.productIds,
|
||||
);
|
||||
|
||||
if (productsToReport.length === 0) {
|
||||
setPendingReportConfirmation(null);
|
||||
toast.warning("没有找到需要转入已报备的明细");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConfirmingReport(true);
|
||||
try {
|
||||
const reportedAt = new Date().toISOString();
|
||||
for (const product of productsToReport) {
|
||||
const nextProduct = markBrandFilingReported({
|
||||
product,
|
||||
brand,
|
||||
reportedAt,
|
||||
});
|
||||
const savedProduct = await saveSharedNonIntegratedProduct(nextProduct, {
|
||||
workspaceCategory: NON_INTEGRATED_PLANNING_LAYOUT_WORKSPACE_CATEGORY,
|
||||
});
|
||||
applySavedProduct(savedProduct);
|
||||
}
|
||||
|
||||
setPendingReportConfirmation(null);
|
||||
setSelectedRowIds([]);
|
||||
setReportView("reported");
|
||||
toast.success("已转入已报备");
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "转入已报备失败,请稍后重试",
|
||||
);
|
||||
} finally {
|
||||
setIsConfirmingReport(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveGujiaQuotationTable = async (
|
||||
headerForm: GujiaQuotationHeaderForm,
|
||||
lineItems: GujiaQuotationLineItem[],
|
||||
@ -789,11 +876,12 @@ export function BrandFilingProductTable({
|
||||
return (
|
||||
<>
|
||||
<BrandFilingProductTableToolbar
|
||||
brandLabel={brandLabel}
|
||||
totalRows={totalRows}
|
||||
completedRows={completedRows}
|
||||
reportView={reportView}
|
||||
unreportedRows={unreportedRows.length}
|
||||
reportedRows={reportedRows.length}
|
||||
selectedCount={selectedRows.length}
|
||||
busy={busy}
|
||||
onReportViewChange={handleReportViewChange}
|
||||
onOpenPlanningTable={handleOpenPlanningTable}
|
||||
onOpenGujiaQuotationTable={handleOpenGujiaQuotationTable}
|
||||
onOpenGujiaProductInfoConfirmation={
|
||||
@ -814,7 +902,7 @@ export function BrandFilingProductTable({
|
||||
<div className="min-h-0 flex-1 overflow-x-auto overflow-y-auto pb-16">
|
||||
<table
|
||||
className={`w-full border-collapse ${
|
||||
showGujiaTemplateStatusColumns ? "min-w-[1460px]" : "min-w-[1220px]"
|
||||
showGujiaTemplateStatusColumns ? "min-w-[1570px]" : "min-w-[1330px]"
|
||||
}`}
|
||||
>
|
||||
<thead>
|
||||
@ -830,19 +918,22 @@ export function BrandFilingProductTable({
|
||||
/>
|
||||
</th>
|
||||
<th className={`${HEADER_CELL_CLASS} w-[80px]`}>{TEXT.index}</th>
|
||||
{showGujiaTemplateStatusColumns ? (
|
||||
<>
|
||||
<th className={`${HEADER_CELL_CLASS} w-[120px]`}>
|
||||
{TEXT.quotationStatus}
|
||||
<th className={`${HEADER_CELL_CLASS} w-[112px]`}>
|
||||
{TEXT.entryDate}
|
||||
</th>
|
||||
<th className={`${HEADER_CELL_CLASS} w-[136px]`}>
|
||||
{TEXT.infoConfirmationStatus}
|
||||
</th>
|
||||
</>
|
||||
) : null}
|
||||
<th className={`${HEADER_CELL_CLASS} w-[120px]`}>
|
||||
{TEXT.planningStatus}
|
||||
</th>
|
||||
{showGujiaTemplateStatusColumns ? (
|
||||
<>
|
||||
<th className={`${HEADER_CELL_CLASS} w-[136px]`}>
|
||||
{TEXT.infoConfirmationStatus}
|
||||
</th>
|
||||
<th className={`${HEADER_CELL_CLASS} w-[120px]`}>
|
||||
{TEXT.quotationStatus}
|
||||
</th>
|
||||
</>
|
||||
) : null}
|
||||
<th className={`${HEADER_CELL_CLASS} w-[112px]`}>{TEXT.preview}</th>
|
||||
<th className={`${HEADER_CELL_CLASS} min-w-[156px]`}>SPU</th>
|
||||
<th className={`${HEADER_CELL_CLASS} min-w-[220px]`}>{TEXT.name}</th>
|
||||
@ -864,7 +955,7 @@ export function BrandFilingProductTable({
|
||||
{pagedRows.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={showGujiaTemplateStatusColumns ? 12 : 10}
|
||||
colSpan={showGujiaTemplateStatusColumns ? 13 : 11}
|
||||
className="px-4 py-12 text-center text-sm text-[var(--obs-shell-muted)]"
|
||||
>
|
||||
{TEXT.empty}
|
||||
@ -888,25 +979,28 @@ export function BrandFilingProductTable({
|
||||
<td className="px-4 py-3 text-sm font-medium text-[var(--obs-shell-title)]">
|
||||
{formatBrandFilingSequenceNumber(row.sequenceNumber)}
|
||||
</td>
|
||||
{showGujiaTemplateStatusColumns ? (
|
||||
<>
|
||||
<td className="px-4 py-3">
|
||||
<BrandFilingStatusBadge
|
||||
completed={row.quotationCompleted}
|
||||
/>
|
||||
<td className="px-4 py-3 text-sm tabular-nums text-[var(--obs-shell-title)]">
|
||||
{formatBrandFilingEntryDate(row.enteredAt)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<BrandFilingStatusBadge
|
||||
completed={row.infoConfirmationCompleted}
|
||||
/>
|
||||
</td>
|
||||
</>
|
||||
) : null}
|
||||
<td className="px-4 py-3">
|
||||
<BrandFilingStatusBadge
|
||||
completed={row.planningCompleted}
|
||||
/>
|
||||
</td>
|
||||
{showGujiaTemplateStatusColumns ? (
|
||||
<>
|
||||
<td className="px-4 py-3">
|
||||
<BrandFilingStatusBadge
|
||||
completed={row.infoConfirmationCompleted}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<BrandFilingStatusBadge
|
||||
completed={row.quotationCompleted}
|
||||
/>
|
||||
</td>
|
||||
</>
|
||||
) : null}
|
||||
<td className="px-4 py-3">
|
||||
{row.previewImage ? (
|
||||
<button
|
||||
@ -1062,6 +1156,14 @@ export function BrandFilingProductTable({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<BrandFilingReportConfirmationDialog
|
||||
count={pendingReportConfirmation?.count ?? 0}
|
||||
isOpen={Boolean(pendingReportConfirmation)}
|
||||
saving={isConfirmingReport}
|
||||
onCancel={() => setPendingReportConfirmation(null)}
|
||||
onConfirm={handleConfirmGujiaTemplatePackageReported}
|
||||
/>
|
||||
|
||||
<ToastContainer
|
||||
toasts={toast.toasts}
|
||||
onClose={toast.removeToast}
|
||||
|
||||
@ -3,6 +3,41 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Download, SquarePen } from "lucide-react";
|
||||
|
||||
export type BrandFilingReportView = "unreported" | "reported";
|
||||
|
||||
function ReportViewButton({
|
||||
active,
|
||||
count,
|
||||
label,
|
||||
onClick,
|
||||
}: {
|
||||
active: boolean;
|
||||
count: number;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`inline-flex h-8 items-center gap-1.5 rounded-[6px] border px-3 text-[12px] font-apple-semibold transition ${
|
||||
active
|
||||
? "border-[var(--obs-shell-action-border)] bg-[var(--obs-shell-action-bg)] text-[var(--obs-shell-action-text)]"
|
||||
: "border-[var(--obs-shell-border)] bg-white text-[var(--obs-shell-control-text)] hover:bg-[var(--obs-shell-hover-bg)] hover:text-[var(--obs-shell-title)]"
|
||||
}`}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<span
|
||||
className={`rounded-full px-1.5 py-0.5 text-[10px] ${
|
||||
active ? "bg-white/20" : "bg-[var(--obs-shell-soft-bg)]"
|
||||
}`}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarButton({
|
||||
disabled,
|
||||
icon,
|
||||
@ -34,11 +69,12 @@ function ToolbarButton({
|
||||
}
|
||||
|
||||
export function BrandFilingProductTableToolbar({
|
||||
brandLabel,
|
||||
totalRows,
|
||||
completedRows,
|
||||
reportView,
|
||||
unreportedRows,
|
||||
reportedRows,
|
||||
selectedCount,
|
||||
busy,
|
||||
onReportViewChange,
|
||||
onOpenPlanningTable,
|
||||
onOpenGujiaQuotationTable,
|
||||
onOpenGujiaProductInfoConfirmation,
|
||||
@ -47,11 +83,12 @@ export function BrandFilingProductTableToolbar({
|
||||
showGujiaQuotationTable = false,
|
||||
showGujiaProductInfoConfirmation = false,
|
||||
}: {
|
||||
brandLabel: string;
|
||||
totalRows: number;
|
||||
completedRows: number;
|
||||
reportView: BrandFilingReportView;
|
||||
unreportedRows: number;
|
||||
reportedRows: number;
|
||||
selectedCount: number;
|
||||
busy: boolean;
|
||||
onReportViewChange: (view: BrandFilingReportView) => void;
|
||||
onOpenPlanningTable: () => void;
|
||||
onOpenGujiaQuotationTable?: () => void;
|
||||
onOpenGujiaProductInfoConfirmation?: () => void;
|
||||
@ -64,24 +101,31 @@ export function BrandFilingProductTableToolbar({
|
||||
<div className="border-b border-[var(--obs-shell-border)] px-5 py-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-[var(--obs-shell-title)]">
|
||||
{brandLabel}
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-3 text-xs text-[var(--obs-shell-muted)]">
|
||||
<span>{totalRows} 个产品</span>
|
||||
<span>{completedRows} 个已完成</span>
|
||||
<span>{selectedCount} 个已选中</span>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<ReportViewButton
|
||||
active={reportView === "unreported"}
|
||||
count={unreportedRows}
|
||||
label="未报备"
|
||||
onClick={() => onReportViewChange("unreported")}
|
||||
/>
|
||||
<ReportViewButton
|
||||
active={reportView === "reported"}
|
||||
count={reportedRows}
|
||||
label="已报备"
|
||||
onClick={() => onReportViewChange("reported")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
{showGujiaQuotationTable && onOpenGujiaQuotationTable ? (
|
||||
<span className="mr-1 text-xs text-[var(--obs-shell-muted)]">
|
||||
已选中 {selectedCount}
|
||||
</span>
|
||||
<ToolbarButton
|
||||
disabled={busy || selectedCount === 0}
|
||||
disabled={busy || selectedCount !== 1}
|
||||
icon={<SquarePen className="h-3.5 w-3.5" aria-hidden="true" />}
|
||||
label="填写产品报价表"
|
||||
onClick={onOpenGujiaQuotationTable}
|
||||
label="填写企划表"
|
||||
onClick={onOpenPlanningTable}
|
||||
/>
|
||||
) : null}
|
||||
{showGujiaProductInfoConfirmation &&
|
||||
onOpenGujiaProductInfoConfirmation ? (
|
||||
<ToolbarButton
|
||||
@ -91,12 +135,14 @@ export function BrandFilingProductTableToolbar({
|
||||
onClick={onOpenGujiaProductInfoConfirmation}
|
||||
/>
|
||||
) : null}
|
||||
{showGujiaQuotationTable && onOpenGujiaQuotationTable ? (
|
||||
<ToolbarButton
|
||||
disabled={busy || selectedCount !== 1}
|
||||
disabled={busy || selectedCount === 0}
|
||||
icon={<SquarePen className="h-3.5 w-3.5" aria-hidden="true" />}
|
||||
label="填写企划表"
|
||||
onClick={onOpenPlanningTable}
|
||||
label="填写产品报价表"
|
||||
onClick={onOpenGujiaQuotationTable}
|
||||
/>
|
||||
) : null}
|
||||
{showGujiaQuotationTable && onDownloadGujiaTemplatePackage ? (
|
||||
<ToolbarButton
|
||||
disabled={busy || selectedCount === 0}
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@/components/ui/Button";
|
||||
|
||||
export function BrandFilingReportConfirmationDialog({
|
||||
count,
|
||||
isOpen,
|
||||
saving,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: {
|
||||
count: number;
|
||||
isOpen: boolean;
|
||||
saving: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/24 px-4">
|
||||
<div className="w-full max-w-[420px] rounded-[10px] border border-[var(--obs-shell-border)] bg-white p-5 shadow-[0_20px_60px_rgba(15,23,42,0.18)]">
|
||||
<div className="text-sm font-medium text-[var(--obs-shell-title)]">
|
||||
确认进入已报备
|
||||
</div>
|
||||
<div className="mt-2 text-sm leading-6 text-[var(--obs-shell-muted)]">
|
||||
全套表已经开始下载。确认后,当前选中的 {count} 条明细会移动到“已报备”。
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
size="small"
|
||||
disabled={saving}
|
||||
onClick={onCancel}
|
||||
>
|
||||
暂不确认
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
loading={saving}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
确认进入已报备
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -9,6 +9,10 @@ import {
|
||||
type GujiaProductInfoConfirmationEditorState,
|
||||
} from "@/app/intake/brand-filing/gujiaProductInfoConfirmationData";
|
||||
import type { GujiaProductInfoConfirmationLineItem } from "@/app/intake/brand-filing/brandFilingTypes";
|
||||
import {
|
||||
buildRequiredFieldsMessage,
|
||||
validateGujiaProductInfoConfirmationRequiredFields,
|
||||
} from "@/app/intake/brand-filing/brandFilingRequiredValidation";
|
||||
import type { NonIntegratedProductRecord } from "@/app/intake/non-integrated/nonIntegratedStorage";
|
||||
|
||||
export function GujiaProductInfoConfirmationDialog({
|
||||
@ -132,6 +136,23 @@ function GujiaProductInfoConfirmationDialogContent({
|
||||
);
|
||||
};
|
||||
|
||||
const getMissingRequiredFields = () =>
|
||||
validateGujiaProductInfoConfirmationRequiredFields(lineItems);
|
||||
|
||||
const handleSaveDraft = () => {
|
||||
onSave(lineItems, completed && getMissingRequiredFields().length === 0);
|
||||
};
|
||||
|
||||
const handleSaveAndConfirm = () => {
|
||||
const missingFields = getMissingRequiredFields();
|
||||
if (missingFields.length > 0) {
|
||||
window.alert(buildRequiredFieldsMessage("产品信息确认函", missingFields));
|
||||
return;
|
||||
}
|
||||
|
||||
onSave(lineItems, true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
@ -189,7 +210,7 @@ function GujiaProductInfoConfirmationDialogContent({
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={() => onSave(lineItems, completed)}
|
||||
onClick={handleSaveDraft}
|
||||
className="inline-flex h-8 items-center rounded-[6px] border border-[var(--obs-shell-border)] bg-white px-4 text-[12px] font-apple-semibold text-[var(--obs-shell-control-text)] transition hover:bg-[var(--obs-shell-hover-bg)] hover:text-[var(--obs-shell-title)] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
保存
|
||||
@ -197,7 +218,7 @@ function GujiaProductInfoConfirmationDialogContent({
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={() => onSave(lineItems, true)}
|
||||
onClick={handleSaveAndConfirm}
|
||||
className="inline-flex h-8 items-center rounded-[6px] border border-[var(--obs-shell-action-border)] bg-[var(--obs-shell-action-bg)] px-4 text-[12px] font-apple-semibold text-[var(--obs-shell-action-text)] transition hover:bg-[var(--obs-shell-action-hover-bg)] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
保存并确认
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { ChangeEvent } from "react";
|
||||
import type { ChangeEvent, ReactNode } from "react";
|
||||
import { buildGujiaProductInfoConfirmationDisplayRows } from "@/app/intake/brand-filing/gujiaProductInfoConfirmationDisplayRows";
|
||||
import {
|
||||
calculateNetWeightKg,
|
||||
@ -35,6 +35,12 @@ const TABLE_COLUMNS = [
|
||||
{ key: "colorBoardImageUrl", label: "色板图片", width: 132 },
|
||||
] as const;
|
||||
|
||||
const OPTIONAL_COLUMN_KEYS = new Set<keyof GujiaProductInfoConfirmationLineItem>([
|
||||
"materialCode",
|
||||
"brandItemCode",
|
||||
"colorBoardImageUrl",
|
||||
]);
|
||||
|
||||
const SHEET_WIDTH = TABLE_COLUMNS.reduce(
|
||||
(total, column) => total + column.width,
|
||||
0,
|
||||
@ -66,6 +72,15 @@ function TextInput({
|
||||
);
|
||||
}
|
||||
|
||||
function RequiredFieldLabel({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<span className="inline-flex items-center justify-center gap-0.5">
|
||||
<span>{children}</span>
|
||||
<span className="text-red-600">*</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ImageInputCell({
|
||||
alt,
|
||||
value,
|
||||
@ -179,7 +194,11 @@ export function GujiaProductInfoConfirmationTemplateSheet({
|
||||
key={column.key}
|
||||
className={`${BORDER_CLASS} h-[62px] whitespace-pre-line px-2 text-center align-middle`}
|
||||
>
|
||||
{column.label}
|
||||
{OPTIONAL_COLUMN_KEYS.has(column.key) ? (
|
||||
column.label
|
||||
) : (
|
||||
<RequiredFieldLabel>{column.label}</RequiredFieldLabel>
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
@ -12,6 +12,10 @@ import type {
|
||||
GujiaQuotationHeaderForm,
|
||||
GujiaQuotationLineItem,
|
||||
} from "@/app/intake/brand-filing/brandFilingTypes";
|
||||
import {
|
||||
buildRequiredFieldsMessage,
|
||||
validateGujiaQuotationRequiredFields,
|
||||
} from "@/app/intake/brand-filing/brandFilingRequiredValidation";
|
||||
import type { NonIntegratedProductRecord } from "@/app/intake/non-integrated/nonIntegratedStorage";
|
||||
|
||||
export function GujiaQuotationTableDialog({
|
||||
@ -150,6 +154,27 @@ function GujiaQuotationTableDialogContent({
|
||||
);
|
||||
};
|
||||
|
||||
const getMissingRequiredFields = () =>
|
||||
validateGujiaQuotationRequiredFields(headerForm, lineItems);
|
||||
|
||||
const handleSaveDraft = () => {
|
||||
onSave(
|
||||
headerForm,
|
||||
lineItems,
|
||||
completed && getMissingRequiredFields().length === 0,
|
||||
);
|
||||
};
|
||||
|
||||
const handleSaveAndConfirm = () => {
|
||||
const missingFields = getMissingRequiredFields();
|
||||
if (missingFields.length > 0) {
|
||||
window.alert(buildRequiredFieldsMessage("产品报价表", missingFields));
|
||||
return;
|
||||
}
|
||||
|
||||
onSave(headerForm, lineItems, true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
@ -209,7 +234,7 @@ function GujiaQuotationTableDialogContent({
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={() => onSave(headerForm, lineItems, completed)}
|
||||
onClick={handleSaveDraft}
|
||||
className="inline-flex h-8 items-center rounded-[6px] border border-[var(--obs-shell-border)] bg-white px-4 text-[12px] font-apple-semibold text-[var(--obs-shell-control-text)] transition hover:bg-[var(--obs-shell-hover-bg)] hover:text-[var(--obs-shell-title)] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
保存
|
||||
@ -217,7 +242,7 @@ function GujiaQuotationTableDialogContent({
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={() => onSave(headerForm, lineItems, true)}
|
||||
onClick={handleSaveAndConfirm}
|
||||
className="inline-flex h-8 items-center rounded-[6px] border border-[var(--obs-shell-action-border)] bg-[var(--obs-shell-action-bg)] px-4 text-[12px] font-apple-semibold text-[var(--obs-shell-action-text)] transition hover:bg-[var(--obs-shell-action-hover-bg)] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
保存并确认
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { ChangeEvent } from "react";
|
||||
import type { ChangeEvent, ReactNode } from "react";
|
||||
import { buildGujiaQuotationDisplayRows } from "@/app/intake/brand-filing/gujiaQuotationDisplayRows";
|
||||
import type {
|
||||
GujiaQuotationHeaderForm,
|
||||
@ -11,6 +11,7 @@ const GUJIA_KUKA_LOGO_SRC = "/brand-filing/gujia-kuka-logo.png";
|
||||
|
||||
const TABLE_COLUMNS = [
|
||||
{ key: "sequence", width: 95 },
|
||||
{ key: "brandItemCode", width: 121 },
|
||||
{ key: "itemCode", width: 121 },
|
||||
{ key: "specification", width: 160 },
|
||||
{ key: "image", width: 164 },
|
||||
@ -56,6 +57,15 @@ function QuotationInput({
|
||||
return <input value={value} onChange={onChange} className={inputClassName} />;
|
||||
}
|
||||
|
||||
function RequiredFieldLabel({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<span className="inline-flex items-center justify-center gap-0.5">
|
||||
<span>{children}</span>
|
||||
<span className="text-red-600">*</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function GujiaQuotationTemplateSheet({
|
||||
headerForm,
|
||||
lineItems,
|
||||
@ -93,7 +103,7 @@ export function GujiaQuotationTemplateSheet({
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={13}
|
||||
colSpan={14}
|
||||
className={`${BORDER_CLASS} h-[58px] px-4 text-center text-[28px] font-semibold`}
|
||||
>
|
||||
<div className="relative flex min-h-[58px] items-center justify-center px-[180px]">
|
||||
@ -109,11 +119,13 @@ export function GujiaQuotationTemplateSheet({
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={13}
|
||||
colSpan={14}
|
||||
className={`${BORDER_CLASS} h-[32px] px-3 text-left align-middle`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="shrink-0 text-[15px]">编号:</span>
|
||||
<span className="shrink-0 text-[15px]">
|
||||
<RequiredFieldLabel>编号:</RequiredFieldLabel>
|
||||
</span>
|
||||
<input
|
||||
value={headerForm.contactName}
|
||||
onChange={(event) =>
|
||||
@ -126,11 +138,13 @@ export function GujiaQuotationTemplateSheet({
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={6}
|
||||
colSpan={7}
|
||||
className={`${BORDER_CLASS} h-[32px] px-3 text-left align-middle`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="shrink-0 text-[15px]">TO(需求方):</span>
|
||||
<span className="shrink-0 text-[15px]">
|
||||
<RequiredFieldLabel>TO(需求方):</RequiredFieldLabel>
|
||||
</span>
|
||||
<input
|
||||
value={headerForm.demandCompany}
|
||||
onChange={(event) =>
|
||||
@ -143,7 +157,7 @@ export function GujiaQuotationTemplateSheet({
|
||||
<td
|
||||
className={`${BORDER_CLASS} px-3 text-left align-middle text-[15px]`}
|
||||
>
|
||||
FROM(供应方):
|
||||
<RequiredFieldLabel>FROM(供应方):</RequiredFieldLabel>
|
||||
</td>
|
||||
<td
|
||||
colSpan={6}
|
||||
@ -160,11 +174,13 @@ export function GujiaQuotationTemplateSheet({
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={6}
|
||||
colSpan={7}
|
||||
className={`${BORDER_CLASS} h-[32px] px-3 text-left align-middle`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="shrink-0 text-[15px]">MOB(手机):</span>
|
||||
<span className="shrink-0 text-[15px]">
|
||||
<RequiredFieldLabel>MOB(手机):</RequiredFieldLabel>
|
||||
</span>
|
||||
<input
|
||||
value={headerForm.demandMobile}
|
||||
onChange={(event) =>
|
||||
@ -177,7 +193,7 @@ export function GujiaQuotationTemplateSheet({
|
||||
<td
|
||||
className={`${BORDER_CLASS} px-3 text-left align-middle text-[15px]`}
|
||||
>
|
||||
MOB(手机):
|
||||
<RequiredFieldLabel>MOB(手机):</RequiredFieldLabel>
|
||||
</td>
|
||||
<td
|
||||
colSpan={6}
|
||||
@ -194,42 +210,47 @@ export function GujiaQuotationTemplateSheet({
|
||||
</tr>
|
||||
<tr className="text-[15px] font-medium leading-tight">
|
||||
<td className={`${BORDER_CLASS} h-[62px] px-2 text-center align-middle`}>
|
||||
序号
|
||||
<RequiredFieldLabel>序号</RequiredFieldLabel>
|
||||
</td>
|
||||
<td className={`${BORDER_CLASS} px-2 text-center align-middle`}>
|
||||
产品货号
|
||||
品牌货号
|
||||
</td>
|
||||
<td className={`${BORDER_CLASS} px-2 text-center align-middle`}>
|
||||
规格
|
||||
<RequiredFieldLabel>产品货号</RequiredFieldLabel>
|
||||
</td>
|
||||
<td className={`${BORDER_CLASS} px-2 text-center align-middle`}>
|
||||
图片
|
||||
<RequiredFieldLabel>规格</RequiredFieldLabel>
|
||||
</td>
|
||||
<td className={`${BORDER_CLASS} px-2 text-center align-middle`}>
|
||||
采购价(含税)
|
||||
<RequiredFieldLabel>图片</RequiredFieldLabel>
|
||||
</td>
|
||||
<td className={`${BORDER_CLASS} px-2 text-center align-middle`}>
|
||||
售价
|
||||
<RequiredFieldLabel>采购价(含税)</RequiredFieldLabel>
|
||||
</td>
|
||||
<td className={`${BORDER_CLASS} px-2 text-center align-middle`}>
|
||||
材质
|
||||
<RequiredFieldLabel>售价</RequiredFieldLabel>
|
||||
</td>
|
||||
<td className={`${BORDER_CLASS} px-2 text-center align-middle`}>
|
||||
规格(CM)
|
||||
<RequiredFieldLabel>材质</RequiredFieldLabel>
|
||||
</td>
|
||||
<td className={`${BORDER_CLASS} px-2 text-center align-middle`}>
|
||||
起订量
|
||||
<RequiredFieldLabel>规格(CM)</RequiredFieldLabel>
|
||||
</td>
|
||||
<td className={`${BORDER_CLASS} px-2 text-center align-middle`}>
|
||||
<RequiredFieldLabel>起订量</RequiredFieldLabel>
|
||||
</td>
|
||||
<td
|
||||
className={`${BORDER_CLASS} px-2 text-center align-middle whitespace-pre-line`}
|
||||
>
|
||||
<RequiredFieldLabel>
|
||||
采购周期{"\n"}(天)
|
||||
</RequiredFieldLabel>
|
||||
</td>
|
||||
<td className={`${BORDER_CLASS} px-2 text-center align-middle`}>
|
||||
包装件数
|
||||
<RequiredFieldLabel>包装件数</RequiredFieldLabel>
|
||||
</td>
|
||||
<td className={`${BORDER_CLASS} px-2 text-center align-middle`}>
|
||||
包装方式
|
||||
<RequiredFieldLabel>包装方式</RequiredFieldLabel>
|
||||
</td>
|
||||
<td className={`${BORDER_CLASS} px-2 text-center align-middle`}>
|
||||
备注
|
||||
@ -245,6 +266,18 @@ export function GujiaQuotationTemplateSheet({
|
||||
{row.sequenceNumber}
|
||||
</td>
|
||||
) : null}
|
||||
<td className={`${BORDER_CLASS} h-[58px] px-2 align-middle text-[16px]`}>
|
||||
<QuotationInput
|
||||
value={row.brandItemCode}
|
||||
onChange={(event) =>
|
||||
onLineItemChange(
|
||||
row.lineId,
|
||||
"brandItemCode",
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className={`${BORDER_CLASS} h-[58px] px-2 align-middle text-[16px]`}>
|
||||
<QuotationInput
|
||||
value={row.itemCode}
|
||||
@ -373,7 +406,7 @@ export function GujiaQuotationTemplateSheet({
|
||||
))}
|
||||
<tr>
|
||||
<td
|
||||
colSpan={13}
|
||||
colSpan={14}
|
||||
className={`${BORDER_CLASS} px-4 pt-5 text-left align-top text-[14px] leading-8 text-[#ff0000] whitespace-pre-line`}
|
||||
style={{ height: "168px" }}
|
||||
>
|
||||
@ -381,14 +414,14 @@ export function GujiaQuotationTemplateSheet({
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={13} className={`${BORDER_CLASS} h-[28px] bg-white`} />
|
||||
<td colSpan={14} className={`${BORDER_CLASS} h-[28px] bg-white`} />
|
||||
</tr>
|
||||
<tr className="text-[15px]">
|
||||
<td className={`${BORDER_CLASS} h-[56px] px-2 text-center align-middle`}>
|
||||
基地名称
|
||||
</td>
|
||||
<td
|
||||
colSpan={12}
|
||||
colSpan={13}
|
||||
className={`${BORDER_CLASS} px-2 text-center align-middle`}
|
||||
>
|
||||
仓库详细地址
|
||||
@ -399,7 +432,7 @@ export function GujiaQuotationTemplateSheet({
|
||||
电商办公仓
|
||||
</td>
|
||||
<td
|
||||
colSpan={12}
|
||||
colSpan={13}
|
||||
className={`${BORDER_CLASS} px-3 text-center align-middle`}
|
||||
>
|
||||
湖州市安吉县阳光二区北庄路曼龙工业链园区3号厂房3楼
|
||||
|
||||
@ -94,6 +94,7 @@ export function buildBrandFilingPlanningLineItems(params: {
|
||||
|
||||
return normalizeBrandFilingPlanningLineItem({
|
||||
...createEmptyBrandFilingPlanningLineItem(),
|
||||
brandItemCode: pickNonEmptyText(matchedItem?.brandItemCode),
|
||||
itemCode: pickNonEmptyText(matchedItem?.itemCode, row.itemCode),
|
||||
sizeRequirement: pickNonEmptyText(
|
||||
matchedItem?.sizeRequirement,
|
||||
@ -158,6 +159,7 @@ export function buildBrandFilingPlanningDisplayRows(params: {
|
||||
const normalizedItem = normalizeBrandFilingPlanningLineItem({
|
||||
...createEmptyBrandFilingPlanningLineItem(),
|
||||
...matchedItem,
|
||||
brandItemCode: pickNonEmptyText(matchedItem?.brandItemCode),
|
||||
itemCode: pickNonEmptyText(matchedItem?.itemCode, row.itemCode),
|
||||
sizeRequirement: pickNonEmptyText(
|
||||
matchedItem?.sizeRequirement,
|
||||
|
||||
@ -125,11 +125,11 @@ export const BRAND_FILING_PLANNING_ROW_LAYOUT = {
|
||||
plan: {
|
||||
dataStartRow: 11,
|
||||
mergedRanges: [
|
||||
["B", "C"],
|
||||
["D", "E"],
|
||||
["F", "G"],
|
||||
["H", "I"],
|
||||
["J", "L"],
|
||||
["C", "D"],
|
||||
["E", "F"],
|
||||
["G", "H"],
|
||||
["I", "J"],
|
||||
["K", "L"],
|
||||
] as const,
|
||||
},
|
||||
promotion: {
|
||||
@ -138,7 +138,6 @@ export const BRAND_FILING_PLANNING_ROW_LAYOUT = {
|
||||
headerBottomRow: 14,
|
||||
dataStartRow: 15,
|
||||
mergedRanges: [
|
||||
["I", "J"],
|
||||
["K", "L"],
|
||||
] as const,
|
||||
},
|
||||
|
||||
@ -20,7 +20,10 @@ import {
|
||||
getBrandFilingMatchedSkuItemsByItemCode,
|
||||
resolveBrandFilingPlanningItemCode,
|
||||
} from "@/app/intake/brand-filing/brandFilingPlanningSkuRows";
|
||||
import { resolveBrandFilingSequenceNumber } from "@/app/intake/brand-filing/brandFilingSequence";
|
||||
import {
|
||||
getBrandFilingBasicInfoApprovedAt,
|
||||
resolveBrandFilingSequenceNumber,
|
||||
} from "@/app/intake/brand-filing/brandFilingSequence";
|
||||
import type { NonIntegratedProductRecord } from "@/app/intake/non-integrated/nonIntegratedStorage";
|
||||
import { getNonIntegratedListPeople } from "@/app/intake/product-development/nonIntegratedProductListLogic";
|
||||
|
||||
@ -271,17 +274,25 @@ export function upsertBrandFilingSequenceNumber(params: {
|
||||
if (!normalizedSequenceNumber) {
|
||||
return product;
|
||||
}
|
||||
if (product.brandFilingData?.[brand]?.sequenceNumber) {
|
||||
|
||||
const currentBrandData = product.brandFilingData?.[brand];
|
||||
if (currentBrandData?.sequenceNumber && currentBrandData.enteredAt) {
|
||||
return product;
|
||||
}
|
||||
const enteredAt =
|
||||
currentBrandData?.enteredAt ||
|
||||
getBrandFilingBasicInfoApprovedAt(product) ||
|
||||
new Date().toISOString();
|
||||
|
||||
return {
|
||||
...product,
|
||||
brandFilingData: {
|
||||
...(product.brandFilingData ?? {}),
|
||||
[brand]: {
|
||||
...(product.brandFilingData?.[brand] ?? {}),
|
||||
sequenceNumber: normalizedSequenceNumber,
|
||||
...(currentBrandData ?? {}),
|
||||
sequenceNumber:
|
||||
currentBrandData?.sequenceNumber ?? normalizedSequenceNumber,
|
||||
enteredAt,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import type { BrandFilingBrand } from "@/app/intake/brand-filing/brandFilingConfig";
|
||||
import type { NonIntegratedProductRecord } from "@/app/intake/non-integrated/nonIntegratedStorage";
|
||||
|
||||
export function markBrandFilingReported(params: {
|
||||
product: NonIntegratedProductRecord;
|
||||
brand: BrandFilingBrand;
|
||||
reportedAt?: string;
|
||||
}) {
|
||||
const reportedAt = params.reportedAt ?? new Date().toISOString();
|
||||
|
||||
return {
|
||||
...params.product,
|
||||
brandFilingData: {
|
||||
...(params.product.brandFilingData ?? {}),
|
||||
[params.brand]: {
|
||||
...(params.product.brandFilingData?.[params.brand] ?? {}),
|
||||
reportedAt,
|
||||
},
|
||||
},
|
||||
} satisfies NonIntegratedProductRecord;
|
||||
}
|
||||
@ -0,0 +1,179 @@
|
||||
import type {
|
||||
BrandFilingPlanningFieldKey,
|
||||
BrandFilingPlanningForm,
|
||||
BrandFilingPlanningLineItem,
|
||||
BrandFilingPlanningLineItemFieldKey,
|
||||
GujiaProductInfoConfirmationLineItem,
|
||||
GujiaQuotationHeaderFieldKey,
|
||||
GujiaQuotationHeaderForm,
|
||||
GujiaQuotationLineItem,
|
||||
} from "@/app/intake/brand-filing/brandFilingTypes";
|
||||
|
||||
function hasText(value: string | null | undefined) {
|
||||
return Boolean(value?.trim());
|
||||
}
|
||||
|
||||
function pushMissingField(
|
||||
missingFields: string[],
|
||||
value: string | null | undefined,
|
||||
label: string,
|
||||
) {
|
||||
if (!hasText(value)) {
|
||||
missingFields.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
const PLANNING_REQUIRED_FORM_FIELDS = [
|
||||
["planningProductLine", "企划产品线"],
|
||||
["productLine", "产品线"],
|
||||
["annualPlanStatus", "是否年度规划内"],
|
||||
["developmentMode", "开发方式"],
|
||||
["productSeries", "产品系列"],
|
||||
["developmentCategory", "开发类别"],
|
||||
["supplierName", "买采供应商名称"],
|
||||
["warehousingStatus", "是否入仓"],
|
||||
["launchDate", "需求上市时间"],
|
||||
["activityStyleStatus", "是否活动款"],
|
||||
["imageUrl", "图片"],
|
||||
["productDemandAnalysis", "产品需求分析"],
|
||||
["legacyIterationNote", "特殊说明(是否老款迭代)"],
|
||||
["productManager", "产品经理"],
|
||||
["planningOwner", "企划负责人"],
|
||||
["purchaseOwner", "采购负责人"],
|
||||
] as const satisfies readonly [BrandFilingPlanningFieldKey, string][];
|
||||
|
||||
const PLANNING_REQUIRED_LINE_FIELDS = [
|
||||
["itemCode", "货号"],
|
||||
["retailPriceDemand", "零售价需求"],
|
||||
["purchasePriceDemand", "采购价需求"],
|
||||
["sizeRequirement", "尺寸要求"],
|
||||
["materialAndColor", "材质/颜色"],
|
||||
["seriesName", "系列"],
|
||||
["channel", "渠道"],
|
||||
["month1Sales", "第1个月"],
|
||||
["month2Sales", "第2个月"],
|
||||
["month3Sales", "第3个月"],
|
||||
["month4To6Sales", "第4-6月"],
|
||||
["month7To9Sales", "第7-9月"],
|
||||
["month10To12Sales", "第10-12月"],
|
||||
] as const satisfies readonly [BrandFilingPlanningLineItemFieldKey, string][];
|
||||
|
||||
const QUOTATION_REQUIRED_HEADER_FIELDS = [
|
||||
["contactName", "编号"],
|
||||
["demandCompany", "TO(需求方)"],
|
||||
["demandMobile", "需求方手机"],
|
||||
["supplyCompany", "FROM(供应方)"],
|
||||
["supplyMobile", "供应方手机"],
|
||||
] as const satisfies readonly [GujiaQuotationHeaderFieldKey, string][];
|
||||
|
||||
const QUOTATION_REQUIRED_LINE_FIELDS = [
|
||||
["sequenceNumber", "序号"],
|
||||
["itemCode", "产品货号"],
|
||||
["specification", "规格"],
|
||||
["imageUrl", "图片"],
|
||||
["purchasePrice", "采购价(含税)"],
|
||||
["salesPrice", "售价"],
|
||||
["material", "材质"],
|
||||
["sizeCm", "规格(CM)"],
|
||||
["minimumOrderQuantity", "起订量"],
|
||||
["leadTimeDays", "采购周期(天)"],
|
||||
["packageCount", "包装件数"],
|
||||
["packageMethod", "包装方式"],
|
||||
] as const satisfies readonly [keyof GujiaQuotationLineItem, string][];
|
||||
|
||||
const PRODUCT_INFO_REQUIRED_LINE_FIELDS = [
|
||||
["sequenceNumber", "序号"],
|
||||
["productImageUrl", "产品图片"],
|
||||
["productLine", "产品线"],
|
||||
["itemCode", "货号"],
|
||||
["productName", "品名"],
|
||||
["sizeCm", "规格"],
|
||||
["certificateMaterial", "合格证的主材/基材"],
|
||||
["accessoryMaterial", "辅材"],
|
||||
["packageCount", "包装件数"],
|
||||
["packageSizeCm", "外包装尺寸"],
|
||||
["packageVolumeM3", "外包装体积"],
|
||||
["grossWeightKg", "包装毛重"],
|
||||
["netWeightKg", "产品净重"],
|
||||
["assemblyMethod", "拆装方式"],
|
||||
["manufacturer", "合格证上的制造商"],
|
||||
["colorBoardCode", "色板编号"],
|
||||
] as const satisfies readonly [
|
||||
keyof GujiaProductInfoConfirmationLineItem,
|
||||
string,
|
||||
][];
|
||||
|
||||
export function validateBrandFilingPlanningRequiredFields(
|
||||
form: BrandFilingPlanningForm,
|
||||
lineItems: BrandFilingPlanningLineItem[],
|
||||
) {
|
||||
const missingFields: string[] = [];
|
||||
|
||||
PLANNING_REQUIRED_FORM_FIELDS.forEach(([key, label]) => {
|
||||
pushMissingField(missingFields, form[key], label);
|
||||
});
|
||||
|
||||
if (lineItems.length === 0) {
|
||||
missingFields.push("企划方案明细");
|
||||
}
|
||||
|
||||
lineItems.forEach((lineItem, index) => {
|
||||
PLANNING_REQUIRED_LINE_FIELDS.forEach(([key, label]) => {
|
||||
pushMissingField(missingFields, lineItem[key], `第 ${index + 1} 行 ${label}`);
|
||||
});
|
||||
});
|
||||
|
||||
return missingFields;
|
||||
}
|
||||
|
||||
export function validateGujiaQuotationRequiredFields(
|
||||
headerForm: GujiaQuotationHeaderForm,
|
||||
lineItems: GujiaQuotationLineItem[],
|
||||
) {
|
||||
const missingFields: string[] = [];
|
||||
|
||||
QUOTATION_REQUIRED_HEADER_FIELDS.forEach(([key, label]) => {
|
||||
pushMissingField(missingFields, headerForm[key], label);
|
||||
});
|
||||
|
||||
if (lineItems.length === 0) {
|
||||
missingFields.push("报价明细");
|
||||
}
|
||||
|
||||
lineItems.forEach((lineItem, index) => {
|
||||
QUOTATION_REQUIRED_LINE_FIELDS.forEach(([key, label]) => {
|
||||
pushMissingField(missingFields, lineItem[key], `第 ${index + 1} 行 ${label}`);
|
||||
});
|
||||
});
|
||||
|
||||
return missingFields;
|
||||
}
|
||||
|
||||
export function validateGujiaProductInfoConfirmationRequiredFields(
|
||||
lineItems: GujiaProductInfoConfirmationLineItem[],
|
||||
) {
|
||||
const missingFields: string[] = [];
|
||||
|
||||
if (lineItems.length === 0) {
|
||||
missingFields.push("信息确认函明细");
|
||||
}
|
||||
|
||||
lineItems.forEach((lineItem, index) => {
|
||||
PRODUCT_INFO_REQUIRED_LINE_FIELDS.forEach(([key, label]) => {
|
||||
pushMissingField(missingFields, lineItem[key], `第 ${index + 1} 行 ${label}`);
|
||||
});
|
||||
});
|
||||
|
||||
return missingFields;
|
||||
}
|
||||
|
||||
export function buildRequiredFieldsMessage(
|
||||
title: string,
|
||||
missingFields: string[],
|
||||
) {
|
||||
const previewFields = missingFields.slice(0, 8).join("、");
|
||||
const suffix =
|
||||
missingFields.length > 8 ? ` 等 ${missingFields.length} 个字段` : "";
|
||||
|
||||
return `${title}还有必填内容未填写:${previewFields}${suffix}。填完后才能确认。`;
|
||||
}
|
||||
@ -109,6 +109,16 @@ export function getBrandFilingBasicInfoApprovedAt(
|
||||
);
|
||||
}
|
||||
|
||||
export function getBrandFilingEnteredAt(
|
||||
product: NonIntegratedProductRecord,
|
||||
brand: BrandFilingBrand,
|
||||
) {
|
||||
return (
|
||||
safeTrim(product.brandFilingData?.[brand]?.enteredAt) ||
|
||||
getBrandFilingBasicInfoApprovedAt(product)
|
||||
);
|
||||
}
|
||||
|
||||
function compareProductsByBrandFilingSequenceAnchor(
|
||||
left: { product: NonIntegratedProductRecord; sourceIndex: number },
|
||||
right: { product: NonIntegratedProductRecord; sourceIndex: number },
|
||||
|
||||
@ -31,6 +31,7 @@ export type BrandFilingPlanningFieldKey =
|
||||
| "imageUrl";
|
||||
|
||||
export type BrandFilingPlanningLineItemFieldKey =
|
||||
| "brandItemCode"
|
||||
| "itemCode"
|
||||
| "retailPriceDemand"
|
||||
| "purchasePriceDemand"
|
||||
@ -80,6 +81,7 @@ export type GujiaQuotationLineItem = {
|
||||
productId: string;
|
||||
skuId: string;
|
||||
sequenceNumber: string;
|
||||
brandItemCode: string;
|
||||
itemCode: string;
|
||||
specification: string;
|
||||
imageUrl: string;
|
||||
@ -134,6 +136,8 @@ export type GujiaProductInfoConfirmationRecord = {
|
||||
|
||||
export type BrandFilingBrandData = {
|
||||
sequenceNumber?: number;
|
||||
enteredAt?: string;
|
||||
reportedAt?: string;
|
||||
planningTable?: BrandFilingPlanningRecord;
|
||||
gujiaQuotationTable?: GujiaQuotationRecord;
|
||||
gujiaProductInfoConfirmation?: GujiaProductInfoConfirmationRecord;
|
||||
@ -178,6 +182,7 @@ export const BRAND_FILING_PLANNING_FIELD_KEYS = [
|
||||
] as const satisfies readonly BrandFilingPlanningFieldKey[];
|
||||
|
||||
export const BRAND_FILING_PLANNING_LINE_ITEM_FIELD_KEYS = [
|
||||
"brandItemCode",
|
||||
"itemCode",
|
||||
"retailPriceDemand",
|
||||
"purchasePriceDemand",
|
||||
@ -316,6 +321,7 @@ export function normalizeGujiaQuotationLineItem(
|
||||
productId: "",
|
||||
skuId: "",
|
||||
sequenceNumber: "",
|
||||
brandItemCode: "",
|
||||
itemCode: "",
|
||||
specification: "",
|
||||
imageUrl: "",
|
||||
@ -336,6 +342,7 @@ export function normalizeGujiaQuotationLineItem(
|
||||
productId: readText(value.productId),
|
||||
skuId: readText(value.skuId),
|
||||
sequenceNumber: readText(value.sequenceNumber),
|
||||
brandItemCode: readText(value.brandItemCode),
|
||||
itemCode: readText(value.itemCode),
|
||||
specification: readText(value.specification),
|
||||
imageUrl: readText(value.imageUrl),
|
||||
@ -440,6 +447,8 @@ function normalizeBrandFilingBrandData(
|
||||
if (!isRecord(value)) return undefined;
|
||||
|
||||
const sequenceNumber = readOptionalSequenceNumber(value.sequenceNumber);
|
||||
const enteredAt = readText(value.enteredAt);
|
||||
const reportedAt = readText(value.reportedAt);
|
||||
const planningTable = normalizeBrandFilingPlanningRecord(value.planningTable);
|
||||
const gujiaQuotationTable = normalizeGujiaQuotationRecord(
|
||||
value.gujiaQuotationTable,
|
||||
@ -450,6 +459,8 @@ function normalizeBrandFilingBrandData(
|
||||
);
|
||||
if (
|
||||
!sequenceNumber &&
|
||||
!enteredAt &&
|
||||
!reportedAt &&
|
||||
!planningTable &&
|
||||
!gujiaQuotationTable &&
|
||||
!gujiaProductInfoConfirmation
|
||||
@ -459,6 +470,8 @@ function normalizeBrandFilingBrandData(
|
||||
|
||||
return {
|
||||
...(sequenceNumber ? { sequenceNumber } : {}),
|
||||
...(enteredAt ? { enteredAt } : {}),
|
||||
...(reportedAt ? { reportedAt } : {}),
|
||||
...(planningTable ? { planningTable } : {}),
|
||||
...(gujiaQuotationTable ? { gujiaQuotationTable } : {}),
|
||||
...(gujiaProductInfoConfirmation ? { gujiaProductInfoConfirmation } : {}),
|
||||
|
||||
@ -292,6 +292,7 @@ function buildDefaultLineItem(params: {
|
||||
sequenceNumber && sequenceNumber > 0
|
||||
? String(sequenceNumber)
|
||||
: String(productIndex + 1),
|
||||
brandItemCode: "",
|
||||
itemCode,
|
||||
specification: buildSpecification(product, measurementRow, skuItem),
|
||||
imageUrl: previewImage,
|
||||
|
||||
@ -16,8 +16,8 @@ const SHEET_NAME = "报价单指导模板";
|
||||
const DATA_START_ROW = 6;
|
||||
const DATA_END_ROW = 279;
|
||||
const FOOTER_START_ROW = 280;
|
||||
const DATA_COLUMN_COUNT = 13;
|
||||
const IMAGE_COLUMN = 4;
|
||||
const DATA_COLUMN_COUNT = 14;
|
||||
const IMAGE_COLUMN = 5;
|
||||
const FOOTER_NOTE_ROW_COUNT = 5;
|
||||
const TEMPLATE_NOTE_TEXT = [
|
||||
"1、上述报价含增值税13%;",
|
||||
@ -160,6 +160,105 @@ function clearRowValues(
|
||||
}
|
||||
}
|
||||
|
||||
function cloneCellStylePart<T>(value: T): T {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function copyCellStyle(sourceCell: ExcelJS.Cell, targetCell: ExcelJS.Cell) {
|
||||
targetCell.numFmt = sourceCell.numFmt;
|
||||
targetCell.font = cloneCellStylePart(sourceCell.font);
|
||||
targetCell.alignment = cloneCellStylePart(sourceCell.alignment);
|
||||
targetCell.fill = cloneCellStylePart(sourceCell.fill);
|
||||
targetCell.border = cloneCellStylePart(sourceCell.border);
|
||||
targetCell.protection = cloneCellStylePart(sourceCell.protection);
|
||||
}
|
||||
|
||||
function unmergeIfExists(worksheet: ExcelJS.Worksheet, ref: string) {
|
||||
try {
|
||||
worksheet.unMergeCells(ref);
|
||||
} catch {
|
||||
// Some refs may already be unmerged after workbook normalization.
|
||||
}
|
||||
}
|
||||
|
||||
function mergeCellsWithStyle(worksheet: ExcelJS.Worksheet, ref: string) {
|
||||
const sourceCell = worksheet.getCell(ref.split(":")[0]!);
|
||||
const styleSnapshot = {
|
||||
numFmt: sourceCell.numFmt,
|
||||
font: cloneCellStylePart(sourceCell.font),
|
||||
alignment: cloneCellStylePart(sourceCell.alignment),
|
||||
fill: cloneCellStylePart(sourceCell.fill),
|
||||
border: cloneCellStylePart(sourceCell.border),
|
||||
protection: cloneCellStylePart(sourceCell.protection),
|
||||
};
|
||||
|
||||
worksheet.mergeCells(ref);
|
||||
const mergedCell = worksheet.getCell(ref.split(":")[0]!);
|
||||
mergedCell.numFmt = styleSnapshot.numFmt;
|
||||
mergedCell.font = cloneCellStylePart(styleSnapshot.font);
|
||||
mergedCell.alignment = cloneCellStylePart(styleSnapshot.alignment);
|
||||
mergedCell.fill = cloneCellStylePart(styleSnapshot.fill);
|
||||
mergedCell.border = cloneCellStylePart(styleSnapshot.border);
|
||||
mergedCell.protection = cloneCellStylePart(styleSnapshot.protection);
|
||||
}
|
||||
|
||||
function prepareQuotationTemplateColumns(worksheet: ExcelJS.Worksheet) {
|
||||
worksheet.getColumn(14).width = worksheet.getColumn(13).width ?? 9.65;
|
||||
|
||||
for (let rowNumber = 1; rowNumber <= FOOTER_START_ROW + 10; rowNumber += 1) {
|
||||
copyCellStyle(
|
||||
worksheet.getRow(rowNumber).getCell(13),
|
||||
worksheet.getRow(rowNumber).getCell(14),
|
||||
);
|
||||
}
|
||||
|
||||
[
|
||||
"A1:M1",
|
||||
"A2:M2",
|
||||
"A3:F3",
|
||||
"H3:M3",
|
||||
"A4:F4",
|
||||
"H4:M4",
|
||||
].forEach((ref) => unmergeIfExists(worksheet, ref));
|
||||
|
||||
mergeCellsWithStyle(worksheet, "A1:N1");
|
||||
mergeCellsWithStyle(worksheet, "A2:N2");
|
||||
mergeCellsWithStyle(worksheet, "A3:G3");
|
||||
mergeCellsWithStyle(worksheet, "I3:N3");
|
||||
mergeCellsWithStyle(worksheet, "A4:G4");
|
||||
mergeCellsWithStyle(worksheet, "I4:N4");
|
||||
|
||||
const headerLabels = [
|
||||
"序号",
|
||||
"品牌货号",
|
||||
"产品货号",
|
||||
"规格",
|
||||
"图片",
|
||||
"采购价(含税)",
|
||||
"售价",
|
||||
"材质",
|
||||
"规格(CM)",
|
||||
"起订量",
|
||||
"采购周期\n(天)",
|
||||
"包装件数",
|
||||
"包装方式",
|
||||
"备注",
|
||||
];
|
||||
headerLabels.forEach((label, index) => {
|
||||
const cell = worksheet.getRow(DATA_START_ROW - 1).getCell(index + 1);
|
||||
cell.value = label;
|
||||
cell.alignment = {
|
||||
...cell.alignment,
|
||||
horizontal: "center",
|
||||
vertical: "middle",
|
||||
wrapText: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function restoreFooterLayout(worksheet: ExcelJS.Worksheet, rowCount: number) {
|
||||
const noteStartRow = DATA_START_ROW + rowCount;
|
||||
const noteEndRow = noteStartRow + FOOTER_NOTE_ROW_COUNT - 1;
|
||||
@ -173,7 +272,7 @@ function restoreFooterLayout(worksheet: ExcelJS.Worksheet, rowCount: number) {
|
||||
clearRowValues(worksheet, rowNumber);
|
||||
}
|
||||
|
||||
worksheet.mergeCells(`A${noteStartRow}:M${noteEndRow}`);
|
||||
worksheet.mergeCells(`A${noteStartRow}:N${noteEndRow}`);
|
||||
const noteCell = worksheet.getCell(`A${noteStartRow}`);
|
||||
noteCell.value = TEMPLATE_NOTE_TEXT;
|
||||
noteCell.font = {
|
||||
@ -187,9 +286,9 @@ function restoreFooterLayout(worksheet: ExcelJS.Worksheet, rowCount: number) {
|
||||
wrapText: true,
|
||||
};
|
||||
|
||||
worksheet.mergeCells(`A${spacerRow}:M${spacerRow}`);
|
||||
worksheet.mergeCells(`B${warehouseHeaderRow}:M${warehouseHeaderRow}`);
|
||||
worksheet.mergeCells(`B${warehouseAddressRow}:M${warehouseAddressRow}`);
|
||||
worksheet.mergeCells(`A${spacerRow}:N${spacerRow}`);
|
||||
worksheet.mergeCells(`B${warehouseHeaderRow}:N${warehouseHeaderRow}`);
|
||||
worksheet.mergeCells(`B${warehouseAddressRow}:N${warehouseAddressRow}`);
|
||||
|
||||
worksheet.getCell(`A${warehouseHeaderRow}`).value = "基地名称";
|
||||
worksheet.getCell(`B${warehouseHeaderRow}`).value = "仓库详细地址";
|
||||
@ -329,16 +428,17 @@ async function buildWorkbookBytes(products: NonIntegratedProductRecord[]) {
|
||||
throw new AppError(ErrorCode.SYSTEM_INTERNAL, "顾家产品报价表模板读取失败");
|
||||
}
|
||||
|
||||
prepareQuotationTemplateColumns(worksheet);
|
||||
prepareWorksheetRows(worksheet, displayRows.length);
|
||||
restoreFooterLayout(worksheet, displayRows.length);
|
||||
|
||||
worksheet.getCell("A2").value = `编号:${initialState.headerForm.contactName.trim()}`;
|
||||
worksheet.getCell("A3").value = `TO(需求方): ${initialState.headerForm.demandCompany.trim()}`;
|
||||
worksheet.getCell("G3").value = "FROM(供应方):";
|
||||
worksheet.getCell("H3").value = initialState.headerForm.supplyCompany.trim();
|
||||
worksheet.getCell("H3").value = "FROM(供应方):";
|
||||
worksheet.getCell("I3").value = initialState.headerForm.supplyCompany.trim();
|
||||
worksheet.getCell("A4").value = `MOB(手机):${initialState.headerForm.demandMobile.trim()}`;
|
||||
worksheet.getCell("G4").value = "MOB(手机):";
|
||||
worksheet.getCell("H4").value = initialState.headerForm.supplyMobile.trim();
|
||||
worksheet.getCell("H4").value = "MOB(手机):";
|
||||
worksheet.getCell("I4").value = initialState.headerForm.supplyMobile.trim();
|
||||
|
||||
const imageTasks = displayRows.map(async (row, index) => {
|
||||
const rowNumber = DATA_START_ROW + index;
|
||||
@ -349,18 +449,19 @@ async function buildWorkbookBytes(products: NonIntegratedProductRecord[]) {
|
||||
}
|
||||
}
|
||||
|
||||
worksheet.getCell(`B${rowNumber}`).value = row.itemCode.trim();
|
||||
worksheet.getCell(`C${rowNumber}`).value = row.specification.trim();
|
||||
worksheet.getCell(`D${rowNumber}`).value = "";
|
||||
worksheet.getCell(`E${rowNumber}`).value = row.purchasePrice.trim();
|
||||
worksheet.getCell(`F${rowNumber}`).value = row.salesPrice.trim();
|
||||
worksheet.getCell(`G${rowNumber}`).value = row.material.trim();
|
||||
worksheet.getCell(`H${rowNumber}`).value = row.sizeCm.trim();
|
||||
worksheet.getCell(`I${rowNumber}`).value = row.minimumOrderQuantity.trim();
|
||||
worksheet.getCell(`J${rowNumber}`).value = row.leadTimeDays.trim();
|
||||
worksheet.getCell(`K${rowNumber}`).value = row.packageCount.trim();
|
||||
worksheet.getCell(`L${rowNumber}`).value = row.packageMethod.trim();
|
||||
worksheet.getCell(`M${rowNumber}`).value = row.remark.trim();
|
||||
worksheet.getCell(`B${rowNumber}`).value = row.brandItemCode.trim();
|
||||
worksheet.getCell(`C${rowNumber}`).value = row.itemCode.trim();
|
||||
worksheet.getCell(`D${rowNumber}`).value = row.specification.trim();
|
||||
worksheet.getCell(`E${rowNumber}`).value = "";
|
||||
worksheet.getCell(`F${rowNumber}`).value = row.purchasePrice.trim();
|
||||
worksheet.getCell(`G${rowNumber}`).value = row.salesPrice.trim();
|
||||
worksheet.getCell(`H${rowNumber}`).value = row.material.trim();
|
||||
worksheet.getCell(`I${rowNumber}`).value = row.sizeCm.trim();
|
||||
worksheet.getCell(`J${rowNumber}`).value = row.minimumOrderQuantity.trim();
|
||||
worksheet.getCell(`K${rowNumber}`).value = row.leadTimeDays.trim();
|
||||
worksheet.getCell(`L${rowNumber}`).value = row.packageCount.trim();
|
||||
worksheet.getCell(`M${rowNumber}`).value = row.packageMethod.trim();
|
||||
worksheet.getCell(`N${rowNumber}`).value = row.remark.trim();
|
||||
|
||||
if (!row.imageUrl.trim()) return;
|
||||
|
||||
|
||||
@ -328,16 +328,34 @@ function insertStyledRows(
|
||||
}
|
||||
|
||||
const DYNAMIC_TEMPLATE_MERGE_REFS = [
|
||||
"B10:C10",
|
||||
"C10:D10",
|
||||
"D10:E10",
|
||||
"E10:F10",
|
||||
"F10:G10",
|
||||
"G10:H10",
|
||||
"H10:I10",
|
||||
"I10:J10",
|
||||
"J10:L10",
|
||||
"K10:L10",
|
||||
"B11:C11",
|
||||
"C11:D11",
|
||||
"D11:E11",
|
||||
"E11:F11",
|
||||
"F11:G11",
|
||||
"G11:H11",
|
||||
"H11:I11",
|
||||
"I11:J11",
|
||||
"J11:L11",
|
||||
"K11:L11",
|
||||
"A12:L12",
|
||||
"A13:A14",
|
||||
"B13:B14",
|
||||
"C13:C14",
|
||||
"C13:D13",
|
||||
"D13:E13",
|
||||
"E13:L13",
|
||||
"F13:L13",
|
||||
"I14:J14",
|
||||
"K14:L14",
|
||||
"A15:B15",
|
||||
@ -491,19 +509,95 @@ function setCellCenterAlignment(
|
||||
};
|
||||
}
|
||||
|
||||
function applyCenterAcrossRange(
|
||||
function clearRowValues(
|
||||
worksheet: ExcelJS.Worksheet,
|
||||
ref: string,
|
||||
rowNumber: number,
|
||||
startColumnNumber = 1,
|
||||
endColumnNumber = 12,
|
||||
) {
|
||||
const addresses = expandRangeAddresses(ref);
|
||||
if (addresses.length === 0) return;
|
||||
for (
|
||||
let columnNumber = startColumnNumber;
|
||||
columnNumber <= endColumnNumber;
|
||||
columnNumber += 1
|
||||
) {
|
||||
worksheet.getCell(`${columnNumberToName(columnNumber)}${rowNumber}`).value =
|
||||
"";
|
||||
}
|
||||
}
|
||||
|
||||
const firstCell = worksheet.getCell(addresses[0]!);
|
||||
firstCell.alignment = {
|
||||
...firstCell.alignment,
|
||||
horizontal: "centerContinuous",
|
||||
vertical: "middle",
|
||||
};
|
||||
function setTextCellValue(
|
||||
worksheet: ExcelJS.Worksheet,
|
||||
address: string,
|
||||
value: string,
|
||||
) {
|
||||
worksheet.getCell(address).value = value.trim();
|
||||
setCellCenterAlignment(worksheet, address);
|
||||
}
|
||||
|
||||
function writePlanningHeaderRows(params: {
|
||||
worksheet: ExcelJS.Worksheet;
|
||||
planHeaderRow: number;
|
||||
promotionHeaderTopRow: number;
|
||||
promotionHeaderBottomRow: number;
|
||||
}) {
|
||||
const {
|
||||
worksheet,
|
||||
planHeaderRow,
|
||||
promotionHeaderTopRow,
|
||||
promotionHeaderBottomRow,
|
||||
} = params;
|
||||
|
||||
clearRowValues(worksheet, planHeaderRow);
|
||||
applyMergeRefs(worksheet, [
|
||||
`C${planHeaderRow}:D${planHeaderRow}`,
|
||||
`E${planHeaderRow}:F${planHeaderRow}`,
|
||||
`G${planHeaderRow}:H${planHeaderRow}`,
|
||||
`I${planHeaderRow}:J${planHeaderRow}`,
|
||||
`K${planHeaderRow}:L${planHeaderRow}`,
|
||||
]);
|
||||
[
|
||||
["A", "品牌货号"],
|
||||
["B", "货号"],
|
||||
["C", "零售价需求"],
|
||||
["E", "采购价需求"],
|
||||
["G", "尺寸要求"],
|
||||
["I", "材质/颜色"],
|
||||
["K", "特殊说明(是否老款迭代)"],
|
||||
].forEach(([column, label]) => {
|
||||
setTextCellValue(worksheet, `${column}${planHeaderRow}`, label);
|
||||
});
|
||||
|
||||
clearRowValues(worksheet, promotionHeaderTopRow);
|
||||
clearRowValues(worksheet, promotionHeaderBottomRow);
|
||||
applyMergeRefs(worksheet, [
|
||||
`A${promotionHeaderTopRow}:A${promotionHeaderBottomRow}`,
|
||||
`B${promotionHeaderTopRow}:B${promotionHeaderBottomRow}`,
|
||||
`C${promotionHeaderTopRow}:C${promotionHeaderBottomRow}`,
|
||||
`D${promotionHeaderTopRow}:E${promotionHeaderTopRow}`,
|
||||
`F${promotionHeaderTopRow}:L${promotionHeaderTopRow}`,
|
||||
`K${promotionHeaderBottomRow}:L${promotionHeaderBottomRow}`,
|
||||
]);
|
||||
[
|
||||
["A", "系列"],
|
||||
["B", "品牌货号"],
|
||||
["C", "货号"],
|
||||
["D", "上市计划"],
|
||||
["F", "上市季度销量"],
|
||||
].forEach(([column, label]) => {
|
||||
setTextCellValue(worksheet, `${column}${promotionHeaderTopRow}`, label);
|
||||
});
|
||||
[
|
||||
["D", "全年销售总量"],
|
||||
["E", "渠道"],
|
||||
["F", "第1个月"],
|
||||
["G", "第2个月"],
|
||||
["H", "第3个月"],
|
||||
["I", "第4-6月"],
|
||||
["J", "第7-9月"],
|
||||
["K", "第10-12月"],
|
||||
].forEach(([column, label]) => {
|
||||
setTextCellValue(worksheet, `${column}${promotionHeaderBottomRow}`, label);
|
||||
});
|
||||
}
|
||||
|
||||
function writePlanningSectionRows(params: {
|
||||
@ -566,22 +660,13 @@ function writePlanningSectionRows(params: {
|
||||
|
||||
applyMergeRefs(worksheet, [
|
||||
`A${promotionSectionTitleRow}:L${promotionSectionTitleRow}`,
|
||||
`A${promotionHeaderTopRow}:A${promotionHeaderBottomRow}`,
|
||||
`B${promotionHeaderTopRow}:B${promotionHeaderBottomRow}`,
|
||||
`C${promotionHeaderTopRow}:D${promotionHeaderTopRow}`,
|
||||
]);
|
||||
applyCenterAcrossRange(
|
||||
writePlanningHeaderRows({
|
||||
worksheet,
|
||||
`E${promotionHeaderTopRow}:L${promotionHeaderTopRow}`,
|
||||
);
|
||||
applyCenterAcrossRange(
|
||||
worksheet,
|
||||
`I${promotionHeaderBottomRow}:J${promotionHeaderBottomRow}`,
|
||||
);
|
||||
applyCenterAcrossRange(
|
||||
worksheet,
|
||||
`K${promotionHeaderBottomRow}:L${promotionHeaderBottomRow}`,
|
||||
);
|
||||
planHeaderRow: planDataStartRow - 1,
|
||||
promotionHeaderTopRow,
|
||||
promotionHeaderBottomRow,
|
||||
});
|
||||
|
||||
for (let offset = 0; offset < rowCount; offset += 1) {
|
||||
const rowNumber = promotionDataStartRow + offset;
|
||||
@ -604,43 +689,48 @@ function writePlanningSectionRows(params: {
|
||||
|
||||
displayRows.forEach((displayRow, index) => {
|
||||
const planRowNumber = planDataStartRow + index;
|
||||
setCellValue(
|
||||
setTextCellValue(
|
||||
worksheet,
|
||||
`A${planRowNumber}`,
|
||||
displayRow.brandItemCode,
|
||||
);
|
||||
setCellValue(
|
||||
worksheet,
|
||||
`B${planRowNumber}`,
|
||||
"itemCode",
|
||||
displayRow.itemCode,
|
||||
);
|
||||
setCellValue(
|
||||
worksheet,
|
||||
`B${planRowNumber}`,
|
||||
`C${planRowNumber}`,
|
||||
"retailPriceDemand",
|
||||
displayRow.retailPriceDemand,
|
||||
);
|
||||
setCellValue(
|
||||
worksheet,
|
||||
`D${planRowNumber}`,
|
||||
`E${planRowNumber}`,
|
||||
"purchasePriceDemand",
|
||||
displayRow.purchasePriceDemand,
|
||||
);
|
||||
setCellValue(
|
||||
worksheet,
|
||||
`F${planRowNumber}`,
|
||||
`G${planRowNumber}`,
|
||||
"sizeRequirement",
|
||||
displayRow.sizeRequirement,
|
||||
);
|
||||
setCellValue(
|
||||
worksheet,
|
||||
`H${planRowNumber}`,
|
||||
`I${planRowNumber}`,
|
||||
"materialAndColor",
|
||||
displayRow.materialAndColor,
|
||||
);
|
||||
setCellValue(
|
||||
worksheet,
|
||||
`J${planRowNumber}`,
|
||||
`K${planRowNumber}`,
|
||||
"legacyIterationNote",
|
||||
record.form.legacyIterationNote,
|
||||
);
|
||||
["A", "B", "D", "F", "H", "J"].forEach((column) => {
|
||||
["A", "B", "C", "E", "G", "I", "K"].forEach((column) => {
|
||||
setCellCenterAlignment(worksheet, `${column}${planRowNumber}`);
|
||||
});
|
||||
|
||||
@ -651,51 +741,56 @@ function writePlanningSectionRows(params: {
|
||||
"seriesName",
|
||||
displayRow.seriesName,
|
||||
);
|
||||
setCellValue(
|
||||
setTextCellValue(
|
||||
worksheet,
|
||||
`B${promotionRowNumber}`,
|
||||
displayRow.brandItemCode,
|
||||
);
|
||||
setCellValue(
|
||||
worksheet,
|
||||
`C${promotionRowNumber}`,
|
||||
"itemCode",
|
||||
displayRow.itemCode,
|
||||
);
|
||||
setCellValue(
|
||||
worksheet,
|
||||
`C${promotionRowNumber}`,
|
||||
`D${promotionRowNumber}`,
|
||||
"annualSalesTarget",
|
||||
displayRow.annualSalesTarget,
|
||||
);
|
||||
setCellValue(
|
||||
worksheet,
|
||||
`D${promotionRowNumber}`,
|
||||
`E${promotionRowNumber}`,
|
||||
"channel",
|
||||
displayRow.channel,
|
||||
);
|
||||
setCellValue(
|
||||
worksheet,
|
||||
`E${promotionRowNumber}`,
|
||||
`F${promotionRowNumber}`,
|
||||
"month1Sales",
|
||||
displayRow.month1Sales,
|
||||
);
|
||||
setCellValue(
|
||||
worksheet,
|
||||
`F${promotionRowNumber}`,
|
||||
`G${promotionRowNumber}`,
|
||||
"month2Sales",
|
||||
displayRow.month2Sales,
|
||||
);
|
||||
setCellValue(
|
||||
worksheet,
|
||||
`G${promotionRowNumber}`,
|
||||
`H${promotionRowNumber}`,
|
||||
"month3Sales",
|
||||
displayRow.month3Sales,
|
||||
);
|
||||
setCellValue(
|
||||
worksheet,
|
||||
`H${promotionRowNumber}`,
|
||||
`I${promotionRowNumber}`,
|
||||
"month4To6Sales",
|
||||
displayRow.month4To6Sales,
|
||||
);
|
||||
setCellValue(
|
||||
worksheet,
|
||||
`I${promotionRowNumber}`,
|
||||
`J${promotionRowNumber}`,
|
||||
"month7To9Sales",
|
||||
displayRow.month7To9Sales,
|
||||
);
|
||||
@ -705,9 +800,11 @@ function writePlanningSectionRows(params: {
|
||||
"month10To12Sales",
|
||||
displayRow.month10To12Sales,
|
||||
);
|
||||
["A", "B", "C", "D", "E", "F", "G", "H", "I", "K"].forEach((column) => {
|
||||
["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"].forEach(
|
||||
(column) => {
|
||||
setCellCenterAlignment(worksheet, `${column}${promotionRowNumber}`);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
setCellValue(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user