+
+
+ {isClearing ? 'Click row to clear to' : 'Click row to copy to'}
{rowsBelow > 0 && (
<>
-
+
- {rowsBelow === 1
- ? "Copy to 1 row below"
- : `Copy to all ${rowsBelow} rows below`}
+ {isClearing
+ ? (rowsBelow === 1
+ ? 'Clear this field on 1 row below'
+ : `Clear this field on all ${rowsBelow} rows below`)
+ : (rowsBelow === 1
+ ? 'Copy to 1 row below'
+ : `Copy to all ${rowsBelow} rows below`)}
>
)}
-
-
-
+
diff --git a/inventory/src/components/product-import/steps/ValidationStep/components/ValidationTable.tsx b/inventory/src/components/product-import/steps/ValidationStep/components/ValidationTable.tsx
index e2f7ca4..70559ed 100644
--- a/inventory/src/components/product-import/steps/ValidationStep/components/ValidationTable.tsx
+++ b/inventory/src/components/product-import/steps/ValidationStep/components/ValidationTable.tsx
@@ -17,7 +17,7 @@ import { apiFetch } from '@/utils/api';
import { type ColumnDef } from '@tanstack/react-table';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Checkbox } from '@/components/ui/checkbox';
-import { ArrowDown, Wand2, Loader2, Calculator, Scale, Pin, PinOff } from 'lucide-react';
+import { ArrowDown, Eraser, Wand2, Loader2, Calculator, Scale, Pin, PinOff } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
@@ -134,6 +134,8 @@ interface CellWrapperProps {
isCopyDownSource: boolean;
isInCopyDownRange: boolean;
isCopyDownTarget: boolean;
+ /** Active copy-down has an empty source, i.e. it clears the targets */
+ isCopyDownClearing: boolean;
totalRowCount: number;
// Inline AI validation (Groq-powered)
inlineAiSuggestion?: InlineAiSuggestion;
@@ -162,6 +164,7 @@ const CellWrapper = memo(({
isCopyDownSource,
isInCopyDownRange,
isCopyDownTarget,
+ isCopyDownClearing,
totalRowCount,
inlineAiSuggestion,
isInlineAiValidating = false,
@@ -200,19 +203,41 @@ const CellWrapper = memo(({
// Check if cell has a value (for showing copy-down button)
const hasValue = value !== undefined && value !== null && value !== '';
- // Check if field has unique validation rule (copy-down should be disabled)
+ // Blank source = copy-down CLEARS the cells below. Matches isBlankCellValue()
+ // in the store, so the button's look always matches what the copy will do.
+ const isBlankValue = !hasValue ||
+ (typeof value === 'string' && value.trim() === '') ||
+ (Array.isArray(value) && value.length === 0);
+
+ // Check if field has unique validation rule (values can't be duplicated down it)
const hasUniqueValidation = field.validations?.some((v: Validation) => v.rule === 'unique') ?? false;
// Check if cell has errors (for positioning copy-down button)
const hasErrors = errors.length > 0;
+ // The cells themselves suppress the error icon on an empty cell whose only error
+ // is "required" (see showErrorIcon in InputCell/SelectCell/MultiSelectCell) - the
+ // red border carries the message instead. Mirror that rule, or the copy/clear
+ // button dodges an icon that was never drawn and drifts toward the middle.
+ const showsErrorIcon = hasErrors &&
+ !(isBlankValue && errors.length === 1 && errors[0]?.type === ErrorType.Required);
+
// Show copy-down button when:
// - Cell is hovered
- // - Cell has a value
// - Not already in copy-down mode
// - There are rows below this one
- // - Field does NOT have unique validation (can't copy unique values)
- const showCopyDownButton = isHovered && hasValue && !isCopyDownActive && rowIndex < totalRowCount - 1 && !hasUniqueValidation;
+ // - Field does NOT have unique validation, OR we're clearing
+ //
+ // An EMPTY cell is a valid source: copying a blank down clears the cells below,
+ // which is the only bulk way to wipe a column of values that shouldn't be there.
+ // Clearing is safe even on unique fields (emptying rows can't create duplicates)
+ // and on required fields (the cleared cells simply flag as required again).
+ const showCopyDownButton = isHovered && !isCopyDownActive && rowIndex < totalRowCount - 1 &&
+ (!hasUniqueValidation || isBlankValue);
+
+ // Keep the copy/clear button in the cell's usual hover slot at the right edge,
+ // stepping left only when an error icon is actually occupying that slot.
+ const copyDownRightClass = showsErrorIcon ? 'right-7' : 'right-0.5';
// UPC Generation logic
const isUpcField = field.key === 'upc';
@@ -223,6 +248,14 @@ const CellWrapper = memo(({
const hasValidSupplier = /^\d+$/.test(supplierIdString);
const showGenerateUpcButton = isHovered && upcIsEmpty && !isValidating && !isCopyDownActive && !isGeneratingUpc;
+ // An empty UPC cell hosts both hover buttons; the copy/clear button owns the
+ // right edge, so the wider generate-UPC button parks to its left.
+ const generateUpcRightClass = !showCopyDownButton
+ ? 'right-1'
+ : showsErrorIcon
+ ? 'right-14'
+ : 'right-7';
+
// Handle starting copy-down
const handleStartCopyDown = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
@@ -868,11 +901,17 @@ const CellWrapper = memo(({
}, [needsCompany, needsLine, company, line]);
// Determine cell highlighting classes
+ // Amber for a clearing run (empty source) so the preview range reads as
+ // "these get wiped", not "these get filled"
const cellHighlightClass = cn(
'relative w-full group',
- isCopyDownSource && 'ring-2 ring-blue-500 ring-inset rounded',
- isInCopyDownRange && 'bg-blue-100',
- isCopyDownTarget && !isInCopyDownRange && 'hover:bg-blue-50 cursor-pointer'
+ isCopyDownSource && (isCopyDownClearing
+ ? 'ring-2 ring-amber-500 ring-inset rounded'
+ : 'ring-2 ring-blue-500 ring-inset rounded'),
+ isInCopyDownRange && (isCopyDownClearing ? 'bg-amber-100' : 'bg-blue-100'),
+ isCopyDownTarget && !isInCopyDownRange && (isCopyDownClearing
+ ? 'hover:bg-amber-50 cursor-pointer'
+ : 'hover:bg-blue-50 cursor-pointer')
);
// When in copy-down mode for this field, make cell non-interactive so clicks go to parent
@@ -932,18 +971,22 @@ const CellWrapper = memo(({
onClick={handleStartCopyDown}
className={cn(
'absolute top-1/2 -translate-y-1/2 z-10 p-1 rounded-full',
- 'bg-blue-50 hover:bg-blue-100 text-blue-500 hover:text-blue-600',
- '',
'shadow-sm',
- // Position further left if there are errors to avoid overlap
- hasErrors ? 'right-7' : 'right-0.5'
+ // Clearing is destructive - give it its own colour so it can't be
+ // mistaken for a normal copy at a glance
+ isBlankValue
+ ? 'bg-amber-50 hover:bg-amber-100 text-amber-600 hover:text-amber-700'
+ : 'bg-blue-50 hover:bg-blue-100 text-blue-500 hover:text-blue-600',
+ copyDownRightClass
)}
>
-
+ {isBlankValue
+ ?
+ :
}
- Copy value to rows below
+ {isBlankValue ? 'Clear this field on rows below' : 'Copy value to rows below'}
@@ -959,7 +1002,8 @@ const CellWrapper = memo(({
onClick={handleGenerateUpc}
disabled={!hasValidSupplier}
className={cn(
- 'absolute right-1 top-1/2 -translate-y-1/2 z-10 flex items-center gap-1',
+ 'absolute top-1/2 -translate-y-1/2 z-10 flex items-center gap-1',
+ generateUpcRightClass,
'rounded-md border border-input bg-background px-2 py-1 text-xs shadow-sm',
'opacity-0 group-hover:opacity-100 transition-opacity',
hasValidSupplier
@@ -1568,6 +1612,7 @@ const VirtualRow = memo(({
isCopyDownSource={isCopyDownSource}
isInCopyDownRange={isInCopyDownRange}
isCopyDownTarget={isCopyDownTarget}
+ isCopyDownClearing={copyDownMode.isClearing}
totalRowCount={totalRowCount}
inlineAiSuggestion={inlineAiSuggestion}
isInlineAiValidating={
diff --git a/inventory/src/components/product-import/steps/ValidationStep/hooks/useCopyDownValidation.ts b/inventory/src/components/product-import/steps/ValidationStep/hooks/useCopyDownValidation.ts
index 30d51a5..e585f54 100644
--- a/inventory/src/components/product-import/steps/ValidationStep/hooks/useCopyDownValidation.ts
+++ b/inventory/src/components/product-import/steps/ValidationStep/hooks/useCopyDownValidation.ts
@@ -2,6 +2,8 @@
* useCopyDownValidation Hook
*
* Watches for copy-down operations and triggers appropriate validations:
+ * - Every copied field -> re-run its field rules on the touched rows
+ * (so copying a BLANK down raises "required" errors on the cleared cells)
* - UPC-related fields (supplier, upc, barcode) -> UPC validation
* - Line field -> Inline AI validation for rows that gain sufficient context
*
@@ -13,6 +15,8 @@ import { useEffect } from 'react';
import { apiFetch } from '@/utils/api';
import { useValidationStore } from '../store/validationStore';
import { useUpcValidation } from './useUpcValidation';
+import { validateFieldValue } from './useValidationActions';
+import { ErrorSource, ErrorType } from '../store/types';
import type { Field } from '../../../types';
import {
buildNameValidationPayload,
@@ -80,10 +84,59 @@ export const useCopyDownValidation = () => {
// Subscribe to pending validations
const pendingUpcValidation = useValidationStore((state) => state.pendingCopyDownValidation);
+ const pendingRevalidation = useValidationStore((state) => state.pendingCopyDownRevalidation);
const pendingInlineAiValidation = useValidationStore((state) => state.pendingInlineAiValidation);
const clearPendingCopyDownValidation = useValidationStore((state) => state.clearPendingCopyDownValidation);
+ const clearPendingCopyDownRevalidation = useValidationStore((state) => state.clearPendingCopyDownRevalidation);
const clearPendingInlineAiValidation = useValidationStore((state) => state.clearPendingInlineAiValidation);
+ // Re-run field rules on the copied rows.
+ // Copying a value down clears errors optimistically in the store; this pass is
+ // what handles the opposite direction — a copied BLANK must re-raise "required"
+ // errors and downgrade the row status, exactly as clearing the cell by hand does.
+ // PERFORMANCE: results are collected first and written in ONE store update,
+ // since "Apply to All" can touch every row in the sheet.
+ useEffect(() => {
+ if (!pendingRevalidation) return;
+
+ const { fieldKey, affectedRows } = pendingRevalidation;
+ const { rows, fields, errors, applyFieldValidationResults } = useValidationStore.getState();
+
+ const field = fields.find((f) => f.key === fieldKey);
+ if (field) {
+ // Uniqueness is a cross-row rule: clearing a cell can resolve a duplicate that
+ // was flagged on a row we never touched. Re-check those rows too, but ONLY the
+ // ones showing an in-sheet duplicate error — never rows carrying a UPC-sourced
+ // error, which reflects the live catalog and can't be settled from the sheet.
+ const isUniqueField = field.validations?.some((v) => v.rule === 'unique') ?? false;
+ const rowsToCheck = isUniqueField
+ ? Array.from(new Set([
+ ...affectedRows,
+ ...rows.reduce
((acc, _row, rowIndex) => {
+ const stale = errors.get(rowIndex)?.[fieldKey]?.some(
+ (e) => e.type === ErrorType.Unique && e.source !== ErrorSource.Upc
+ );
+ if (stale) acc.push(rowIndex);
+ return acc;
+ }, []),
+ ])).sort((a, b) => a - b)
+ : affectedRows;
+
+ const results = rowsToCheck
+ .filter((rowIndex) => rows[rowIndex])
+ .map((rowIndex) => ({
+ rowIndex,
+ error: validateFieldValue(rows[rowIndex][fieldKey], field, rows, rowIndex),
+ }));
+
+ if (results.length > 0) {
+ applyFieldValidationResults(fieldKey, results);
+ }
+ }
+
+ clearPendingCopyDownRevalidation();
+ }, [pendingRevalidation, clearPendingCopyDownRevalidation]);
+
// Handle UPC validation
useEffect(() => {
if (!pendingUpcValidation) return;
diff --git a/inventory/src/components/product-import/steps/ValidationStep/hooks/useValidationActions.ts b/inventory/src/components/product-import/steps/ValidationStep/hooks/useValidationActions.ts
index 6655872..c0a2085 100644
--- a/inventory/src/components/product-import/steps/ValidationStep/hooks/useValidationActions.ts
+++ b/inventory/src/components/product-import/steps/ValidationStep/hooks/useValidationActions.ts
@@ -30,8 +30,11 @@ const isEmpty = (value: unknown): boolean => {
/**
* Validate a single field value against its validation rules
+ *
+ * Exported so bulk callers (e.g. copy-down re-validation) can reuse the exact
+ * same rule evaluation without pulling in the per-row store writes.
*/
-const validateFieldValue = (
+export const validateFieldValue = (
value: unknown,
field: Field,
allRows: RowData[],
diff --git a/inventory/src/components/product-import/steps/ValidationStep/store/types.ts b/inventory/src/components/product-import/steps/ValidationStep/store/types.ts
index 3c80b9b..a7bc85a 100644
--- a/inventory/src/components/product-import/steps/ValidationStep/store/types.ts
+++ b/inventory/src/components/product-import/steps/ValidationStep/store/types.ts
@@ -151,6 +151,8 @@ export interface CopyDownState {
sourceRowIndex: number | null;
sourceFieldKey: string | null;
targetRowIndex: number | null; // Hover preview - which row the user is hovering on
+ /** Source cell is empty, so completing the copy CLEARS the target cells */
+ isClearing: boolean;
}
/**
@@ -162,6 +164,16 @@ export interface PendingCopyDownValidation {
affectedRows: number[];
}
+/**
+ * Tracks rows that need their field re-validated after copy-down completes.
+ * Unlike the optimistic error-clearing done inline, this runs the real rule set,
+ * so copying a BLANK value down surfaces "required" errors on the cleared cells.
+ */
+export interface PendingCopyDownRevalidation {
+ fieldKey: string;
+ affectedRows: number[];
+}
+
/**
* Tracks rows that need inline AI validation after line copy-down.
* When line is copied to rows that already have company + name/description,
@@ -390,6 +402,7 @@ export interface ValidationState {
// === Copy-Down Mode ===
copyDownMode: CopyDownState;
pendingCopyDownValidation: PendingCopyDownValidation | null;
+ pendingCopyDownRevalidation: PendingCopyDownRevalidation | null;
pendingInlineAiValidation: PendingInlineAiValidation | null;
// === Dialogs ===
@@ -450,6 +463,10 @@ export interface ValidationActions {
allErrors: Map>,
allStatuses: Map
) => void;
+ applyFieldValidationResults: (
+ fieldKey: string,
+ results: Array<{ rowIndex: number; error: ValidationError | null }>
+ ) => void;
clearRowErrors: (rowIndex: number) => void;
clearFieldError: (rowIndex: number, field: string) => void;
setRowValidationStatus: (rowIndex: number, status: RowValidationStatus) => void;
@@ -496,6 +513,7 @@ export interface ValidationActions {
completeCopyDown: (targetRowIndex: number) => void;
setTargetRowHover: (rowIndex: number | null) => void;
clearPendingCopyDownValidation: () => void;
+ clearPendingCopyDownRevalidation: () => void;
clearPendingInlineAiValidation: () => void;
// === Dialogs ===
diff --git a/inventory/src/components/product-import/steps/ValidationStep/store/validationStore.ts b/inventory/src/components/product-import/steps/ValidationStep/store/validationStore.ts
index 92ac2ed..9e0bb21 100644
--- a/inventory/src/components/product-import/steps/ValidationStep/store/validationStore.ts
+++ b/inventory/src/components/product-import/steps/ValidationStep/store/validationStore.ts
@@ -57,8 +57,16 @@ const initialCopyDownState: CopyDownState = {
sourceRowIndex: null,
sourceFieldKey: null,
targetRowIndex: null,
+ isClearing: false,
};
+/** A cell counts as empty (and so clears its targets) when it has no usable value */
+const isBlankCellValue = (value: unknown): boolean =>
+ value === undefined ||
+ value === null ||
+ (typeof value === 'string' && value.trim() === '') ||
+ (Array.isArray(value) && value.length === 0);
+
// Fields that require UPC validation when changed via copy-down
const UPC_VALIDATION_FIELDS = ['supplier', 'upc', 'barcode'];
@@ -111,6 +119,7 @@ const getInitialState = (): ValidationState => ({
// Copy-Down Mode
copyDownMode: { ...initialCopyDownState },
pendingCopyDownValidation: null,
+ pendingCopyDownRevalidation: null,
pendingInlineAiValidation: null,
// Dialogs
@@ -450,6 +459,46 @@ export const useValidationStore = create()(
});
},
+ /**
+ * PERFORMANCE: Apply one field's validation outcome across many rows in a
+ * SINGLE store update. Used by copy-down, where "Apply to All" can touch
+ * hundreds of rows and per-row setError() calls would re-clone the errors
+ * Map every time.
+ */
+ applyFieldValidationResults: (
+ fieldKey: string,
+ results: Array<{ rowIndex: number; error: ValidationError | null }>
+ ) => {
+ set((state) => {
+ for (const { rowIndex, error } of results) {
+ const rowErrors = state.errors.get(rowIndex);
+
+ if (error) {
+ state.errors.set(rowIndex, { ...(rowErrors ?? {}), [fieldKey]: [error] });
+ state.rowValidationStatus.set(rowIndex, 'error');
+ continue;
+ }
+
+ if (rowErrors && rowErrors[fieldKey]) {
+ const remaining = { ...rowErrors };
+ delete remaining[fieldKey];
+ if (Object.keys(remaining).length === 0) {
+ state.errors.delete(rowIndex);
+ } else {
+ state.errors.set(rowIndex, remaining);
+ }
+ }
+
+ // Only promote rows that were already carrying a status — never mark a
+ // never-validated row "validated" off the back of a single field.
+ const stillHasErrors = Object.keys(state.errors.get(rowIndex) ?? {}).length > 0;
+ if (!stillHasErrors && state.rowValidationStatus.has(rowIndex)) {
+ state.rowValidationStatus.set(rowIndex, 'validated');
+ }
+ }
+ });
+ },
+
clearRowErrors: (rowIndex: number) => {
set((state) => {
state.errors.delete(rowIndex);
@@ -677,6 +726,9 @@ export const useValidationStore = create()(
sourceRowIndex: rowIndex,
sourceFieldKey: fieldKey,
targetRowIndex: null,
+ // Resolved once at start so the banner and range highlight can warn
+ // that this run wipes the target cells rather than filling them
+ isClearing: isBlankCellValue(state.rows[rowIndex]?.[fieldKey]),
};
});
},
@@ -698,8 +750,10 @@ export const useValidationStore = create()(
// First, perform the copy operation
set((state) => {
- const sourceValue = state.rows[sourceRowIndex]?.[fieldKey];
- if (sourceValue === undefined) return;
+ const sourceRow = state.rows[sourceRowIndex];
+ if (!sourceRow) return;
+
+ const sourceValue = sourceRow[fieldKey];
// Clone value for arrays/objects to prevent reference sharing
const cloneValue = (val: unknown): unknown => {
@@ -709,15 +763,23 @@ export const useValidationStore = create()(
};
// Check if value is non-empty (for clearing required errors)
- const hasValue = sourceValue !== null && sourceValue !== '' &&
- !(Array.isArray(sourceValue) && sourceValue.length === 0);
+ const hasValue = !isBlankCellValue(sourceValue);
+
+ // Copying a BLANK source clears the target cells. "Blank" arrives in several
+ // shapes here (undefined for an unmapped column, null, '', []), and writing
+ // undefined/null back would leave the target cell uncontrolled — so write the
+ // canonical empty value for whatever shape the target already holds.
+ const valueForTarget = (targetValue: unknown): unknown => {
+ if (hasValue) return cloneValue(sourceValue);
+ return Array.isArray(targetValue) ? [] : '';
+ };
// Track affected rows for UPC validation
const affectedRows: number[] = [];
for (let i = sourceRowIndex + 1; i <= targetRowIndex; i++) {
if (state.rows[i]) {
- state.rows[i][fieldKey] = cloneValue(sourceValue);
+ state.rows[i][fieldKey] = valueForTarget(state.rows[i][fieldKey]);
affectedRows.push(i);
// Clear validation errors for this field if value is non-empty
@@ -739,6 +801,29 @@ export const useValidationStore = create()(
// Reset copy-down mode
state.copyDownMode = { ...initialCopyDownState };
+ // Re-run the field's validation rules on every touched row. This is what
+ // makes a blank copy-down behave like manually clearing each cell:
+ // "required" errors appear instead of the cells looking silently valid.
+ if (affectedRows.length > 0) {
+ state.pendingCopyDownRevalidation = { fieldKey, affectedRows };
+ }
+
+ // A cleared name/description invalidates any AI suggestion built from the
+ // old text, so drop those suggestions rather than leave them stranded.
+ if (!hasValue && (fieldKey === 'name' || fieldKey === 'description')) {
+ const field = fieldKey as 'name' | 'description';
+ for (const rowIdx of affectedRows) {
+ const productIndex = state.rows[rowIdx]?.__index;
+ if (!productIndex) continue;
+ const existing = state.inlineAi.suggestions.get(productIndex);
+ if (!existing?.[field]) continue;
+ state.inlineAi.suggestions.set(productIndex, {
+ ...existing,
+ [field]: undefined,
+ });
+ }
+ }
+
// If this field affects UPC validation, store the affected rows
// so a hook can trigger validation using the existing validateUpc function
if (UPC_VALIDATION_FIELDS.includes(fieldKey) && affectedRows.length > 0) {
@@ -801,6 +886,12 @@ export const useValidationStore = create()(
});
},
+ clearPendingCopyDownRevalidation: () => {
+ set((state) => {
+ state.pendingCopyDownRevalidation = null;
+ });
+ },
+
clearPendingInlineAiValidation: () => {
set((state) => {
state.pendingInlineAiValidation = null;
diff --git a/inventory/src/utils/businessTime.ts b/inventory/src/utils/businessTime.ts
index 22eb269..b5791a6 100644
--- a/inventory/src/utils/businessTime.ts
+++ b/inventory/src/utils/businessTime.ts
@@ -31,3 +31,45 @@ export function toDateOnly(date: Date): string {
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
+
+/**
+ * Parse a server-supplied business date for DISPLAY, without shifting a day.
+ *
+ * The APIs hand back business days as bare 'YYYY-MM-DD' (pg via formatDateCol,
+ * mysql2 via dateStrings) — and `new Date('2026-07-28')` is spec'd to parse as
+ * UTC midnight, which renders as Jul 27 in any zone west of UTC. Appending a
+ * time part makes the same string parse as LOCAL midnight, so the calendar date
+ * round-trips through toLocaleDateString().
+ *
+ * Midnight-Z timestamps ('2026-07-28T00:00:00.000Z') stand for a whole business
+ * day too, so they get the same treatment. Anything else is a real instant and
+ * is parsed as-is.
+ */
+export function parseBusinessDay(value: string | number | Date | null | undefined): Date | null {
+ if (value == null) return null;
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
+ if (typeof value === 'number') return new Date(value);
+
+ const trimmed = value.trim();
+ if (!trimmed) return null;
+
+ // Bare business date, or a midnight-Z timestamp standing in for one.
+ const dayOnly = /^(\d{4}-\d{2}-\d{2})(?:T00:00:00(?:\.000)?Z)?$/.exec(trimmed);
+ if (dayOnly) {
+ const parsed = new Date(`${dayOnly[1]}T00:00:00`);
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
+ }
+
+ const parsed = new Date(trimmed);
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
+}
+
+/** Format a server-supplied business day for display; '' when unparseable. */
+export function formatBusinessDay(
+ value: string | number | Date | null | undefined,
+ options: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' },
+ locales: Intl.LocalesArgument = 'en-US'
+): string {
+ const parsed = parseBusinessDay(value);
+ return parsed ? parsed.toLocaleDateString(locales, options) : '';
+}