@bsol-oss/react-datatable5 13.0.1-beta.7 → 13.0.1-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import { Button as Button$1, AbsoluteCenter, Spinner, Span, IconButton, Portal, Dialog, Flex, Text, useDisclosure, DialogBackdrop, RadioGroup as RadioGroup$1, Grid, Box, Slider as Slider$1, HStack, For, CheckboxCard as CheckboxCard$1, Input, Menu, createRecipeContext, createContext as createContext$1, Pagination as Pagination$1, usePaginationContext, Tooltip as Tooltip$1, Group, InputElement, Icon, EmptyState as EmptyState$2, VStack, List, Table as Table$1, Checkbox as Checkbox$1, Card, MenuRoot as MenuRoot$1, MenuTrigger as MenuTrigger$1, Clipboard, Badge, Link, Tag as Tag$1, Image, Alert, Field as Field$1, Popover, useFilter, useListCollection, Combobox, Tabs, Skeleton, NumberInput, Show, RadioCard, CheckboxGroup, InputGroup as InputGroup$1, Center, Heading, Stack } from '@chakra-ui/react';
2
+ import { Button as Button$1, AbsoluteCenter, Spinner, Span, IconButton, Portal, Dialog, Flex, Text, useDisclosure, DialogBackdrop, RadioGroup as RadioGroup$1, Grid, Box, Slider as Slider$1, HStack, For, CheckboxCard as CheckboxCard$1, Input, Menu, createRecipeContext, createContext as createContext$1, Pagination as Pagination$1, usePaginationContext, Tooltip as Tooltip$1, Group, InputElement, Icon, EmptyState as EmptyState$2, VStack, List, Table as Table$1, Checkbox as Checkbox$1, Card, MenuRoot as MenuRoot$1, MenuTrigger as MenuTrigger$1, Clipboard, Badge, Link, Tag as Tag$1, Image, Alert, Field as Field$1, Popover, useFilter, useListCollection, Combobox, Tabs, useCombobox, Show, Skeleton, NumberInput, RadioCard, CheckboxGroup, InputGroup as InputGroup$1, Select, Center, Heading, Stack } from '@chakra-ui/react';
3
3
  import { AiOutlineColumnWidth } from 'react-icons/ai';
4
4
  import * as React from 'react';
5
5
  import { createContext, useContext, useState, useMemo, useCallback, useEffect, useRef, forwardRef } from 'react';
6
6
  import { LuX, LuCheck, LuChevronRight, LuCopy, LuExternalLink, LuSearch, LuImage, LuFile } from 'react-icons/lu';
7
7
  import { MdOutlineSort, MdFilterAlt, MdSearch, MdOutlineChecklist, MdClear, MdOutlineViewColumn, MdFilterListAlt, MdPushPin, MdCancel, MdDateRange } from 'react-icons/md';
8
- import { FaUpDown, FaGripLinesVertical, FaTrash } from 'react-icons/fa6';
9
- import { BiDownArrow, BiUpArrow, BiError } from 'react-icons/bi';
8
+ import { FaUpDown, FaGripLinesVertical } from 'react-icons/fa6';
9
+ import { BiDownArrow, BiUpArrow, BiX, BiError } from 'react-icons/bi';
10
10
  import { CgClose, CgTrash } from 'react-icons/cg';
11
11
  import { HiMiniEllipsisHorizontal, HiChevronLeft, HiChevronRight } from 'react-icons/hi2';
12
12
  import { IoMdEye, IoMdCheckbox, IoMdClock } from 'react-icons/io';
@@ -28,9 +28,9 @@ import { FormProvider, useFormContext, useForm as useForm$1 } from 'react-hook-f
28
28
  import Ajv from 'ajv';
29
29
  import addFormats from 'ajv-formats';
30
30
  import dayjs from 'dayjs';
31
- import utc from 'dayjs/plugin/utc';
32
- import timezone from 'dayjs/plugin/timezone';
33
31
  import customParseFormat from 'dayjs/plugin/customParseFormat';
32
+ import timezone from 'dayjs/plugin/timezone';
33
+ import utc from 'dayjs/plugin/utc';
34
34
  import { TiDeleteOutline } from 'react-icons/ti';
35
35
  import { rankItem } from '@tanstack/match-sorter-utils';
36
36
 
@@ -4660,7 +4660,7 @@ function DatePickerInput({ value, onChange, placeholder = 'Select a date', dateF
4660
4660
  }, timezone = 'Asia/Hong_Kong', minDate, maxDate, firstDayOfWeek, showOutsideDays, monthsToDisplay = 1, insideDialog = false, readOnly = false, showHelperButtons = true, }) {
4661
4661
  const [open, setOpen] = useState(false);
4662
4662
  const [inputValue, setInputValue] = useState('');
4663
- // Update input value when prop value changes
4663
+ // Sync inputValue with value prop changes
4664
4664
  useEffect(() => {
4665
4665
  if (value) {
4666
4666
  const formatted = typeof value === 'string'
@@ -4673,7 +4673,7 @@ function DatePickerInput({ value, onChange, placeholder = 'Select a date', dateF
4673
4673
  else {
4674
4674
  setInputValue('');
4675
4675
  }
4676
- }, [value, displayFormat, timezone]);
4676
+ }, [value, timezone, displayFormat]);
4677
4677
  // Convert value to Date object for DatePicker
4678
4678
  const selectedDate = value
4679
4679
  ? typeof value === 'string'
@@ -4755,7 +4755,14 @@ function DatePickerInput({ value, onChange, placeholder = 'Select a date', dateF
4755
4755
  }
4756
4756
  };
4757
4757
  const handleDateSelected = ({ date }) => {
4758
+ console.debug('[DatePickerInput] handleDateSelected called:', {
4759
+ date: date.toISOString(),
4760
+ timezone,
4761
+ dateFormat,
4762
+ formattedDate: dayjs(date).tz(timezone).format(dateFormat),
4763
+ });
4758
4764
  const formattedDate = dayjs(date).tz(timezone).format(dateFormat);
4765
+ console.debug('[DatePickerInput] Calling onChange with formatted date:', formattedDate);
4759
4766
  onChange?.(formattedDate);
4760
4767
  setOpen(false);
4761
4768
  };
@@ -5069,7 +5076,7 @@ const EnumPicker = ({ column, isMultiple = false, schema, prefix, showTotalAndLi
5069
5076
  const watchEnum = watch(colLabel);
5070
5077
  const watchEnums = (watch(colLabel) ?? []);
5071
5078
  const dataList = schema.enum ?? [];
5072
- // Helper function to render enum value
5079
+ // Helper function to render enum value (returns ReactNode)
5073
5080
  // If renderDisplay is provided, use it; otherwise show the enum string value directly
5074
5081
  const renderEnumValue = (value) => {
5075
5082
  if (renderDisplay) {
@@ -5078,6 +5085,35 @@ const EnumPicker = ({ column, isMultiple = false, schema, prefix, showTotalAndLi
5078
5085
  // If no renderDisplay provided, show the enum string value directly
5079
5086
  return value;
5080
5087
  };
5088
+ // Helper function to get string representation for input display
5089
+ // Converts ReactNode to string for combobox input display
5090
+ const getDisplayString = (value) => {
5091
+ if (renderDisplay) {
5092
+ const rendered = renderDisplay(value);
5093
+ // If renderDisplay returns a string, use it directly
5094
+ if (typeof rendered === 'string') {
5095
+ return rendered;
5096
+ }
5097
+ // If it's a React element, try to extract text content
5098
+ // For now, fallback to the raw value if we can't extract text
5099
+ // In most cases, renderDisplay should return a string or simple element
5100
+ if (typeof rendered === 'object' &&
5101
+ rendered !== null &&
5102
+ 'props' in rendered) {
5103
+ const props = rendered.props;
5104
+ // Try to extract text from React element props
5105
+ if (props?.children) {
5106
+ const children = props.children;
5107
+ if (typeof children === 'string') {
5108
+ return children;
5109
+ }
5110
+ }
5111
+ }
5112
+ // Fallback: use raw value if we can't extract string
5113
+ return value;
5114
+ }
5115
+ return value;
5116
+ };
5081
5117
  // Debug log when renderDisplay is missing
5082
5118
  if (!renderDisplay) {
5083
5119
  console.debug(`[EnumPicker] Missing renderDisplay for field '${colLabel}'. Add renderDisplay function to schema for field '${colLabel}' to provide custom UI rendering. Currently showing enum string values directly.`, {
@@ -5093,19 +5129,29 @@ const EnumPicker = ({ column, isMultiple = false, schema, prefix, showTotalAndLi
5093
5129
  : watchEnum
5094
5130
  ? [watchEnum]
5095
5131
  : [];
5132
+ // Track input focus state for single selection
5133
+ const [isInputFocused, setIsInputFocused] = useState(false);
5134
+ // Get the selected value for single selection display
5135
+ const selectedSingleValue = !isMultiple && watchEnum ? watchEnum : null;
5136
+ const selectedSingleRendered = selectedSingleValue
5137
+ ? renderEnumValue(selectedSingleValue)
5138
+ : null;
5139
+ const isSelectedSingleValueString = typeof selectedSingleRendered === 'string';
5096
5140
  const comboboxItems = useMemo(() => {
5097
5141
  return dataList.map((item) => ({
5098
5142
  label: item, // Internal: used for search/filtering only
5099
5143
  value: item,
5100
5144
  raw: item, // Passed to renderEnumValue for UI rendering
5145
+ displayLabel: getDisplayString(item), // Used for input display when selected
5101
5146
  }));
5102
- }, [dataList]);
5147
+ }, [dataList, renderDisplay]);
5103
5148
  // Use filter hook for combobox
5104
5149
  const { contains } = useFilter({ sensitivity: 'base' });
5105
5150
  // Create collection for combobox
5151
+ // itemToString uses displayLabel to show rendered display in input when selected
5106
5152
  const { collection, filter } = useListCollection({
5107
5153
  initialItems: comboboxItems,
5108
- itemToString: (item) => item.label,
5154
+ itemToString: (item) => item.displayLabel, // Use displayLabel for selected value display
5109
5155
  itemToValue: (item) => item.value,
5110
5156
  filter: contains,
5111
5157
  });
@@ -5143,7 +5189,26 @@ const EnumPicker = ({ column, isMultiple = false, schema, prefix, showTotalAndLi
5143
5189
  }, children: renderEnumValue(enumValue) }, enumValue));
5144
5190
  }) })), jsxs(Combobox.Root, { collection: collection, value: currentValue, onValueChange: handleValueChange, onInputValueChange: handleInputValueChange, multiple: isMultiple, closeOnSelect: !isMultiple, openOnClick: true, invalid: !!errors[colLabel], width: "100%", positioning: insideDialog
5145
5191
  ? { strategy: 'fixed', hideWhenDetached: true }
5146
- : undefined, children: [jsxs(Combobox.Control, { children: [jsx(Combobox.Input, { placeholder: enumPickerLabels?.typeToSearch ?? 'Type to search' }), jsxs(Combobox.IndicatorGroup, { children: [!isMultiple && currentValue.length > 0 && (jsx(Combobox.ClearTrigger, { onClick: () => {
5192
+ : undefined, children: [jsxs(Combobox.Control, { position: "relative", children: [!isMultiple &&
5193
+ selectedSingleValue &&
5194
+ !isInputFocused &&
5195
+ !isSelectedSingleValueString &&
5196
+ selectedSingleRendered && (jsx(Box, { position: "absolute", left: 3, top: "50%", transform: "translateY(-50%)", pointerEvents: "none", zIndex: 1, maxWidth: "calc(100% - 60px)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", children: selectedSingleRendered })), jsx(Combobox.Input, { placeholder: !isMultiple && selectedSingleValue && !isInputFocused
5197
+ ? undefined
5198
+ : enumPickerLabels?.typeToSearch ?? 'Type to search', onFocus: () => setIsInputFocused(true), onBlur: () => setIsInputFocused(false), style: {
5199
+ color: !isMultiple &&
5200
+ selectedSingleValue &&
5201
+ !isInputFocused &&
5202
+ !isSelectedSingleValueString
5203
+ ? 'transparent'
5204
+ : undefined,
5205
+ caretColor: !isMultiple &&
5206
+ selectedSingleValue &&
5207
+ !isInputFocused &&
5208
+ !isSelectedSingleValueString
5209
+ ? 'transparent'
5210
+ : undefined,
5211
+ } }), jsxs(Combobox.IndicatorGroup, { children: [!isMultiple && currentValue.length > 0 && (jsx(Combobox.ClearTrigger, { onClick: () => {
5147
5212
  setValue(colLabel, '');
5148
5213
  } })), jsx(Combobox.Trigger, {})] })] }), insideDialog ? (jsx(Combobox.Positioner, { children: jsxs(Combobox.Content, { children: [showTotalAndLimit && (jsx(Text, { p: 2, fontSize: "sm", color: "fg.muted", children: `${enumPickerLabels?.total ?? 'Total'}: ${collection.items.length}` })), collection.items.length === 0 ? (jsx(Combobox.Empty, { children: enumPickerLabels?.emptySearchResult ?? 'No results found' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.ItemText, { children: renderEnumValue(item.raw) }), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) }))] }) })) : (jsx(Portal, { children: jsx(Combobox.Positioner, { children: jsxs(Combobox.Content, { children: [showTotalAndLimit && (jsx(Text, { p: 2, fontSize: "sm", color: "fg.muted", children: `${enumPickerLabels?.total ?? 'Total'}: ${collection.items.length}` })), collection.items.length === 0 ? (jsx(Combobox.Empty, { children: enumPickerLabels?.emptySearchResult ?? 'No results found' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.ItemText, { children: renderEnumValue(item.raw) }), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) }))] }) }) }))] })] }));
5149
5214
  };
@@ -5887,15 +5952,46 @@ const FormMediaLibraryBrowser = ({ column, schema, prefix, }) => {
5887
5952
  }) })] }));
5888
5953
  };
5889
5954
 
5890
- // Default renderDisplay function that stringifies JSON
5955
+ // Default renderDisplay function that intelligently displays items
5956
+ // If item is an object, tries to find common display fields (name, title, label, etc.)
5957
+ // Otherwise falls back to JSON.stringify
5891
5958
  const defaultRenderDisplay = (item) => {
5959
+ // Check if item is an object (not null, not array, not primitive)
5960
+ if (item !== null &&
5961
+ typeof item === 'object' &&
5962
+ !Array.isArray(item) &&
5963
+ !(item instanceof Date)) {
5964
+ const obj = item;
5965
+ // Try common display fields in order of preference
5966
+ const displayFields = [
5967
+ 'name',
5968
+ 'title',
5969
+ 'label',
5970
+ 'displayName',
5971
+ 'display_name',
5972
+ 'text',
5973
+ 'value',
5974
+ ];
5975
+ for (const field of displayFields) {
5976
+ if (obj[field] !== undefined && obj[field] !== null) {
5977
+ const value = obj[field];
5978
+ // Return the value if it's a string or number, otherwise stringify it
5979
+ if (typeof value === 'string' || typeof value === 'number') {
5980
+ return String(value);
5981
+ }
5982
+ }
5983
+ }
5984
+ // If no display field found, fall back to JSON.stringify
5985
+ return JSON.stringify(item);
5986
+ }
5987
+ // For non-objects (primitives, arrays, dates), use JSON.stringify
5892
5988
  return JSON.stringify(item);
5893
5989
  };
5894
5990
 
5895
5991
  const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
5896
5992
  const { watch, getValues, formState: { errors }, setValue, } = useFormContext();
5897
5993
  const { idMap, setIdMap, idPickerLabels, insideDialog } = useSchemaContext();
5898
- const { renderDisplay, loadInitialValues, foreign_key, variant } = schema;
5994
+ const { renderDisplay, itemToValue: schemaItemToValue, loadInitialValues, foreign_key, variant, } = schema;
5899
5995
  // loadInitialValues should be provided in schema for id-picker fields
5900
5996
  // It's used to load the record of the id so the display is human-readable
5901
5997
  if (variant === 'id-picker' && !loadInitialValues) {
@@ -6028,17 +6124,51 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
6028
6124
  // Depend on idMapKey which only changes when items we care about change
6029
6125
  // eslint-disable-next-line react-hooks/exhaustive-deps
6030
6126
  }, [currentValueKey, idMapKey]);
6127
+ // Default itemToValue function: extract value from item using column_ref
6128
+ const defaultItemToValue = (item) => String(item[column_ref]);
6129
+ // Use schema's itemToValue if provided, otherwise use default
6130
+ const itemToValueFn = schemaItemToValue
6131
+ ? (item) => schemaItemToValue(item)
6132
+ : defaultItemToValue;
6133
+ // itemToString function: convert item to readable string using renderDisplay
6134
+ // This ensures items can always be displayed as readable strings in the combobox
6135
+ const renderFn = renderDisplay || defaultRenderDisplay;
6136
+ const itemToStringFn = (item) => {
6137
+ const rendered = renderFn(item);
6138
+ // If already a string or number, return it
6139
+ if (typeof rendered === 'string')
6140
+ return rendered;
6141
+ if (typeof rendered === 'number')
6142
+ return String(rendered);
6143
+ // For ReactNode, fall back to defaultRenderDisplay which converts to string
6144
+ return String(defaultRenderDisplay(item));
6145
+ };
6031
6146
  // Transform data for combobox collection
6032
6147
  // label is used for filtering/searching (must be a string)
6148
+ // displayLabel is used for input display when selected (string representation of rendered display)
6033
6149
  // raw item is stored for custom rendering
6034
6150
  // Also include items from idMap that match currentValue (for initial values display)
6035
6151
  const comboboxItems = useMemo(() => {
6036
6152
  const renderFn = renderDisplay || defaultRenderDisplay;
6153
+ // Helper to convert rendered display to string for displayLabel
6154
+ // For ReactNodes (non-string/number), we can't safely stringify due to circular refs
6155
+ // So we use the label (which is already a string) as fallback
6156
+ const getDisplayString = (rendered, fallbackLabel) => {
6157
+ if (typeof rendered === 'string')
6158
+ return rendered;
6159
+ if (typeof rendered === 'number')
6160
+ return String(rendered);
6161
+ // For ReactNode, use the fallback label (which is already a string representation)
6162
+ // The actual ReactNode will be rendered in the overlay, not in the input
6163
+ return fallbackLabel;
6164
+ };
6037
6165
  const itemsFromDataList = dataList.map((item) => {
6038
6166
  const rendered = renderFn(item);
6167
+ const label = typeof rendered === 'string' ? rendered : JSON.stringify(item); // Use string for filtering
6039
6168
  return {
6040
- label: typeof rendered === 'string' ? rendered : JSON.stringify(item), // Use string for filtering
6041
- value: String(item[column_ref]),
6169
+ label, // Use string for filtering
6170
+ displayLabel: getDisplayString(rendered, label), // String representation for input display
6171
+ value: itemToValueFn(item),
6042
6172
  raw: item,
6043
6173
  };
6044
6174
  });
@@ -6047,25 +6177,28 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
6047
6177
  const itemsFromIdMap = idMapItems
6048
6178
  .map((item) => {
6049
6179
  // Check if this item is already in itemsFromDataList
6050
- const alreadyIncluded = itemsFromDataList.some((i) => i.value === String(item[column_ref]));
6180
+ const alreadyIncluded = itemsFromDataList.some((i) => i.value === itemToValueFn(item));
6051
6181
  if (alreadyIncluded)
6052
6182
  return null;
6053
6183
  const rendered = renderFn(item);
6184
+ const label = typeof rendered === 'string' ? rendered : JSON.stringify(item);
6054
6185
  return {
6055
- label: typeof rendered === 'string' ? rendered : JSON.stringify(item),
6056
- value: String(item[column_ref]),
6186
+ label,
6187
+ displayLabel: getDisplayString(rendered, label), // String representation for input display
6188
+ value: itemToValueFn(item),
6057
6189
  raw: item,
6058
6190
  };
6059
6191
  })
6060
6192
  .filter((item) => item !== null);
6061
6193
  return [...itemsFromIdMap, ...itemsFromDataList];
6062
- }, [dataList, column_ref, renderDisplay, idMapItems]);
6194
+ }, [dataList, column_ref, renderDisplay, idMapItems, itemToValueFn]);
6063
6195
  // Use filter hook for combobox
6064
6196
  const { contains } = useFilter({ sensitivity: 'base' });
6065
6197
  // Create collection for combobox
6198
+ // itemToString uses displayLabel to show rendered display in input when selected
6066
6199
  const { collection, filter, set } = useListCollection({
6067
6200
  initialItems: comboboxItems,
6068
- itemToString: (item) => item.label,
6201
+ itemToString: (item) => item.displayLabel, // Use displayLabel for selected value display
6069
6202
  itemToValue: (item) => item.value,
6070
6203
  filter: contains,
6071
6204
  });
@@ -6120,6 +6253,8 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
6120
6253
  idPickerLabels,
6121
6254
  insideDialog: insideDialog ?? false,
6122
6255
  renderDisplay,
6256
+ itemToValue: itemToValueFn,
6257
+ itemToString: itemToStringFn,
6123
6258
  loadInitialValues: loadInitialValues ??
6124
6259
  (async () => ({ data: { data: [], count: 0 }, idMap: {} })), // Fallback if not provided
6125
6260
  column_ref,
@@ -6130,60 +6265,69 @@ const useIdPickerData = ({ column, schema, prefix, isMultiple, }) => {
6130
6265
 
6131
6266
  const IdPickerSingle = ({ column, schema, prefix, }) => {
6132
6267
  const formI18n = useFormI18n(column, prefix, schema);
6133
- const { required, gridColumn = 'span 12', gridRow = 'span 1', renderDisplay, } = schema;
6268
+ const { required, gridColumn = 'span 12', gridRow = 'span 1' } = schema;
6134
6269
  const isRequired = required?.some((columnId) => columnId === column);
6135
- const { colLabel, currentValue, searchText, setSearchText, isLoading, isFetching, isPending, isError, isSearching, isLoadingInitialValues, isFetchingInitialValues, missingIds, collection, idMap, idPickerLabels, insideDialog, renderDisplay: renderDisplayFn, errors, setValue, } = useIdPickerData({
6270
+ const { colLabel, currentValue, searchText, setSearchText, isLoading, isFetching, isPending, isError, isSearching, collection, filter, idMap, idPickerLabels, insideDialog, renderDisplay: renderDisplayFn, itemToValue, itemToString, errors, setValue, } = useIdPickerData({
6136
6271
  column,
6137
6272
  schema,
6138
6273
  prefix,
6139
6274
  isMultiple: false,
6140
6275
  });
6141
- const handleInputValueChange = (details) => {
6142
- setSearchText(details.inputValue);
6143
- };
6144
- const handleValueChange = (details) => {
6145
- setValue(colLabel, details.value[0] || '');
6146
- };
6147
- const renderDisplayFunction = renderDisplayFn || renderDisplay || defaultRenderDisplay;
6148
- return (jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
6149
- gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: [currentValue.length > 0 && (jsx(Flex, { mb: 2, children: (() => {
6150
- const id = currentValue[0];
6151
- const item = idMap[id];
6152
- // Show loading skeleton while fetching initial values
6153
- if (item === undefined &&
6154
- (isLoadingInitialValues || isFetchingInitialValues) &&
6155
- missingIds.includes(id)) {
6156
- return jsx(Skeleton, { height: "24px", width: "100px", borderRadius: "md" });
6157
- }
6158
- // Only show "not found" if we're not loading and item is still missing
6159
- if (item === undefined) {
6160
- return (jsx(Text, { fontSize: "sm", children: idPickerLabels?.undefined ?? 'Undefined' }));
6161
- }
6162
- return jsx(Text, { fontSize: "sm", children: renderDisplayFunction(item) });
6163
- })() })), jsxs(Combobox.Root, { collection: collection, value: currentValue, onValueChange: handleValueChange, onInputValueChange: handleInputValueChange, multiple: false, closeOnSelect: true, openOnClick: true, invalid: !!errors[colLabel], width: "100%", positioning: insideDialog
6164
- ? { strategy: 'fixed', hideWhenDetached: true }
6165
- : undefined, children: [jsxs(Combobox.Control, { children: [jsx(Combobox.Input, { placeholder: idPickerLabels?.typeToSearch ?? 'Type to search' }), jsxs(Combobox.IndicatorGroup, { children: [(isFetching || isLoading || isPending) && jsx(Spinner, { size: "xs" }), isError && (jsx(Icon, { color: "fg.error", children: jsx(BiError, {}) })), currentValue.length > 0 && (jsx(Combobox.ClearTrigger, { onClick: () => {
6166
- setValue(colLabel, '');
6167
- } })), jsx(Combobox.Trigger, {})] })] }), insideDialog ? (jsx(Combobox.Positioner, { children: jsx(Combobox.Content, { children: isError ? (jsx(Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
6276
+ // Get the selected value for single selection display
6277
+ const selectedId = currentValue.length > 0 ? currentValue[0] : null;
6278
+ const selectedItem = selectedId
6279
+ ? idMap[selectedId]
6280
+ : undefined;
6281
+ // Use itemToValue to get the combobox value from the selected item, or use the ID directly
6282
+ const comboboxValue = selectedItem
6283
+ ? itemToString(selectedItem)
6284
+ : selectedId || '';
6285
+ // itemToString is available from the hook and can be used to get a readable string
6286
+ // representation of any item. The collection's itemToString is automatically used
6287
+ // by the combobox to display selected values.
6288
+ // Use useCombobox hook to control input value
6289
+ const combobox = useCombobox({
6290
+ collection,
6291
+ value: [comboboxValue],
6292
+ onInputValueChange(e) {
6293
+ setSearchText(e.inputValue);
6294
+ filter(e.inputValue);
6295
+ },
6296
+ onValueChange(e) {
6297
+ setValue(colLabel, e.value[0] || '');
6298
+ // Clear the input value after selection
6299
+ setSearchText('');
6300
+ },
6301
+ multiple: false,
6302
+ closeOnSelect: true,
6303
+ openOnClick: true,
6304
+ invalid: !!errors[colLabel],
6305
+ });
6306
+ // Use renderDisplay from hook (which comes from schema) or fallback to default
6307
+ const renderDisplayFunction = renderDisplayFn || defaultRenderDisplay;
6308
+ // Get the selected value for single selection display (already computed above)
6309
+ const selectedRendered = selectedItem
6310
+ ? renderDisplayFunction(selectedItem)
6311
+ : null;
6312
+ return (jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
6313
+ gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: jsxs(Combobox.RootProvider, { value: combobox, width: "100%", children: [jsx(Show, { when: selectedId && selectedRendered, children: jsxs(HStack, { justifyContent: 'space-between', children: [jsx(Box, { children: selectedRendered }), currentValue.length > 0 && (jsx(Button$1, { variant: "ghost", size: "sm", onClick: () => {
6314
+ setValue(colLabel, '');
6315
+ }, children: jsx(Icon, { children: jsx(BiX, {}) }) }))] }) }), jsx(Show, { when: !selectedId || !selectedRendered, children: jsxs(Combobox.Control, { position: "relative", children: [jsx(Combobox.Input, { placeholder: idPickerLabels?.typeToSearch ?? 'Type to search' }), jsxs(Combobox.IndicatorGroup, { children: [(isFetching || isLoading || isPending) && jsx(Spinner, { size: "xs" }), isError && (jsx(Icon, { color: "fg.error", children: jsx(BiError, {}) })), jsx(Combobox.Trigger, {})] })] }) }), insideDialog ? (jsx(Combobox.Positioner, { children: jsx(Combobox.Content, { children: isError ? (jsx(Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
6316
+ // Show skeleton items to prevent UI shift
6317
+ jsx(Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsx(Flex, { p: 2, align: "center", gap: 2, children: jsx(Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsx(Combobox.Empty, { children: searchText
6318
+ ? idPickerLabels?.emptySearchResult ?? 'No results found'
6319
+ : idPickerLabels?.initialResults ??
6320
+ 'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [renderDisplayFunction(item.raw), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) })) : (jsx(Portal, { children: jsx(Combobox.Positioner, { children: jsx(Combobox.Content, { children: isError ? (jsx(Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
6168
6321
  // Show skeleton items to prevent UI shift
6169
6322
  jsx(Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsx(Flex, { p: 2, align: "center", gap: 2, children: jsx(Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsx(Combobox.Empty, { children: searchText
6170
6323
  ? idPickerLabels?.emptySearchResult ?? 'No results found'
6171
6324
  : idPickerLabels?.initialResults ??
6172
- 'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.ItemText, { children: !!renderDisplayFunction === true
6173
- ? renderDisplayFunction(item.raw)
6174
- : item.label }), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) })) : (jsx(Portal, { children: jsx(Combobox.Positioner, { children: jsx(Combobox.Content, { children: isError ? (jsx(Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
6175
- // Show skeleton items to prevent UI shift
6176
- jsx(Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsx(Flex, { p: 2, align: "center", gap: 2, children: jsx(Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsx(Combobox.Empty, { children: searchText
6177
- ? idPickerLabels?.emptySearchResult ?? 'No results found'
6178
- : idPickerLabels?.initialResults ??
6179
- 'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.ItemText, { children: !!renderDisplayFunction === true
6180
- ? renderDisplayFunction(item.raw)
6181
- : item.label }), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) }) }))] })] }));
6325
+ 'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsx(Combobox.Item, { item: item, children: renderDisplayFunction(item.raw) }, item.value ?? `item-${index}`))) })) }) }) }))] }) }));
6182
6326
  };
6183
6327
 
6184
6328
  const IdPickerMultiple = ({ column, schema, prefix, }) => {
6185
6329
  const formI18n = useFormI18n(column, prefix, schema);
6186
- const { required, gridColumn = 'span 12', gridRow = 'span 1', renderDisplay, } = schema;
6330
+ const { required, gridColumn = 'span 12', gridRow = 'span 1' } = schema;
6187
6331
  const isRequired = required?.some((columnId) => columnId === column);
6188
6332
  const { colLabel, currentValue, searchText, setSearchText, isLoading, isFetching, isPending, isError, isSearching, isLoadingInitialValues, isFetchingInitialValues, missingIds, collection, idMap, idPickerLabels, insideDialog, renderDisplay: renderDisplayFn, errors, setValue, } = useIdPickerData({
6189
6333
  column,
@@ -6197,7 +6341,8 @@ const IdPickerMultiple = ({ column, schema, prefix, }) => {
6197
6341
  const handleValueChange = (details) => {
6198
6342
  setValue(colLabel, details.value);
6199
6343
  };
6200
- const renderDisplayFunction = renderDisplayFn || renderDisplay || defaultRenderDisplay;
6344
+ // Use renderDisplay from hook (which comes from schema) or fallback to default
6345
+ const renderDisplayFunction = renderDisplayFn || defaultRenderDisplay;
6201
6346
  return (jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
6202
6347
  gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: [currentValue.length > 0 && (jsx(Flex, { flexFlow: 'wrap', gap: 1, mb: 2, children: currentValue.map((id) => {
6203
6348
  const item = idMap[id];
@@ -6222,16 +6367,12 @@ const IdPickerMultiple = ({ column, schema, prefix, }) => {
6222
6367
  jsx(Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsx(Flex, { p: 2, align: "center", gap: 2, children: jsx(Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsx(Combobox.Empty, { children: searchText
6223
6368
  ? idPickerLabels?.emptySearchResult ?? 'No results found'
6224
6369
  : idPickerLabels?.initialResults ??
6225
- 'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.ItemText, { children: !!renderDisplayFunction === true
6226
- ? renderDisplayFunction(item.raw)
6227
- : item.label }), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) })) : (jsx(Portal, { children: jsx(Combobox.Positioner, { children: jsx(Combobox.Content, { children: isError ? (jsx(Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
6370
+ 'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [renderDisplayFunction(item.raw), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) })) : (jsx(Portal, { children: jsx(Combobox.Positioner, { children: jsx(Combobox.Content, { children: isError ? (jsx(Text, { p: 2, color: "fg.error", fontSize: "sm", children: idPickerLabels?.emptySearchResult ?? 'Loading failed' })) : isFetching || isLoading || isPending || isSearching ? (
6228
6371
  // Show skeleton items to prevent UI shift
6229
6372
  jsx(Fragment, { children: Array.from({ length: 5 }).map((_, index) => (jsx(Flex, { p: 2, align: "center", gap: 2, children: jsx(Skeleton, { height: "20px", flex: "1" }) }, `skeleton-${index}`))) })) : collection.items.length === 0 ? (jsx(Combobox.Empty, { children: searchText
6230
6373
  ? idPickerLabels?.emptySearchResult ?? 'No results found'
6231
6374
  : idPickerLabels?.initialResults ??
6232
- 'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [jsx(Combobox.ItemText, { children: !!renderDisplayFunction === true
6233
- ? renderDisplayFunction(item.raw)
6234
- : item.label }), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) }) }))] })] }));
6375
+ 'Start typing to search' })) : (jsx(Fragment, { children: collection.items.map((item, index) => (jsxs(Combobox.Item, { item: item, children: [renderDisplayFunction(item.raw), jsx(Combobox.ItemIndicator, {})] }, item.value ?? `item-${index}`))) })) }) }) }))] })] }));
6235
6376
  };
6236
6377
 
6237
6378
  const NumberInputRoot = React.forwardRef(function NumberInput$1(props, ref) {
@@ -6608,11 +6749,193 @@ const TextAreaInput = ({ column, schema, prefix, }) => {
6608
6749
 
6609
6750
  dayjs.extend(utc);
6610
6751
  dayjs.extend(timezone);
6611
- const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem, onChange = () => { }, startTime, selectedDate, timezone = 'Asia/Hong_Kong', portalled = true, labels = {
6612
- placeholder: 'hh:mm AM/PM',
6613
- emptyMessage: 'No time found',
6614
- }, }) => {
6615
- // Generate time options (every 15 minutes in 12-hour format)
6752
+ const TimePicker$1 = (props) => {
6753
+ const { format = '12h', value: controlledValue, onChange: controlledOnChange, hour: uncontrolledHour, setHour: uncontrolledSetHour, minute: uncontrolledMinute, setMinute: uncontrolledSetMinute, startTime, selectedDate, timezone = 'Asia/Hong_Kong', portalled = true, labels = {
6754
+ placeholder: format === '24h' ? 'HH:mm:ss' : 'hh:mm AM/PM',
6755
+ emptyMessage: 'No time found',
6756
+ }, onTimeChange, } = props;
6757
+ const is24Hour = format === '24h';
6758
+ const uncontrolledMeridiem = is24Hour ? undefined : props.meridiem;
6759
+ const uncontrolledSetMeridiem = is24Hour ? undefined : props.setMeridiem;
6760
+ const uncontrolledSecond = is24Hour ? props.second : undefined;
6761
+ const uncontrolledSetSecond = is24Hour ? props.setSecond : undefined;
6762
+ // Determine if we're in controlled mode
6763
+ const isControlled = controlledValue !== undefined;
6764
+ // Parse time string to extract hour, minute, second, meridiem
6765
+ const parseTimeString = (timeStr) => {
6766
+ if (!timeStr || !timeStr.trim()) {
6767
+ return { hour: null, minute: null, second: null, meridiem: null };
6768
+ }
6769
+ // Remove timezone suffix if present (e.g., "14:30:00Z" -> "14:30:00")
6770
+ const timeWithoutTz = timeStr.replace(/[Z+-]\d{2}:?\d{2}$/, '').trim();
6771
+ // Try parsing 24-hour format: "HH:mm:ss" or "HH:mm"
6772
+ const time24Pattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/;
6773
+ const match24 = timeWithoutTz.match(time24Pattern);
6774
+ if (match24) {
6775
+ const hour24 = parseInt(match24[1], 10);
6776
+ const minute = parseInt(match24[2], 10);
6777
+ const second = match24[3] ? parseInt(match24[3], 10) : 0;
6778
+ if (hour24 >= 0 &&
6779
+ hour24 <= 23 &&
6780
+ minute >= 0 &&
6781
+ minute <= 59 &&
6782
+ second >= 0 &&
6783
+ second <= 59) {
6784
+ if (is24Hour) {
6785
+ return { hour: hour24, minute, second, meridiem: null };
6786
+ }
6787
+ else {
6788
+ // Convert to 12-hour format
6789
+ let hour12 = hour24;
6790
+ let meridiem;
6791
+ if (hour24 === 0) {
6792
+ hour12 = 12;
6793
+ meridiem = 'am';
6794
+ }
6795
+ else if (hour24 === 12) {
6796
+ hour12 = 12;
6797
+ meridiem = 'pm';
6798
+ }
6799
+ else if (hour24 > 12) {
6800
+ hour12 = hour24 - 12;
6801
+ meridiem = 'pm';
6802
+ }
6803
+ else {
6804
+ hour12 = hour24;
6805
+ meridiem = 'am';
6806
+ }
6807
+ return { hour: hour12, minute, second: null, meridiem };
6808
+ }
6809
+ }
6810
+ }
6811
+ // Try parsing 12-hour format: "hh:mm AM/PM" or "hh:mm:ss AM/PM"
6812
+ const time12Pattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\s*(am|pm|AM|PM)$/i;
6813
+ const match12 = timeWithoutTz.match(time12Pattern);
6814
+ if (match12 && !is24Hour) {
6815
+ const hour12 = parseInt(match12[1], 10);
6816
+ const minute = parseInt(match12[2], 10);
6817
+ const second = match12[3] ? parseInt(match12[3], 10) : null;
6818
+ const meridiem = match12[4].toLowerCase();
6819
+ if (hour12 >= 1 &&
6820
+ hour12 <= 12 &&
6821
+ minute >= 0 &&
6822
+ minute <= 59 &&
6823
+ (second === null || (second >= 0 && second <= 59))) {
6824
+ return { hour: hour12, minute, second, meridiem };
6825
+ }
6826
+ }
6827
+ return { hour: null, minute: null, second: null, meridiem: null };
6828
+ };
6829
+ // Format time values to time string
6830
+ const formatTimeString = (hour, minute, second, meridiem) => {
6831
+ if (hour === null || minute === null) {
6832
+ return undefined;
6833
+ }
6834
+ if (is24Hour) {
6835
+ const h = hour.toString().padStart(2, '0');
6836
+ const m = minute.toString().padStart(2, '0');
6837
+ const s = (second ?? 0).toString().padStart(2, '0');
6838
+ return `${h}:${m}:${s}`;
6839
+ }
6840
+ else {
6841
+ if (meridiem === null) {
6842
+ return undefined;
6843
+ }
6844
+ const h = hour.toString();
6845
+ const m = minute.toString().padStart(2, '0');
6846
+ return `${h}:${m} ${meridiem.toUpperCase()}`;
6847
+ }
6848
+ };
6849
+ // Internal state for controlled mode
6850
+ const [internalHour, setInternalHour] = useState(null);
6851
+ const [internalMinute, setInternalMinute] = useState(null);
6852
+ const [internalSecond, setInternalSecond] = useState(null);
6853
+ const [internalMeridiem, setInternalMeridiem] = useState(null);
6854
+ // Use controlled or uncontrolled values
6855
+ const hour = isControlled ? internalHour : uncontrolledHour ?? null;
6856
+ const minute = isControlled ? internalMinute : uncontrolledMinute ?? null;
6857
+ const second = isControlled ? internalSecond : uncontrolledSecond ?? null;
6858
+ const meridiem = isControlled
6859
+ ? internalMeridiem
6860
+ : uncontrolledMeridiem ?? null;
6861
+ // Setters that work for both modes
6862
+ const setHour = isControlled
6863
+ ? setInternalHour
6864
+ : uncontrolledSetHour || (() => { });
6865
+ const setMinute = isControlled
6866
+ ? setInternalMinute
6867
+ : uncontrolledSetMinute || (() => { });
6868
+ const setSecond = isControlled
6869
+ ? setInternalSecond
6870
+ : uncontrolledSetSecond || (() => { });
6871
+ const setMeridiem = isControlled
6872
+ ? setInternalMeridiem
6873
+ : uncontrolledSetMeridiem || (() => { });
6874
+ // Sync internal state with controlled value prop
6875
+ const prevValueRef = useRef(controlledValue);
6876
+ useEffect(() => {
6877
+ if (!isControlled)
6878
+ return;
6879
+ if (prevValueRef.current === controlledValue) {
6880
+ return;
6881
+ }
6882
+ prevValueRef.current = controlledValue;
6883
+ const parsed = parseTimeString(controlledValue);
6884
+ setInternalHour(parsed.hour);
6885
+ setInternalMinute(parsed.minute);
6886
+ if (is24Hour) {
6887
+ setInternalSecond(parsed.second);
6888
+ }
6889
+ else {
6890
+ setInternalMeridiem(parsed.meridiem);
6891
+ }
6892
+ }, [controlledValue, isControlled, is24Hour]);
6893
+ // Wrapper onChange that calls both controlled and uncontrolled onChange
6894
+ const handleTimeChange = (newHour, newMinute, newSecond, newMeridiem) => {
6895
+ if (isControlled) {
6896
+ const timeString = formatTimeString(newHour, newMinute, newSecond, newMeridiem);
6897
+ controlledOnChange?.(timeString);
6898
+ }
6899
+ else {
6900
+ // Call legacy onTimeChange if provided
6901
+ if (onTimeChange) {
6902
+ if (is24Hour) {
6903
+ const timeChange24h = onTimeChange;
6904
+ timeChange24h({
6905
+ hour: newHour,
6906
+ minute: newMinute,
6907
+ second: newSecond,
6908
+ });
6909
+ }
6910
+ else {
6911
+ const timeChange12h = onTimeChange;
6912
+ timeChange12h({
6913
+ hour: newHour,
6914
+ minute: newMinute,
6915
+ meridiem: newMeridiem,
6916
+ });
6917
+ }
6918
+ }
6919
+ }
6920
+ };
6921
+ const [inputValue, setInputValue] = useState('');
6922
+ // Sync inputValue with current time
6923
+ useEffect(() => {
6924
+ if (is24Hour && second !== undefined) {
6925
+ if (hour !== null && minute !== null && second !== null) {
6926
+ const formatted = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')}`;
6927
+ setInputValue(formatted);
6928
+ }
6929
+ else {
6930
+ setInputValue('');
6931
+ }
6932
+ }
6933
+ else {
6934
+ // 12-hour format - input is managed by combobox
6935
+ setInputValue('');
6936
+ }
6937
+ }, [hour, minute, second, is24Hour]);
6938
+ // Generate time options based on format
6616
6939
  const timeOptions = useMemo(() => {
6617
6940
  const options = [];
6618
6941
  // Get start time for comparison if provided
@@ -6623,32 +6946,25 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
6623
6946
  const selectedDateObj = dayjs(selectedDate).tz(timezone);
6624
6947
  if (startDateObj.isValid() && selectedDateObj.isValid()) {
6625
6948
  startDateTime = startDateObj;
6626
- // Only filter if dates are the same
6627
6949
  shouldFilterByDate =
6628
6950
  startDateObj.format('YYYY-MM-DD') ===
6629
6951
  selectedDateObj.format('YYYY-MM-DD');
6630
6952
  }
6631
6953
  }
6632
- // Generate 12-hour format options (1-12 for hours, AM/PM)
6633
- for (let h = 1; h <= 12; h++) {
6634
- for (let m = 0; m < 60; m += 15) {
6635
- for (const mer of ['am', 'pm']) {
6636
- // Convert 12-hour to 24-hour for comparison
6637
- let hour24 = h;
6638
- if (mer === 'am' && h === 12)
6639
- hour24 = 0;
6640
- else if (mer === 'pm' && h < 12)
6641
- hour24 = h + 12;
6642
- // Filter out times that would result in negative duration (only when dates are the same)
6954
+ if (is24Hour) {
6955
+ // Generate 24-hour format options (0-23 for hours)
6956
+ for (let h = 0; h < 24; h++) {
6957
+ for (let m = 0; m < 60; m += 15) {
6958
+ // Filter out times that would result in negative duration
6643
6959
  if (startDateTime && selectedDate && shouldFilterByDate) {
6644
6960
  const selectedDateObj = dayjs(selectedDate).tz(timezone);
6645
6961
  const optionDateTime = selectedDateObj
6646
- .hour(hour24)
6962
+ .hour(h)
6647
6963
  .minute(m)
6648
6964
  .second(0)
6649
6965
  .millisecond(0);
6650
6966
  if (optionDateTime.isBefore(startDateTime)) {
6651
- continue; // Skip this option as it would result in negative duration
6967
+ continue;
6652
6968
  }
6653
6969
  }
6654
6970
  // Calculate duration if startTime is provided
@@ -6656,7 +6972,7 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
6656
6972
  if (startDateTime && selectedDate) {
6657
6973
  const selectedDateObj = dayjs(selectedDate).tz(timezone);
6658
6974
  const optionDateTime = selectedDateObj
6659
- .hour(hour24)
6975
+ .hour(h)
6660
6976
  .minute(m)
6661
6977
  .second(0)
6662
6978
  .millisecond(0);
@@ -6681,62 +6997,133 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
6681
6997
  }
6682
6998
  }
6683
6999
  }
6684
- const hourDisplay = h.toString();
6685
- const minuteDisplay = m.toString().padStart(2, '0');
6686
- const timeDisplay = `${hourDisplay}:${minuteDisplay} ${mer.toUpperCase()}`;
7000
+ const timeDisplay = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:00`;
6687
7001
  options.push({
6688
7002
  label: timeDisplay,
6689
- value: `${h}:${m}:${mer}`,
7003
+ value: `${h}:${m}:0`,
6690
7004
  hour: h,
6691
7005
  minute: m,
6692
- meridiem: mer,
6693
- searchText: timeDisplay, // Use base time without duration for searching
7006
+ second: 0,
7007
+ searchText: timeDisplay,
6694
7008
  durationText,
6695
7009
  });
6696
7010
  }
6697
7011
  }
6698
7012
  }
6699
- // Sort options by time (convert to 24-hour for proper chronological sorting)
6700
- return options.sort((a, b) => {
6701
- // Convert 12-hour to 24-hour for comparison
6702
- let hour24A = a.hour;
6703
- if (a.meridiem === 'am' && a.hour === 12)
6704
- hour24A = 0;
6705
- else if (a.meridiem === 'pm' && a.hour < 12)
6706
- hour24A = a.hour + 12;
6707
- let hour24B = b.hour;
6708
- if (b.meridiem === 'am' && b.hour === 12)
6709
- hour24B = 0;
6710
- else if (b.meridiem === 'pm' && b.hour < 12)
6711
- hour24B = b.hour + 12;
6712
- // Compare by hour first, then minute
6713
- if (hour24A !== hour24B) {
6714
- return hour24A - hour24B;
6715
- }
6716
- return a.minute - b.minute;
6717
- });
6718
- }, [startTime, selectedDate, timezone]);
7013
+ else {
7014
+ // Generate 12-hour format options (1-12 for hours, AM/PM)
7015
+ for (let h = 1; h <= 12; h++) {
7016
+ for (let m = 0; m < 60; m += 15) {
7017
+ for (const mer of ['am', 'pm']) {
7018
+ // Convert 12-hour to 24-hour for comparison
7019
+ let hour24 = h;
7020
+ if (mer === 'am' && h === 12)
7021
+ hour24 = 0;
7022
+ else if (mer === 'pm' && h < 12)
7023
+ hour24 = h + 12;
7024
+ // Filter out times that would result in negative duration
7025
+ if (startDateTime && selectedDate && shouldFilterByDate) {
7026
+ const selectedDateObj = dayjs(selectedDate).tz(timezone);
7027
+ const optionDateTime = selectedDateObj
7028
+ .hour(hour24)
7029
+ .minute(m)
7030
+ .second(0)
7031
+ .millisecond(0);
7032
+ if (optionDateTime.isBefore(startDateTime)) {
7033
+ continue;
7034
+ }
7035
+ }
7036
+ // Calculate duration if startTime is provided
7037
+ let durationText;
7038
+ if (startDateTime && selectedDate) {
7039
+ const selectedDateObj = dayjs(selectedDate).tz(timezone);
7040
+ const optionDateTime = selectedDateObj
7041
+ .hour(hour24)
7042
+ .minute(m)
7043
+ .second(0)
7044
+ .millisecond(0);
7045
+ if (optionDateTime.isValid() &&
7046
+ optionDateTime.isAfter(startDateTime)) {
7047
+ const diffMs = optionDateTime.diff(startDateTime);
7048
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
7049
+ const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
7050
+ const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
7051
+ if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
7052
+ let diffText = '';
7053
+ if (diffHours > 0) {
7054
+ diffText = `${diffHours}h ${diffMinutes}m`;
7055
+ }
7056
+ else if (diffMinutes > 0) {
7057
+ diffText = `${diffMinutes}m ${diffSeconds}s`;
7058
+ }
7059
+ else {
7060
+ diffText = `${diffSeconds}s`;
7061
+ }
7062
+ durationText = `+${diffText}`;
7063
+ }
7064
+ }
7065
+ }
7066
+ const hourDisplay = h.toString();
7067
+ const minuteDisplay = m.toString().padStart(2, '0');
7068
+ const timeDisplay = `${hourDisplay}:${minuteDisplay} ${mer.toUpperCase()}`;
7069
+ options.push({
7070
+ label: timeDisplay,
7071
+ value: `${h}:${m}:${mer}`,
7072
+ hour: h,
7073
+ minute: m,
7074
+ meridiem: mer,
7075
+ searchText: timeDisplay,
7076
+ durationText,
7077
+ });
7078
+ }
7079
+ }
7080
+ }
7081
+ // Sort 12-hour options by time (convert to 24-hour for proper chronological sorting)
7082
+ return options.sort((a, b) => {
7083
+ const a12 = a;
7084
+ const b12 = b;
7085
+ let hour24A = a12.hour;
7086
+ if (a12.meridiem === 'am' && a12.hour === 12)
7087
+ hour24A = 0;
7088
+ else if (a12.meridiem === 'pm' && a12.hour < 12)
7089
+ hour24A = a12.hour + 12;
7090
+ let hour24B = b12.hour;
7091
+ if (b12.meridiem === 'am' && b12.hour === 12)
7092
+ hour24B = 0;
7093
+ else if (b12.meridiem === 'pm' && b12.hour < 12)
7094
+ hour24B = b12.hour + 12;
7095
+ if (hour24A !== hour24B) {
7096
+ return hour24A - hour24B;
7097
+ }
7098
+ return a12.minute - b12.minute;
7099
+ });
7100
+ }
7101
+ return options;
7102
+ }, [startTime, selectedDate, timezone, is24Hour]);
6719
7103
  // itemToString returns only the clean display text (no metadata)
6720
7104
  const itemToString = useMemo(() => {
6721
7105
  return (item) => {
6722
- return item.searchText; // Clean display text only
7106
+ return item.searchText;
6723
7107
  };
6724
7108
  }, []);
6725
- // Custom filter function that filters by time and supports 24-hour format input
7109
+ // Custom filter function
7110
+ const { contains } = useFilter({ sensitivity: 'base' });
6726
7111
  const customTimeFilter = useMemo(() => {
7112
+ if (is24Hour) {
7113
+ return contains; // Simple contains filter for 24-hour format
7114
+ }
7115
+ // For 12-hour format, support both 12-hour and 24-hour input
6727
7116
  return (itemText, filterText) => {
6728
7117
  if (!filterText) {
6729
- return true; // Show all items when no filter
7118
+ return true;
6730
7119
  }
6731
7120
  const lowerItemText = itemText.toLowerCase();
6732
7121
  const lowerFilterText = filterText.toLowerCase();
6733
- // First, try matching against the display text (12-hour format)
6734
7122
  if (lowerItemText.includes(lowerFilterText)) {
6735
7123
  return true;
6736
7124
  }
6737
- // Find the corresponding item to check 24-hour format matches
6738
7125
  const item = timeOptions.find((opt) => opt.searchText.toLowerCase() === lowerItemText);
6739
- if (!item) {
7126
+ if (!item || !('meridiem' in item)) {
6740
7127
  return false;
6741
7128
  }
6742
7129
  // Convert item to 24-hour format for matching
@@ -6747,18 +7134,17 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
6747
7134
  hour24 = item.hour + 12;
6748
7135
  const hour24Str = hour24.toString().padStart(2, '0');
6749
7136
  const minuteStr = item.minute.toString().padStart(2, '0');
6750
- // Check if filterText matches 24-hour format variations
6751
7137
  const formats = [
6752
- `${hour24Str}:${minuteStr}`, // "13:30"
6753
- `${hour24Str}${minuteStr}`, // "1330"
6754
- hour24Str, // "13"
6755
- `${hour24}:${minuteStr}`, // "13:30" (without padding)
6756
- hour24.toString(), // "13" (without padding)
7138
+ `${hour24Str}:${minuteStr}`,
7139
+ `${hour24Str}${minuteStr}`,
7140
+ hour24Str,
7141
+ `${hour24}:${minuteStr}`,
7142
+ hour24.toString(),
6757
7143
  ];
6758
7144
  return formats.some((format) => format.toLowerCase().includes(lowerFilterText) ||
6759
7145
  lowerFilterText.includes(format.toLowerCase()));
6760
7146
  };
6761
- }, [timeOptions]);
7147
+ }, [timeOptions, is24Hour, contains]);
6762
7148
  const { collection, filter } = useListCollection({
6763
7149
  initialItems: timeOptions,
6764
7150
  itemToString: itemToString,
@@ -6767,32 +7153,48 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
6767
7153
  });
6768
7154
  // Get current value string for combobox
6769
7155
  const currentValue = useMemo(() => {
6770
- if (hour === null || minute === null || meridiem === null) {
6771
- return '';
7156
+ if (is24Hour) {
7157
+ if (hour === null || minute === null || second === null) {
7158
+ return '';
7159
+ }
7160
+ return `${hour}:${minute}:${second}`;
7161
+ }
7162
+ else {
7163
+ if (hour === null || minute === null || meridiem === null) {
7164
+ return '';
7165
+ }
7166
+ return `${hour}:${minute}:${meridiem}`;
6772
7167
  }
6773
- return `${hour}:${minute}:${meridiem}`;
6774
- }, [hour, minute, meridiem]);
7168
+ }, [hour, minute, second, meridiem, is24Hour]);
6775
7169
  // Calculate duration difference
6776
7170
  const durationDiff = useMemo(() => {
6777
- if (!startTime ||
6778
- !selectedDate ||
6779
- hour === null ||
6780
- minute === null ||
6781
- meridiem === null) {
7171
+ if (!startTime || !selectedDate || hour === null || minute === null) {
6782
7172
  return null;
6783
7173
  }
7174
+ if (is24Hour) {
7175
+ if (second === null)
7176
+ return null;
7177
+ }
7178
+ else {
7179
+ if (meridiem === null)
7180
+ return null;
7181
+ }
6784
7182
  const startDateObj = dayjs(startTime).tz(timezone);
6785
7183
  const selectedDateObj = dayjs(selectedDate).tz(timezone);
6786
- // Convert 12-hour to 24-hour format
7184
+ // Convert to 24-hour format
6787
7185
  let hour24 = hour;
6788
- if (meridiem === 'am' && hour === 12)
6789
- hour24 = 0;
6790
- else if (meridiem === 'pm' && hour < 12)
6791
- hour24 = hour + 12;
7186
+ if (!is24Hour && meridiem) {
7187
+ if (meridiem === 'am' && hour === 12)
7188
+ hour24 = 0;
7189
+ else if (meridiem === 'pm' && hour < 12)
7190
+ hour24 = hour + 12;
7191
+ }
6792
7192
  const currentDateTime = selectedDateObj
6793
7193
  .hour(hour24)
6794
7194
  .minute(minute)
6795
- .second(0)
7195
+ .second(is24Hour && second !== null && second !== undefined
7196
+ ? second
7197
+ : 0)
6796
7198
  .millisecond(0);
6797
7199
  if (!startDateObj.isValid() || !currentDateTime.isValid()) {
6798
7200
  return null;
@@ -6818,13 +7220,28 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
6818
7220
  return `+${diffText}`;
6819
7221
  }
6820
7222
  return null;
6821
- }, [hour, minute, meridiem, startTime, selectedDate, timezone]);
7223
+ }, [
7224
+ hour,
7225
+ minute,
7226
+ second,
7227
+ meridiem,
7228
+ startTime,
7229
+ selectedDate,
7230
+ timezone,
7231
+ is24Hour,
7232
+ ]);
6822
7233
  const handleClear = () => {
6823
7234
  setHour(null);
6824
7235
  setMinute(null);
6825
- setMeridiem(null);
6826
- filter(''); // Reset filter to show all options
6827
- onChange({ hour: null, minute: null, meridiem: null });
7236
+ if (is24Hour && setSecond) {
7237
+ setSecond(null);
7238
+ handleTimeChange(null, null, null, null);
7239
+ }
7240
+ else if (!is24Hour && setMeridiem) {
7241
+ setMeridiem(null);
7242
+ handleTimeChange(null, null, null, null);
7243
+ }
7244
+ filter('');
6828
7245
  };
6829
7246
  const handleValueChange = (details) => {
6830
7247
  if (details.value.length === 0) {
@@ -6836,112 +7253,165 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
6836
7253
  if (selectedOption) {
6837
7254
  setHour(selectedOption.hour);
6838
7255
  setMinute(selectedOption.minute);
6839
- setMeridiem(selectedOption.meridiem);
6840
- filter(''); // Reset filter after selection
6841
- onChange({
6842
- hour: selectedOption.hour,
6843
- minute: selectedOption.minute,
6844
- meridiem: selectedOption.meridiem,
6845
- });
7256
+ filter('');
7257
+ if (is24Hour) {
7258
+ const opt24 = selectedOption;
7259
+ if (setSecond)
7260
+ setSecond(opt24.second);
7261
+ handleTimeChange(opt24.hour, opt24.minute, opt24.second, null);
7262
+ }
7263
+ else {
7264
+ const opt12 = selectedOption;
7265
+ if (setMeridiem)
7266
+ setMeridiem(opt12.meridiem);
7267
+ handleTimeChange(opt12.hour, opt12.minute, null, opt12.meridiem);
7268
+ }
6846
7269
  }
6847
7270
  };
6848
7271
  // Parse input value and update state
6849
7272
  const parseAndCommitInput = (value) => {
6850
7273
  const trimmedValue = value.trim();
6851
- // Filter the collection based on input
6852
7274
  filter(trimmedValue);
6853
7275
  if (!trimmedValue) {
6854
7276
  return;
6855
7277
  }
6856
- // Parse 24-hour format first (e.g., "13:30", "14:00", "1330", "1400", "9:05", "905")
6857
- const timePattern24Hour = /^(\d{1,2}):?(\d{2})$/;
6858
- const match24Hour = trimmedValue.match(timePattern24Hour);
6859
- if (match24Hour) {
6860
- const parsedHour24 = parseInt(match24Hour[1], 10);
6861
- const parsedMinute = parseInt(match24Hour[2], 10);
6862
- // Validate 24-hour format ranges
6863
- if (parsedHour24 >= 0 &&
6864
- parsedHour24 <= 23 &&
6865
- parsedMinute >= 0 &&
6866
- parsedMinute <= 59) {
6867
- // Convert 24-hour to 12-hour format
6868
- let hour12;
6869
- let meridiem;
6870
- if (parsedHour24 === 0) {
6871
- hour12 = 12;
6872
- meridiem = 'am';
6873
- }
6874
- else if (parsedHour24 === 12) {
6875
- hour12 = 12;
6876
- meridiem = 'pm';
6877
- }
6878
- else if (parsedHour24 > 12) {
6879
- hour12 = parsedHour24 - 12;
6880
- meridiem = 'pm';
7278
+ if (is24Hour) {
7279
+ // Parse 24-hour format: "HH:mm:ss" or "HH:mm" or "HHmmss" or "HHmm"
7280
+ const timePattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/;
7281
+ const match = trimmedValue.match(timePattern);
7282
+ if (match) {
7283
+ const parsedHour = parseInt(match[1], 10);
7284
+ const parsedMinute = parseInt(match[2], 10);
7285
+ const parsedSecond = match[3] ? parseInt(match[3], 10) : 0;
7286
+ if (parsedHour >= 0 &&
7287
+ parsedHour <= 23 &&
7288
+ parsedMinute >= 0 &&
7289
+ parsedMinute <= 59 &&
7290
+ parsedSecond >= 0 &&
7291
+ parsedSecond <= 59) {
7292
+ setHour(parsedHour);
7293
+ setMinute(parsedMinute);
7294
+ if (setSecond)
7295
+ setSecond(parsedSecond);
7296
+ handleTimeChange(parsedHour, parsedMinute, parsedSecond, null);
7297
+ return;
6881
7298
  }
6882
- else {
6883
- hour12 = parsedHour24;
6884
- meridiem = 'am';
7299
+ }
7300
+ // Try numbers only format: "123045" or "1230"
7301
+ const numbersOnly = trimmedValue.replace(/[^0-9]/g, '');
7302
+ if (numbersOnly.length >= 4) {
7303
+ const parsedHour = parseInt(numbersOnly.slice(0, 2), 10);
7304
+ const parsedMinute = parseInt(numbersOnly.slice(2, 4), 10);
7305
+ const parsedSecond = numbersOnly.length >= 6 ? parseInt(numbersOnly.slice(4, 6), 10) : 0;
7306
+ if (parsedHour >= 0 &&
7307
+ parsedHour <= 23 &&
7308
+ parsedMinute >= 0 &&
7309
+ parsedMinute <= 59 &&
7310
+ parsedSecond >= 0 &&
7311
+ parsedSecond <= 59) {
7312
+ setHour(parsedHour);
7313
+ setMinute(parsedMinute);
7314
+ if (setSecond)
7315
+ setSecond(parsedSecond);
7316
+ handleTimeChange(parsedHour, parsedMinute, parsedSecond, null);
7317
+ return;
6885
7318
  }
6886
- setHour(hour12);
6887
- setMinute(parsedMinute);
6888
- setMeridiem(meridiem);
6889
- onChange({
6890
- hour: hour12,
6891
- minute: parsedMinute,
6892
- meridiem: meridiem,
6893
- });
6894
- return;
6895
7319
  }
6896
7320
  }
6897
- // Parse formats like "1:30 PM", "1:30PM", "1:30 pm", "1:30pm"
6898
- const timePattern12Hour = /^(\d{1,2}):(\d{1,2})\s*(am|pm|AM|PM)$/i;
6899
- const match12Hour = trimmedValue.match(timePattern12Hour);
6900
- if (match12Hour) {
6901
- const parsedHour = parseInt(match12Hour[1], 10);
6902
- const parsedMinute = parseInt(match12Hour[2], 10);
6903
- const parsedMeridiem = match12Hour[3].toLowerCase();
6904
- // Validate ranges
6905
- if (parsedHour >= 1 &&
6906
- parsedHour <= 12 &&
6907
- parsedMinute >= 0 &&
6908
- parsedMinute <= 59) {
6909
- setHour(parsedHour);
6910
- setMinute(parsedMinute);
6911
- setMeridiem(parsedMeridiem);
6912
- onChange({
6913
- hour: parsedHour,
6914
- minute: parsedMinute,
6915
- meridiem: parsedMeridiem,
6916
- });
6917
- return;
7321
+ else {
7322
+ // Parse 24-hour format first (e.g., "13:30", "14:00", "1330", "1400")
7323
+ const timePattern24Hour = /^(\d{1,2}):?(\d{2})$/;
7324
+ const match24Hour = trimmedValue.match(timePattern24Hour);
7325
+ if (match24Hour) {
7326
+ const parsedHour24 = parseInt(match24Hour[1], 10);
7327
+ const parsedMinute = parseInt(match24Hour[2], 10);
7328
+ if (parsedHour24 >= 0 &&
7329
+ parsedHour24 <= 23 &&
7330
+ parsedMinute >= 0 &&
7331
+ parsedMinute <= 59) {
7332
+ // Convert 24-hour to 12-hour format
7333
+ let hour12;
7334
+ let meridiem;
7335
+ if (parsedHour24 === 0) {
7336
+ hour12 = 12;
7337
+ meridiem = 'am';
7338
+ }
7339
+ else if (parsedHour24 === 12) {
7340
+ hour12 = 12;
7341
+ meridiem = 'pm';
7342
+ }
7343
+ else if (parsedHour24 > 12) {
7344
+ hour12 = parsedHour24 - 12;
7345
+ meridiem = 'pm';
7346
+ }
7347
+ else {
7348
+ hour12 = parsedHour24;
7349
+ meridiem = 'am';
7350
+ }
7351
+ setHour(hour12);
7352
+ setMinute(parsedMinute);
7353
+ if (setMeridiem)
7354
+ setMeridiem(meridiem);
7355
+ handleTimeChange(hour12, parsedMinute, null, meridiem);
7356
+ return;
7357
+ }
6918
7358
  }
6919
- }
6920
- // Try to parse formats like "130pm" or "130 pm" (without colon)
6921
- const timePatternNoColon = /^(\d{1,4})\s*(am|pm|AM|PM)$/i;
6922
- const matchNoColon = trimmedValue.match(timePatternNoColon);
6923
- if (matchNoColon) {
6924
- const numbersOnly = matchNoColon[1];
6925
- const parsedMeridiem = matchNoColon[2].toLowerCase();
6926
- if (numbersOnly.length >= 3) {
6927
- const parsedHour = parseInt(numbersOnly.slice(0, -2), 10);
6928
- const parsedMinute = parseInt(numbersOnly.slice(-2), 10);
6929
- // Validate ranges
7359
+ // Parse formats like "1:30 PM", "1:30PM", "1:30 pm", "1:30pm"
7360
+ const timePattern12Hour = /^(\d{1,2}):(\d{1,2})\s*(am|pm|AM|PM)$/i;
7361
+ const match12Hour = trimmedValue.match(timePattern12Hour);
7362
+ if (match12Hour) {
7363
+ const parsedHour = parseInt(match12Hour[1], 10);
7364
+ const parsedMinute = parseInt(match12Hour[2], 10);
7365
+ const parsedMeridiem = match12Hour[3].toLowerCase();
6930
7366
  if (parsedHour >= 1 &&
6931
7367
  parsedHour <= 12 &&
6932
7368
  parsedMinute >= 0 &&
6933
7369
  parsedMinute <= 59) {
6934
7370
  setHour(parsedHour);
6935
7371
  setMinute(parsedMinute);
6936
- setMeridiem(parsedMeridiem);
6937
- onChange({
6938
- hour: parsedHour,
6939
- minute: parsedMinute,
6940
- meridiem: parsedMeridiem,
6941
- });
7372
+ if (setMeridiem)
7373
+ setMeridiem(parsedMeridiem);
7374
+ handleTimeChange(parsedHour, parsedMinute, null, parsedMeridiem);
7375
+ return;
7376
+ }
7377
+ }
7378
+ // Parse formats like "12am" or "1pm" (hour only with meridiem, no minutes)
7379
+ const timePatternHourOnly = /^(\d{1,2})\s*(am|pm|AM|PM)$/i;
7380
+ const matchHourOnly = trimmedValue.match(timePatternHourOnly);
7381
+ if (matchHourOnly) {
7382
+ const parsedHour = parseInt(matchHourOnly[1], 10);
7383
+ const parsedMeridiem = matchHourOnly[2].toLowerCase();
7384
+ if (parsedHour >= 1 && parsedHour <= 12) {
7385
+ setHour(parsedHour);
7386
+ setMinute(0); // Default to 0 minutes when only hour is provided
7387
+ if (setMeridiem)
7388
+ setMeridiem(parsedMeridiem);
7389
+ handleTimeChange(parsedHour, 0, null, parsedMeridiem);
6942
7390
  return;
6943
7391
  }
6944
7392
  }
7393
+ // Try to parse formats like "130pm" or "130 pm" (without colon, with minutes)
7394
+ const timePatternNoColon = /^(\d{1,4})\s*(am|pm|AM|PM)$/i;
7395
+ const matchNoColon = trimmedValue.match(timePatternNoColon);
7396
+ if (matchNoColon) {
7397
+ const numbersOnly = matchNoColon[1];
7398
+ const parsedMeridiem = matchNoColon[2].toLowerCase();
7399
+ if (numbersOnly.length >= 3) {
7400
+ const parsedHour = parseInt(numbersOnly.slice(0, -2), 10);
7401
+ const parsedMinute = parseInt(numbersOnly.slice(-2), 10);
7402
+ if (parsedHour >= 1 &&
7403
+ parsedHour <= 12 &&
7404
+ parsedMinute >= 0 &&
7405
+ parsedMinute <= 59) {
7406
+ setHour(parsedHour);
7407
+ setMinute(parsedMinute);
7408
+ if (setMeridiem)
7409
+ setMeridiem(parsedMeridiem);
7410
+ handleTimeChange(parsedHour, parsedMinute, null, parsedMeridiem);
7411
+ return;
7412
+ }
7413
+ }
7414
+ }
6945
7415
  }
6946
7416
  // Parse failed, select first result
6947
7417
  selectFirstResult();
@@ -6952,55 +7422,84 @@ const TimePicker$1 = ({ hour, setHour, minute, setMinute, meridiem, setMeridiem,
6952
7422
  const firstItem = collection.items[0];
6953
7423
  setHour(firstItem.hour);
6954
7424
  setMinute(firstItem.minute);
6955
- setMeridiem(firstItem.meridiem);
6956
- filter(''); // Reset filter after selection
6957
- onChange({
6958
- hour: firstItem.hour,
6959
- minute: firstItem.minute,
6960
- meridiem: firstItem.meridiem,
6961
- });
7425
+ filter('');
7426
+ if (is24Hour) {
7427
+ const opt24 = firstItem;
7428
+ if (setSecond)
7429
+ setSecond(opt24.second);
7430
+ handleTimeChange(opt24.hour, opt24.minute, opt24.second, null);
7431
+ }
7432
+ else {
7433
+ const opt12 = firstItem;
7434
+ if (setMeridiem)
7435
+ setMeridiem(opt12.meridiem);
7436
+ handleTimeChange(opt12.hour, opt12.minute, null, opt12.meridiem);
7437
+ }
6962
7438
  }
6963
7439
  };
6964
7440
  const handleInputValueChange = (details) => {
6965
- // Filter the collection based on input, but don't parse yet
7441
+ if (is24Hour) {
7442
+ setInputValue(details.inputValue);
7443
+ }
6966
7444
  filter(details.inputValue);
6967
7445
  };
6968
7446
  const handleFocus = (e) => {
6969
- // Select all text when focusing
6970
7447
  e.target.select();
6971
7448
  };
6972
7449
  const handleBlur = (e) => {
6973
- // Parse and commit the input value when losing focus
6974
- const inputValue = e.target.value;
6975
- if (inputValue) {
6976
- parseAndCommitInput(inputValue);
7450
+ const inputVal = e.target.value;
7451
+ if (is24Hour) {
7452
+ setInputValue(inputVal);
7453
+ }
7454
+ if (inputVal) {
7455
+ parseAndCommitInput(inputVal);
6977
7456
  }
6978
7457
  };
6979
7458
  const handleKeyDown = (e) => {
6980
- // Commit input on Enter key
6981
7459
  if (e.key === 'Enter') {
6982
7460
  e.preventDefault();
6983
- const inputValue = e.currentTarget.value;
6984
- if (inputValue) {
6985
- parseAndCommitInput(inputValue);
7461
+ const inputVal = e.currentTarget.value;
7462
+ if (is24Hour) {
7463
+ setInputValue(inputVal);
7464
+ }
7465
+ if (inputVal) {
7466
+ parseAndCommitInput(inputVal);
6986
7467
  }
6987
- // Blur the input
6988
7468
  e.currentTarget?.blur();
6989
7469
  }
6990
7470
  };
6991
- return (jsx(Flex, { direction: "column", gap: 3, children: jsxs(Flex, { alignItems: "center", gap: "2", width: "auto", minWidth: "300px", children: [jsxs(Combobox.Root, { collection: collection, value: currentValue ? [currentValue] : [], onValueChange: handleValueChange, onInputValueChange: handleInputValueChange, allowCustomValue: true, selectionBehavior: "replace", openOnClick: true, flex: 1, children: [jsxs(Combobox.Control, { children: [jsx(InputGroup$1, { startElement: jsx(BsClock, {}), children: jsx(Combobox.Input, { placeholder: labels?.placeholder ?? 'hh:mm AM/PM', onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown }) }), jsx(Combobox.IndicatorGroup, { children: jsx(Combobox.Trigger, {}) })] }), jsx(Portal, { disabled: !portalled, children: jsx(Combobox.Positioner, { children: jsxs(Combobox.Content, { children: [jsx(Combobox.Empty, { children: labels?.emptyMessage ?? 'No time found' }), collection.items.map((item) => (jsxs(Combobox.Item, { item: item, children: [jsxs(Flex, { alignItems: "center", gap: 2, width: "100%", children: [jsx(Text, { flex: 1, children: item.label }), item.durationText && (jsx(Tag$1.Root, { size: "sm", children: jsx(Tag$1.Label, { children: item.durationText }) }))] }), jsx(Combobox.ItemIndicator, {})] }, item.value)))] }) }) })] }), durationDiff && (jsx(Tag$1.Root, { size: "sm", children: jsx(Tag$1.Label, { children: durationDiff }) })), jsx(Button$1, { onClick: handleClear, size: "sm", variant: "ghost", children: jsx(Icon, { children: jsx(MdCancel, {}) }) })] }) }));
7471
+ return (jsx(Flex, { direction: "column", gap: 3, children: jsxs(Flex, { alignItems: "center", gap: "2", width: "auto", minWidth: "300px", children: [jsxs(Combobox.Root, { collection: collection, value: currentValue ? [currentValue] : [], onValueChange: handleValueChange, onInputValueChange: handleInputValueChange, allowCustomValue: true, selectionBehavior: "replace", flex: 1, children: [jsxs(Combobox.Control, { children: [jsx(InputGroup$1, { startElement: jsx(BsClock, {}), children: jsx(Combobox.Input, { value: is24Hour ? inputValue : undefined, placeholder: labels?.placeholder ?? (is24Hour ? 'HH:mm:ss' : 'hh:mm AM/PM'), onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown }) }), jsx(Combobox.IndicatorGroup, { children: jsx(Combobox.Trigger, {}) })] }), jsx(Portal, { disabled: !portalled, children: jsx(Combobox.Positioner, { children: jsxs(Combobox.Content, { children: [jsx(Combobox.Empty, { children: labels?.emptyMessage ?? 'No time found' }), collection.items.map((item) => (jsxs(Combobox.Item, { item: item, children: [jsxs(Flex, { alignItems: "center", gap: 2, width: "100%", children: [jsx(Text, { flex: 1, children: item.label }), item.durationText && (jsx(Tag$1.Root, { size: "sm", children: jsx(Tag$1.Label, { children: item.durationText }) }))] }), jsx(Combobox.ItemIndicator, {})] }, item.value)))] }) }) })] }), durationDiff && (jsx(Tag$1.Root, { size: "sm", children: jsx(Tag$1.Label, { children: durationDiff }) }))] }) }));
6992
7472
  };
6993
7473
 
6994
7474
  dayjs.extend(timezone);
6995
7475
  const TimePicker = ({ column, schema, prefix }) => {
6996
7476
  const { watch, formState: { errors }, setValue, } = useFormContext();
6997
7477
  const { timezone, insideDialog, timePickerLabels } = useSchemaContext();
6998
- const { required, gridColumn = 'span 12', gridRow = 'span 1', timeFormat = 'HH:mm:ssZ', displayTimeFormat = 'hh:mm A', } = schema;
7478
+ const { required, gridColumn = 'span 12', gridRow = 'span 1', timeFormat = 'HH:mm:ssZ', displayTimeFormat = 'hh:mm A', startTimeField, selectedDateField, } = schema;
6999
7479
  const isRequired = required?.some((columnId) => columnId === column);
7000
7480
  const colLabel = `${prefix}${column}`;
7001
7481
  const formI18n = useFormI18n(column, prefix, schema);
7002
7482
  const [open, setOpen] = useState(false);
7003
7483
  const value = watch(colLabel);
7484
+ // Watch startTime and selectedDate fields for offset calculation
7485
+ const startTimeValue = startTimeField
7486
+ ? watch(`${prefix}${startTimeField}`)
7487
+ : undefined;
7488
+ const selectedDateValue = selectedDateField
7489
+ ? watch(`${prefix}${selectedDateField}`)
7490
+ : undefined;
7491
+ // Convert to ISO string format for startTime if it's a date-time string
7492
+ const startTime = startTimeValue
7493
+ ? dayjs(startTimeValue).tz(timezone).isValid()
7494
+ ? dayjs(startTimeValue).tz(timezone).toISOString()
7495
+ : undefined
7496
+ : undefined;
7497
+ // Convert selectedDate to YYYY-MM-DD format
7498
+ const selectedDate = selectedDateValue
7499
+ ? dayjs(selectedDateValue).tz(timezone).isValid()
7500
+ ? dayjs(selectedDateValue).tz(timezone).format('YYYY-MM-DD')
7501
+ : undefined
7502
+ : undefined;
7004
7503
  const displayedTime = dayjs(`1970-01-01T${value}`).tz(timezone).isValid()
7005
7504
  ? dayjs(`1970-01-01T${value}`).tz(timezone).format(displayTimeFormat)
7006
7505
  : '';
@@ -7056,941 +7555,860 @@ const TimePicker = ({ column, schema, prefix }) => {
7056
7555
  return (jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
7057
7556
  gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: jsxs(Popover.Root, { open: open, onOpenChange: (e) => setOpen(e.open), closeOnInteractOutside: true, children: [jsx(Popover.Trigger, { asChild: true, children: jsxs(Button, { size: "sm", variant: "outline", onClick: () => {
7058
7557
  setOpen(true);
7059
- }, justifyContent: 'start', children: [jsx(IoMdClock, {}), !!value ? `${displayedTime}` : ''] }) }), insideDialog ? (jsx(Popover.Positioner, { children: jsx(Popover.Content, { maxH: "70vh", overflowY: "auto", children: jsx(Popover.Body, { overflow: "visible", children: jsx(TimePicker$1, { hour: hour, setHour: setHour, minute: minute, setMinute: setMinute, meridiem: meridiem, setMeridiem: setMeridiem, onChange: handleTimeChange, labels: timePickerLabels }) }) }) })) : (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { children: jsx(Popover.Body, { children: jsx(TimePicker$1, { hour: hour, setHour: setHour, minute: minute, setMinute: setMinute, meridiem: meridiem, setMeridiem: setMeridiem, onChange: handleTimeChange, labels: timePickerLabels }) }) }) }) }))] }) }));
7558
+ }, justifyContent: 'start', children: [jsx(IoMdClock, {}), !!value ? `${displayedTime}` : ''] }) }), insideDialog ? (jsx(Popover.Positioner, { children: jsx(Popover.Content, { maxH: "70vh", overflowY: "auto", children: jsx(Popover.Body, { overflow: "visible", children: jsx(TimePicker$1, { hour: hour, setHour: setHour, minute: minute, setMinute: setMinute, meridiem: meridiem, setMeridiem: setMeridiem, onChange: handleTimeChange, startTime: startTime, selectedDate: selectedDate, timezone: timezone, portalled: false, labels: timePickerLabels }) }) }) })) : (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { children: jsx(Popover.Body, { children: jsx(TimePicker$1, { format: "12h", hour: hour, setHour: setHour, minute: minute, setMinute: setMinute, meridiem: meridiem, setMeridiem: setMeridiem, onChange: handleTimeChange, startTime: startTime, selectedDate: selectedDate, timezone: timezone, portalled: false, labels: timePickerLabels }) }) }) }) }))] }) }));
7060
7559
  };
7061
7560
 
7062
7561
  dayjs.extend(utc);
7063
7562
  dayjs.extend(timezone);
7064
- function IsoTimePicker({ hour, setHour, minute, setMinute, second, setSecond,
7065
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
7066
- onChange = (_newValue) => { }, startTime, selectedDate, timezone = 'Asia/Hong_Kong', portalled = true, labels = {
7067
- placeholder: 'HH:mm:ss',
7068
- emptyMessage: 'No time found',
7069
- }, }) {
7070
- // Generate time options (every 15 minutes, seconds always 0)
7071
- const timeOptions = useMemo(() => {
7072
- const options = [];
7073
- // Get start time for comparison if provided
7074
- let startDateTime = null;
7075
- let shouldFilterByDate = false;
7076
- if (startTime && selectedDate) {
7077
- const startDateObj = dayjs(startTime).tz(timezone);
7078
- const selectedDateObj = dayjs(selectedDate).tz(timezone);
7079
- if (startDateObj.isValid() && selectedDateObj.isValid()) {
7080
- startDateTime = startDateObj;
7081
- // Only filter if dates are the same
7082
- shouldFilterByDate =
7083
- startDateObj.format('YYYY-MM-DD') ===
7084
- selectedDateObj.format('YYYY-MM-DD');
7085
- }
7086
- }
7087
- for (let h = 0; h < 24; h++) {
7088
- for (let m = 0; m < 60; m += 15) {
7089
- const timeDisplay = `${h.toString().padStart(2, '0')}:${m
7090
- .toString()
7091
- .padStart(2, '0')}:00`;
7092
- // Filter out times that would result in negative duration (only when dates are the same)
7093
- if (startDateTime && selectedDate && shouldFilterByDate) {
7094
- const selectedDateObj = dayjs(selectedDate).tz(timezone);
7095
- const optionDateTime = selectedDateObj
7096
- .hour(h)
7097
- .minute(m)
7098
- .second(0)
7099
- .millisecond(0);
7100
- if (optionDateTime.isBefore(startDateTime)) {
7101
- continue; // Skip this option as it would result in negative duration
7102
- }
7103
- }
7104
- // Calculate duration if startTime is provided
7105
- let durationText;
7106
- if (startDateTime && selectedDate) {
7107
- const selectedDateObj = dayjs(selectedDate).tz(timezone);
7108
- const optionDateTime = selectedDateObj
7109
- .hour(h)
7110
- .minute(m)
7111
- .second(0)
7112
- .millisecond(0);
7113
- if (optionDateTime.isValid() &&
7114
- optionDateTime.isAfter(startDateTime)) {
7115
- const diffMs = optionDateTime.diff(startDateTime);
7116
- const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
7117
- const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
7118
- const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
7119
- if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
7120
- let diffText = '';
7121
- if (diffHours > 0) {
7122
- diffText = `${diffHours}h ${diffMinutes}m`;
7123
- }
7124
- else if (diffMinutes > 0) {
7125
- diffText = `${diffMinutes}m ${diffSeconds}s`;
7126
- }
7127
- else {
7128
- diffText = `${diffSeconds}s`;
7129
- }
7130
- durationText = `+${diffText}`;
7131
- }
7132
- }
7133
- }
7134
- options.push({
7135
- label: timeDisplay,
7136
- value: `${h}:${m}:0`,
7137
- hour: h,
7138
- minute: m,
7139
- second: 0,
7140
- searchText: timeDisplay, // Use base time without duration for searching
7141
- durationText,
7142
- });
7143
- }
7144
- }
7145
- return options;
7146
- }, [startTime, selectedDate, timezone]);
7147
- const { contains } = useFilter({ sensitivity: 'base' });
7148
- const { collection, filter } = useListCollection({
7149
- initialItems: timeOptions,
7150
- itemToString: (item) => item.searchText, // Use searchText (without duration) for filtering
7151
- itemToValue: (item) => item.value,
7152
- filter: contains,
7153
- });
7154
- // Get current value string for combobox
7155
- const currentValue = useMemo(() => {
7156
- if (hour === null || minute === null || second === null) {
7157
- return '';
7158
- }
7159
- return `${hour}:${minute}:${second}`;
7160
- }, [hour, minute, second]);
7161
- // Calculate duration difference
7162
- const durationDiff = useMemo(() => {
7163
- if (!startTime ||
7164
- !selectedDate ||
7165
- hour === null ||
7166
- minute === null ||
7167
- second === null) {
7563
+ dayjs.extend(customParseFormat);
7564
+ function DateTimePicker$1({ value, onChange, format = 'date-time', showSeconds = false, labels = {
7565
+ monthNamesShort: [
7566
+ 'January',
7567
+ 'February',
7568
+ 'March',
7569
+ 'April',
7570
+ 'May',
7571
+ 'June',
7572
+ 'July',
7573
+ 'August',
7574
+ 'September',
7575
+ 'October',
7576
+ 'November',
7577
+ 'December',
7578
+ ],
7579
+ weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
7580
+ backButtonLabel: 'Back',
7581
+ forwardButtonLabel: 'Forward',
7582
+ }, timePickerLabels, timezone: tz = 'Asia/Hong_Kong', startTime, minDate, maxDate, portalled = false, defaultDate, defaultTime, showQuickActions = false, quickActionLabels = {
7583
+ yesterday: 'Yesterday',
7584
+ today: 'Today',
7585
+ tomorrow: 'Tomorrow',
7586
+ plus7Days: '+7 Days',
7587
+ }, showTimezoneSelector = false, }) {
7588
+ const is24Hour = format === 'iso-date-time' || showSeconds;
7589
+ const { monthNamesShort = [
7590
+ 'January',
7591
+ 'February',
7592
+ 'March',
7593
+ 'April',
7594
+ 'May',
7595
+ 'June',
7596
+ 'July',
7597
+ 'August',
7598
+ 'September',
7599
+ 'October',
7600
+ 'November',
7601
+ 'December',
7602
+ ], weekdayNamesShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], backButtonLabel = 'Back', forwardButtonLabel = 'Forward', } = labels;
7603
+ // Parse value to get date and time
7604
+ const parsedValue = useMemo(() => {
7605
+ if (!value)
7168
7606
  return null;
7169
- }
7170
- const startDateObj = dayjs(startTime).tz(timezone);
7171
- const selectedDateObj = dayjs(selectedDate).tz(timezone);
7172
- const currentDateTime = selectedDateObj
7173
- .hour(hour)
7174
- .minute(minute)
7175
- .second(second ?? 0)
7176
- .millisecond(0);
7177
- if (!startDateObj.isValid() || !currentDateTime.isValid()) {
7607
+ const dateObj = dayjs(value).tz(tz);
7608
+ if (!dateObj.isValid())
7178
7609
  return null;
7610
+ return dateObj;
7611
+ }, [value, tz]);
7612
+ // Initialize date state
7613
+ const [selectedDate, setSelectedDate] = useState(() => {
7614
+ if (parsedValue) {
7615
+ return parsedValue.toDate();
7179
7616
  }
7180
- const diffMs = currentDateTime.diff(startDateObj);
7181
- if (diffMs < 0) {
7182
- return null;
7617
+ if (defaultDate) {
7618
+ const defaultDateObj = dayjs(defaultDate).tz(tz);
7619
+ return defaultDateObj.isValid() ? defaultDateObj.toDate() : new Date();
7183
7620
  }
7184
- const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
7185
- const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
7186
- const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
7187
- if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
7188
- let diffText = '';
7189
- if (diffHours > 0) {
7190
- diffText = `${diffHours}h ${diffMinutes}m`;
7191
- }
7192
- else if (diffMinutes > 0) {
7193
- diffText = `${diffMinutes}m ${diffSeconds}s`;
7194
- }
7195
- else {
7196
- diffText = `${diffSeconds}s`;
7197
- }
7198
- return `+${diffText}`;
7621
+ return new Date();
7622
+ });
7623
+ // Initialize time state
7624
+ const [hour, setHour] = useState(() => {
7625
+ if (parsedValue) {
7626
+ return parsedValue.hour();
7627
+ }
7628
+ if (defaultTime?.hour !== null && defaultTime?.hour !== undefined) {
7629
+ return defaultTime.hour;
7199
7630
  }
7200
7631
  return null;
7201
- }, [hour, minute, second, startTime, selectedDate, timezone]);
7202
- const handleClear = () => {
7203
- setHour(null);
7204
- setMinute(null);
7205
- setSecond(null);
7206
- filter(''); // Reset filter to show all options
7207
- onChange({ hour: null, minute: null, second: null });
7208
- };
7209
- const handleValueChange = (details) => {
7210
- if (details.value.length === 0) {
7211
- handleClear();
7212
- return;
7632
+ });
7633
+ const [minute, setMinute] = useState(() => {
7634
+ if (parsedValue) {
7635
+ return parsedValue.minute();
7213
7636
  }
7214
- const selectedValue = details.value[0];
7215
- const selectedOption = timeOptions.find((opt) => opt.value === selectedValue);
7216
- if (selectedOption) {
7217
- setHour(selectedOption.hour);
7218
- setMinute(selectedOption.minute);
7219
- setSecond(selectedOption.second);
7220
- filter(''); // Reset filter after selection
7221
- onChange({
7222
- hour: selectedOption.hour,
7223
- minute: selectedOption.minute,
7224
- second: selectedOption.second,
7225
- });
7637
+ if (defaultTime?.minute !== null && defaultTime?.minute !== undefined) {
7638
+ return defaultTime.minute;
7226
7639
  }
7227
- };
7228
- // Parse input value and update state
7229
- const parseAndCommitInput = (value) => {
7230
- const trimmedValue = value.trim();
7231
- // Filter the collection based on input
7232
- filter(trimmedValue);
7233
- if (!trimmedValue) {
7640
+ return null;
7641
+ });
7642
+ const [second, setSecond] = useState(() => {
7643
+ if (parsedValue) {
7644
+ return parsedValue.second();
7645
+ }
7646
+ if (defaultTime?.second !== null && defaultTime?.second !== undefined) {
7647
+ return defaultTime.second;
7648
+ }
7649
+ return showSeconds ? 0 : null;
7650
+ });
7651
+ const [meridiem, setMeridiem] = useState(() => {
7652
+ if (parsedValue) {
7653
+ const h = parsedValue.hour();
7654
+ return h < 12 ? 'am' : 'pm';
7655
+ }
7656
+ if (defaultTime?.meridiem !== null && defaultTime?.meridiem !== undefined) {
7657
+ return defaultTime.meridiem;
7658
+ }
7659
+ return is24Hour ? null : 'am';
7660
+ });
7661
+ // Popover state - separate for date, time, and timezone
7662
+ const [datePopoverOpen, setDatePopoverOpen] = useState(false);
7663
+ const [timePopoverOpen, setTimePopoverOpen] = useState(false);
7664
+ const [timezonePopoverOpen, setTimezonePopoverOpen] = useState(false);
7665
+ const [calendarPopoverOpen, setCalendarPopoverOpen] = useState(false);
7666
+ // Timezone offset state
7667
+ const [timezoneOffset, setTimezoneOffset] = useState(() => {
7668
+ if (parsedValue) {
7669
+ return parsedValue.format('Z');
7670
+ }
7671
+ // Default to +08:00
7672
+ return '+08:00';
7673
+ });
7674
+ // Sync timezone offset when value changes
7675
+ // Generate timezone offset options (UTC-12 to UTC+14)
7676
+ const timezoneOffsetOptions = useMemo(() => {
7677
+ const options = [];
7678
+ for (let offset = -12; offset <= 14; offset++) {
7679
+ const sign = offset >= 0 ? '+' : '-';
7680
+ const hours = Math.abs(offset).toString().padStart(2, '0');
7681
+ const value = `${sign}${hours}:00`;
7682
+ const label = `UTC${sign}${hours}:00`;
7683
+ options.push({ value, label });
7684
+ }
7685
+ return options;
7686
+ }, []);
7687
+ // Create collection for Select
7688
+ const { collection: timezoneCollection } = useListCollection({
7689
+ initialItems: timezoneOffsetOptions,
7690
+ itemToString: (item) => item.label,
7691
+ itemToValue: (item) => item.value,
7692
+ });
7693
+ // Date input state
7694
+ const [dateInputValue, setDateInputValue] = useState('');
7695
+ // Sync date input value with selected date
7696
+ useEffect(() => {
7697
+ if (selectedDate) {
7698
+ const formatted = dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
7699
+ setDateInputValue(formatted);
7700
+ }
7701
+ else {
7702
+ setDateInputValue('');
7703
+ }
7704
+ }, [selectedDate, tz]);
7705
+ // Parse and validate date input
7706
+ const parseAndValidateDateInput = (inputVal) => {
7707
+ // If empty, clear the value
7708
+ if (!inputVal.trim()) {
7709
+ setSelectedDate(null);
7710
+ updateDateTime(null, hour, minute, second, meridiem);
7234
7711
  return;
7235
7712
  }
7236
- // Parse HH:mm:ss or HH:mm format
7237
- const timePattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/;
7238
- const match = trimmedValue.match(timePattern);
7239
- if (match) {
7240
- const parsedHour = parseInt(match[1], 10);
7241
- const parsedMinute = parseInt(match[2], 10);
7242
- const parsedSecond = match[3] ? parseInt(match[3], 10) : 0;
7243
- // Validate ranges
7244
- if (parsedHour >= 0 &&
7245
- parsedHour <= 23 &&
7246
- parsedMinute >= 0 &&
7247
- parsedMinute <= 59 &&
7248
- parsedSecond >= 0 &&
7249
- parsedSecond <= 59) {
7250
- setHour(parsedHour);
7251
- setMinute(parsedMinute);
7252
- setSecond(parsedSecond);
7253
- onChange({
7254
- hour: parsedHour,
7255
- minute: parsedMinute,
7256
- second: parsedSecond,
7257
- });
7713
+ // Try parsing with common date formats
7714
+ let parsedDate = dayjs(inputVal, 'YYYY-MM-DD', true);
7715
+ // If that fails, try other common formats
7716
+ if (!parsedDate.isValid()) {
7717
+ parsedDate = dayjs(inputVal);
7718
+ }
7719
+ // If valid, check constraints and update
7720
+ if (parsedDate.isValid()) {
7721
+ const dateObj = parsedDate.tz(tz).toDate();
7722
+ // Check min/max constraints
7723
+ if (minDate && dateObj < minDate) {
7724
+ // Invalid: before minDate, reset to current selected date
7725
+ if (selectedDate) {
7726
+ const formatted = dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
7727
+ setDateInputValue(formatted);
7728
+ }
7729
+ else {
7730
+ setDateInputValue('');
7731
+ }
7258
7732
  return;
7259
7733
  }
7260
- }
7261
- else {
7262
- // Try to parse formats like "123045" (HHmmss) or "1230" (HHmm)
7263
- const numbersOnly = trimmedValue.replace(/[^0-9]/g, '');
7264
- if (numbersOnly.length >= 4) {
7265
- const parsedHour = parseInt(numbersOnly.slice(0, 2), 10);
7266
- const parsedMinute = parseInt(numbersOnly.slice(2, 4), 10);
7267
- const parsedSecond = numbersOnly.length >= 6 ? parseInt(numbersOnly.slice(4, 6), 10) : 0;
7268
- // Validate ranges
7269
- if (parsedHour >= 0 &&
7270
- parsedHour <= 23 &&
7271
- parsedMinute >= 0 &&
7272
- parsedMinute <= 59 &&
7273
- parsedSecond >= 0 &&
7274
- parsedSecond <= 59) {
7275
- setHour(parsedHour);
7276
- setMinute(parsedMinute);
7277
- setSecond(parsedSecond);
7278
- onChange({
7279
- hour: parsedHour,
7280
- minute: parsedMinute,
7281
- second: parsedSecond,
7282
- });
7283
- return;
7734
+ if (maxDate && dateObj > maxDate) {
7735
+ // Invalid: after maxDate, reset to current selected date
7736
+ if (selectedDate) {
7737
+ const formatted = dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
7738
+ setDateInputValue(formatted);
7284
7739
  }
7740
+ else {
7741
+ setDateInputValue('');
7742
+ }
7743
+ return;
7285
7744
  }
7286
- }
7287
- // Parse failed, select first result
7288
- selectFirstResult();
7289
- };
7290
- // Select first result from filtered collection
7291
- const selectFirstResult = () => {
7292
- if (collection.items.length > 0) {
7293
- const firstItem = collection.items[0];
7294
- setHour(firstItem.hour);
7295
- setMinute(firstItem.minute);
7296
- setSecond(firstItem.second);
7297
- filter(''); // Reset filter after selection
7298
- onChange({
7299
- hour: firstItem.hour,
7300
- minute: firstItem.minute,
7301
- second: firstItem.second,
7302
- });
7303
- }
7304
- };
7305
- const [inputValue, setInputValue] = useState('');
7306
- // Sync inputValue with currentValue when time changes externally
7307
- useEffect(() => {
7308
- if (hour !== null && minute !== null && second !== null) {
7309
- const formattedValue = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')}`;
7310
- setInputValue(formattedValue);
7745
+ // Valid date - update selected date
7746
+ setSelectedDate(dateObj);
7747
+ updateDateTime(dateObj, hour, minute, second, meridiem);
7748
+ // Format and update input value
7749
+ const formatted = parsedDate.tz(tz).format('YYYY-MM-DD');
7750
+ setDateInputValue(formatted);
7311
7751
  }
7312
7752
  else {
7313
- setInputValue('');
7753
+ // Invalid date - reset to current selected date
7754
+ if (selectedDate) {
7755
+ const formatted = dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
7756
+ setDateInputValue(formatted);
7757
+ }
7758
+ else {
7759
+ setDateInputValue('');
7760
+ }
7314
7761
  }
7315
- }, [hour, minute, second]);
7316
- const handleInputValueChange = (details) => {
7317
- // Update local input value state
7318
- setInputValue(details.inputValue);
7319
- // Filter the collection based on input, but don't parse yet
7320
- filter(details.inputValue);
7321
7762
  };
7322
- const handleFocus = (e) => {
7323
- // Select all text when focusing
7324
- e.target.select();
7763
+ const handleDateInputChange = (e) => {
7764
+ setDateInputValue(e.target.value);
7325
7765
  };
7326
- const handleBlur = (e) => {
7327
- // Parse and commit the input value when losing focus
7328
- const inputVal = e.target.value;
7329
- setInputValue(inputVal);
7330
- if (inputVal) {
7331
- parseAndCommitInput(inputVal);
7332
- }
7766
+ const handleDateInputBlur = () => {
7767
+ parseAndValidateDateInput(dateInputValue);
7333
7768
  };
7334
- const handleKeyDown = (e) => {
7335
- // Commit input on Enter key
7769
+ const handleDateInputKeyDown = (e) => {
7336
7770
  if (e.key === 'Enter') {
7337
7771
  e.preventDefault();
7338
- const inputVal = e.currentTarget.value;
7339
- setInputValue(inputVal);
7340
- if (inputVal) {
7341
- parseAndCommitInput(inputVal);
7342
- }
7343
- // Blur the input
7344
- e.currentTarget?.blur();
7772
+ parseAndValidateDateInput(dateInputValue);
7345
7773
  }
7346
7774
  };
7347
- return (jsx(Flex, { direction: "column", gap: 3, children: jsxs(Flex, { alignItems: "center", gap: "2", width: "auto", minWidth: "300px", children: [jsxs(Combobox.Root, { collection: collection, value: currentValue ? [currentValue] : [], onValueChange: handleValueChange, onInputValueChange: handleInputValueChange, allowCustomValue: true, selectionBehavior: "replace", openOnClick: true, flex: 1, children: [jsxs(Combobox.Control, { children: [jsx(InputGroup$1, { startElement: jsx(BsClock, {}), children: jsx(Combobox.Input, { value: inputValue, placeholder: labels.placeholder, onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown }) }), jsx(Combobox.IndicatorGroup, { children: jsx(Combobox.Trigger, {}) })] }), jsx(Portal, { disabled: !portalled, children: jsx(Combobox.Positioner, { children: jsxs(Combobox.Content, { children: [jsx(Combobox.Empty, { children: labels.emptyMessage }), collection.items.map((item) => (jsxs(Combobox.Item, { item: item, children: [jsxs(Flex, { alignItems: "center", gap: 2, width: "100%", children: [jsx(Text, { flex: 1, children: item.label }), item.durationText && (jsx(Tag$1.Root, { size: "sm", children: jsx(Tag$1.Label, { children: item.durationText }) }))] }), jsx(Combobox.ItemIndicator, {})] }, item.value)))] }) }) })] }), durationDiff && (jsx(Tag$1.Root, { size: "sm", children: jsx(Tag$1.Label, { children: durationDiff }) })), jsx(Button$1, { onClick: handleClear, size: "sm", variant: "ghost", children: jsx(Icon, { children: jsx(MdCancel, {}) }) })] }) }));
7348
- }
7349
-
7350
- dayjs.extend(utc);
7351
- dayjs.extend(timezone);
7352
- function DateTimePicker$1({ value, onChange, format = 'date-time', showSeconds = false, labels = {
7353
- monthNamesShort: [
7354
- 'Jan',
7355
- 'Feb',
7356
- 'Mar',
7357
- 'Apr',
7358
- 'May',
7359
- 'Jun',
7360
- 'Jul',
7361
- 'Aug',
7362
- 'Sep',
7363
- 'Oct',
7364
- 'Nov',
7365
- 'Dec',
7366
- ],
7367
- weekdayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
7368
- backButtonLabel: 'Back',
7369
- forwardButtonLabel: 'Next',
7370
- }, timePickerLabels, timezone = 'Asia/Hong_Kong', startTime, minDate, maxDate, portalled = false, defaultDate, defaultTime, }) {
7371
- console.log('[DateTimePicker] Component initialized with props:', {
7372
- value,
7373
- format,
7374
- showSeconds,
7375
- timezone,
7376
- startTime,
7377
- minDate,
7378
- maxDate,
7379
- });
7380
- // Initialize selectedDate from value prop, converting ISO to YYYY-MM-DD format
7381
- const getDateString = useCallback((val) => {
7382
- if (!val)
7383
- return '';
7384
- const dateObj = dayjs(val).tz(timezone);
7385
- return dateObj.isValid() ? dateObj.format('YYYY-MM-DD') : '';
7386
- }, [timezone]);
7387
- const [selectedDate, setSelectedDate] = useState(getDateString(value));
7388
- // Helper to get time values from value prop with timezone
7389
- const getTimeFromValue = useCallback((val) => {
7390
- console.log('[DateTimePicker] getTimeFromValue called:', {
7391
- val,
7392
- timezone,
7393
- showSeconds,
7394
- });
7395
- if (!val) {
7396
- console.log('[DateTimePicker] No value provided, returning nulls');
7397
- return {
7398
- hour12: null,
7399
- minute: null,
7400
- meridiem: null,
7401
- hour24: null,
7402
- second: null,
7403
- };
7404
- }
7405
- const dateObj = dayjs(val).tz(timezone);
7406
- console.log('[DateTimePicker] Parsed date object:', {
7407
- original: val,
7408
- timezone,
7409
- isValid: dateObj.isValid(),
7410
- formatted: dateObj.format('YYYY-MM-DD HH:mm:ss Z'),
7411
- hour24: dateObj.hour(),
7412
- minute: dateObj.minute(),
7413
- second: dateObj.second(),
7414
- });
7415
- if (!dateObj.isValid()) {
7416
- console.log('[DateTimePicker] Invalid date object, returning nulls');
7417
- return {
7418
- hour12: null,
7419
- minute: null,
7420
- meridiem: null,
7421
- hour24: null,
7422
- second: null,
7423
- };
7424
- }
7425
- const hour24Value = dateObj.hour();
7426
- const hour12Value = hour24Value % 12 || 12;
7427
- const minuteValue = dateObj.minute();
7428
- const meridiemValue = hour24Value >= 12 ? 'pm' : 'am';
7429
- const secondValue = showSeconds ? dateObj.second() : null;
7430
- const result = {
7431
- hour12: hour12Value,
7432
- minute: minuteValue,
7433
- meridiem: meridiemValue,
7434
- hour24: hour24Value,
7435
- second: secondValue,
7436
- };
7437
- console.log('[DateTimePicker] Extracted time values:', result);
7438
- return result;
7439
- }, [timezone, showSeconds]);
7440
- const initialTime = getTimeFromValue(value);
7441
- console.log('[DateTimePicker] Initial time from value:', {
7442
- value,
7443
- initialTime,
7444
- });
7445
- // Normalize startTime to ignore milliseconds (needed for effectiveDefaultDate calculation)
7446
- const normalizedStartTime = startTime
7447
- ? dayjs(startTime).tz(timezone).millisecond(0).toISOString()
7448
- : undefined;
7449
- // Calculate effective defaultDate: use prop if provided, otherwise use startTime date, otherwise use today
7450
- const effectiveDefaultDate = useMemo(() => {
7451
- if (defaultDate) {
7452
- return defaultDate;
7453
- }
7454
- if (normalizedStartTime &&
7455
- dayjs(normalizedStartTime).tz(timezone).isValid()) {
7456
- return dayjs(normalizedStartTime).tz(timezone).format('YYYY-MM-DD');
7457
- }
7458
- return dayjs().tz(timezone).format('YYYY-MM-DD');
7459
- }, [defaultDate, normalizedStartTime, timezone]);
7460
- // Initialize time with default values if no value is provided
7461
- const getInitialTimeValues = () => {
7462
- if (value && initialTime.hour12 !== null) {
7463
- return initialTime;
7464
- }
7465
- // If no value or no time in value, use defaultTime or 00:00
7466
- if (defaultTime) {
7467
- if (format === 'iso-date-time') {
7468
- const defaultTime24 = defaultTime;
7469
- return {
7470
- hour12: null,
7471
- minute: defaultTime24.minute ?? 0,
7472
- meridiem: null,
7473
- hour24: defaultTime24.hour ?? 0,
7474
- second: showSeconds ? defaultTime24.second ?? 0 : null,
7475
- };
7476
- }
7477
- else {
7478
- const defaultTime12 = defaultTime;
7479
- return {
7480
- hour12: defaultTime12.hour ?? 12,
7481
- minute: defaultTime12.minute ?? 0,
7482
- meridiem: defaultTime12.meridiem ?? 'am',
7483
- hour24: null,
7484
- second: null,
7485
- };
7486
- }
7487
- }
7488
- // Default to 00:00
7489
- if (format === 'iso-date-time') {
7490
- return {
7491
- hour12: null,
7492
- minute: 0,
7493
- meridiem: null,
7494
- hour24: 0,
7495
- second: showSeconds ? 0 : null,
7496
- };
7775
+ // Helper functions to get dates in the correct timezone
7776
+ const getToday = () => dayjs().tz(tz).startOf('day').toDate();
7777
+ const getYesterday = () => dayjs().tz(tz).subtract(1, 'day').startOf('day').toDate();
7778
+ const getTomorrow = () => dayjs().tz(tz).add(1, 'day').startOf('day').toDate();
7779
+ const getPlus7Days = () => dayjs().tz(tz).add(7, 'day').startOf('day').toDate();
7780
+ // Check if a date is within min/max constraints
7781
+ const isDateValid = (date) => {
7782
+ if (minDate) {
7783
+ const minDateStart = dayjs(minDate).tz(tz).startOf('day').toDate();
7784
+ const dateStart = dayjs(date).tz(tz).startOf('day').toDate();
7785
+ if (dateStart < minDateStart)
7786
+ return false;
7497
7787
  }
7498
- else {
7499
- return {
7500
- hour12: 12,
7501
- minute: 0,
7502
- meridiem: 'am',
7503
- hour24: null,
7504
- second: null,
7505
- };
7788
+ if (maxDate) {
7789
+ const maxDateStart = dayjs(maxDate).tz(tz).startOf('day').toDate();
7790
+ const dateStart = dayjs(date).tz(tz).startOf('day').toDate();
7791
+ if (dateStart > maxDateStart)
7792
+ return false;
7506
7793
  }
7794
+ return true;
7507
7795
  };
7508
- const initialTimeValues = getInitialTimeValues();
7509
- // Time state for 12-hour format
7510
- const [hour12, setHour12] = useState(initialTimeValues.hour12);
7511
- const [minute, setMinute] = useState(initialTimeValues.minute);
7512
- const [meridiem, setMeridiem] = useState(initialTimeValues.meridiem);
7513
- // Time state for 24-hour format
7514
- const [hour24, setHour24] = useState(initialTimeValues.hour24);
7515
- const [second, setSecond] = useState(initialTimeValues.second);
7516
- // Sync selectedDate and time states when value prop changes
7517
- useEffect(() => {
7518
- console.log('[DateTimePicker] useEffect triggered - value changed:', {
7519
- value,
7520
- timezone,
7521
- format,
7522
- });
7523
- // If value is null, undefined, or invalid, clear date but keep default time values
7524
- if (!value || value === null || value === undefined) {
7525
- console.log('[DateTimePicker] Value is null/undefined, clearing date but keeping default time');
7526
- setSelectedDate('');
7527
- // Keep default time values instead of clearing them
7528
- if (format === 'iso-date-time') {
7529
- setHour24(defaultTime ? defaultTime.hour ?? 0 : 0);
7530
- setMinute(defaultTime ? defaultTime.minute ?? 0 : 0);
7531
- setSecond(showSeconds
7532
- ? defaultTime
7533
- ? defaultTime.second ?? 0
7534
- : 0
7535
- : null);
7536
- }
7537
- else {
7538
- setHour12(defaultTime ? defaultTime.hour ?? 12 : 12);
7539
- setMinute(defaultTime ? defaultTime.minute ?? 0 : 0);
7540
- setMeridiem(defaultTime ? defaultTime.meridiem ?? 'am' : 'am');
7541
- }
7542
- return;
7796
+ // Handle quick action button clicks
7797
+ const handleQuickActionClick = (date) => {
7798
+ if (isDateValid(date)) {
7799
+ setSelectedDate(date);
7800
+ updateDateTime(date, hour, minute, second, meridiem);
7801
+ // Close the calendar popover if open
7802
+ setCalendarPopoverOpen(false);
7543
7803
  }
7544
- // Check if value is valid
7545
- const dateObj = dayjs(value).tz(timezone);
7546
- if (!dateObj.isValid()) {
7547
- console.log('[DateTimePicker] Invalid value, clearing date but keeping default time');
7548
- setSelectedDate('');
7549
- // Keep default time values instead of clearing them
7550
- if (format === 'iso-date-time') {
7551
- setHour24(defaultTime ? defaultTime.hour ?? 0 : 0);
7552
- setMinute(defaultTime ? defaultTime.minute ?? 0 : 0);
7553
- setSecond(showSeconds
7554
- ? defaultTime
7555
- ? defaultTime.second ?? 0
7556
- : 0
7557
- : null);
7558
- }
7559
- else {
7560
- setHour12(defaultTime ? defaultTime.hour ?? 12 : 12);
7561
- setMinute(defaultTime ? defaultTime.minute ?? 0 : 0);
7562
- setMeridiem(defaultTime ? defaultTime.meridiem ?? 'am' : 'am');
7804
+ };
7805
+ // Display text for buttons
7806
+ const dateDisplayText = useMemo(() => {
7807
+ if (!selectedDate)
7808
+ return 'Select date';
7809
+ return dayjs(selectedDate).tz(tz).format('YYYY-MM-DD');
7810
+ }, [selectedDate, tz]);
7811
+ const timeDisplayText = useMemo(() => {
7812
+ if (hour === null || minute === null)
7813
+ return 'Select time';
7814
+ if (is24Hour) {
7815
+ // 24-hour format: never show meridiem, always use 24-hour format (0-23)
7816
+ const hour24 = hour >= 0 && hour <= 23 ? hour : hour % 24;
7817
+ const s = second ?? 0;
7818
+ if (showSeconds) {
7819
+ return `${hour24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
7563
7820
  }
7564
- return;
7821
+ return `${hour24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
7565
7822
  }
7566
- const dateString = getDateString(value);
7567
- console.log('[DateTimePicker] Setting selectedDate:', dateString);
7568
- setSelectedDate(dateString);
7569
- const timeData = getTimeFromValue(value);
7570
- console.log('[DateTimePicker] Updating time states:', {
7571
- timeData,
7572
- });
7573
- setHour12(timeData.hour12);
7574
- setMinute(timeData.minute);
7575
- setMeridiem(timeData.meridiem);
7576
- setHour24(timeData.hour24);
7577
- setSecond(timeData.second);
7578
- }, [value, getTimeFromValue, getDateString, timezone]);
7579
- const handleDateChange = (date) => {
7580
- console.log('[DateTimePicker] handleDateChange called:', {
7581
- date,
7582
- timezone,
7583
- showSeconds,
7584
- currentTimeStates: { hour12, minute, meridiem, hour24, second },
7585
- });
7586
- // If date is empty or invalid, clear all fields
7587
- if (!date || date === '') {
7588
- console.log('[DateTimePicker] Empty date, clearing all fields');
7589
- setSelectedDate('');
7590
- setHour12(null);
7591
- setMinute(null);
7592
- setMeridiem(null);
7593
- setHour24(null);
7594
- setSecond(null);
7595
- onChange?.(undefined);
7596
- return;
7597
- }
7598
- setSelectedDate(date);
7599
- // Parse the date string (YYYY-MM-DD) in the specified timezone
7600
- const dateObj = dayjs.tz(date, timezone);
7601
- console.log('[DateTimePicker] Parsed date object:', {
7602
- date,
7603
- timezone,
7604
- isValid: dateObj.isValid(),
7605
- isoString: dateObj.toISOString(),
7606
- formatted: dateObj.format('YYYY-MM-DD HH:mm:ss Z'),
7607
- });
7608
- if (!dateObj.isValid()) {
7609
- console.warn('[DateTimePicker] Invalid date object in handleDateChange, clearing fields');
7610
- setSelectedDate('');
7611
- setHour12(null);
7612
- setMinute(null);
7613
- setMeridiem(null);
7614
- setHour24(null);
7615
- setSecond(null);
7823
+ else {
7824
+ // 12-hour format: always show meridiem (AM/PM)
7825
+ const hour12 = hour >= 1 && hour <= 12 ? hour : hour % 12;
7826
+ if (meridiem === null)
7827
+ return 'Select time';
7828
+ const hourDisplay = hour12.toString();
7829
+ const minuteDisplay = minute.toString().padStart(2, '0');
7830
+ return `${hourDisplay}:${minuteDisplay} ${meridiem.toUpperCase()}`;
7831
+ }
7832
+ }, [hour, minute, second, meridiem, is24Hour, showSeconds]);
7833
+ const timezoneDisplayText = useMemo(() => {
7834
+ if (!showTimezoneSelector)
7835
+ return '';
7836
+ // Show offset as is (e.g., "+08:00")
7837
+ return timezoneOffset;
7838
+ }, [timezoneOffset, showTimezoneSelector]);
7839
+ // Update selectedDate when value changes externally
7840
+ useEffect(() => {
7841
+ if (parsedValue) {
7842
+ setSelectedDate(parsedValue.toDate());
7843
+ setHour(parsedValue.hour());
7844
+ setMinute(parsedValue.minute());
7845
+ setSecond(parsedValue.second());
7846
+ if (!is24Hour) {
7847
+ const h = parsedValue.hour();
7848
+ setMeridiem(h < 12 ? 'am' : 'pm');
7849
+ }
7850
+ }
7851
+ }, [parsedValue, is24Hour]);
7852
+ // Combine date and time and call onChange
7853
+ const updateDateTime = (newDate, newHour, newMinute, newSecond, newMeridiem, timezoneOffsetOverride) => {
7854
+ if (!newDate || newHour === null || newMinute === null) {
7616
7855
  onChange?.(undefined);
7617
7856
  return;
7618
7857
  }
7619
- // Check if time values are null - if so, use defaultTime or set to 00:00
7620
- const hasTimeValues = format === 'iso-date-time'
7621
- ? hour24 !== null || minute !== null
7622
- : hour12 !== null || minute !== null || meridiem !== null;
7623
- let timeDataToUse = undefined;
7624
- if (!hasTimeValues) {
7625
- // Use defaultTime if provided, otherwise default to 00:00
7626
- if (defaultTime) {
7627
- console.log('[DateTimePicker] No time values set, using defaultTime');
7628
- if (format === 'iso-date-time') {
7629
- const defaultTime24 = defaultTime;
7630
- setHour24(defaultTime24.hour ?? 0);
7631
- setMinute(defaultTime24.minute ?? 0);
7632
- if (showSeconds) {
7633
- setSecond(defaultTime24.second ?? 0);
7634
- }
7635
- timeDataToUse = {
7636
- hour: defaultTime24.hour ?? 0,
7637
- minute: defaultTime24.minute ?? 0,
7638
- second: showSeconds ? defaultTime24.second ?? 0 : undefined,
7639
- };
7858
+ // Convert 12-hour to 24-hour if needed
7859
+ let hour24 = newHour;
7860
+ if (!is24Hour && newMeridiem) {
7861
+ // In 12-hour format, hour should be 1-12
7862
+ // If hour is > 12, it might already be in 24-hour format, convert it first
7863
+ let hour12 = newHour;
7864
+ if (newHour > 12) {
7865
+ // Hour is in 24-hour format, convert to 12-hour first
7866
+ if (newHour === 12) {
7867
+ hour12 = 12;
7640
7868
  }
7641
7869
  else {
7642
- const defaultTime12 = defaultTime;
7643
- setHour12(defaultTime12.hour ?? 12);
7644
- setMinute(defaultTime12.minute ?? 0);
7645
- setMeridiem(defaultTime12.meridiem ?? 'am');
7646
- timeDataToUse = {
7647
- hour: defaultTime12.hour ?? 12,
7648
- minute: defaultTime12.minute ?? 0,
7649
- meridiem: defaultTime12.meridiem ?? 'am',
7650
- };
7870
+ hour12 = newHour - 12;
7871
+ }
7872
+ }
7873
+ // Now convert 12-hour to 24-hour format (0-23)
7874
+ if (newMeridiem === 'am') {
7875
+ if (hour12 === 12) {
7876
+ hour24 = 0; // 12 AM = 0:00
7877
+ }
7878
+ else {
7879
+ hour24 = hour12; // 1-11 AM = 1-11
7651
7880
  }
7652
7881
  }
7653
7882
  else {
7654
- console.log('[DateTimePicker] No time values set, defaulting to 00:00');
7655
- if (format === 'iso-date-time') {
7656
- setHour24(0);
7657
- setMinute(0);
7658
- if (showSeconds) {
7659
- setSecond(0);
7660
- }
7661
- timeDataToUse = {
7662
- hour: 0,
7663
- minute: 0,
7664
- second: showSeconds ? 0 : undefined,
7665
- };
7883
+ // PM
7884
+ if (hour12 === 12) {
7885
+ hour24 = 12; // 12 PM = 12:00
7666
7886
  }
7667
7887
  else {
7668
- setHour12(12);
7669
- setMinute(0);
7670
- setMeridiem('am');
7671
- timeDataToUse = {
7672
- hour: 12,
7673
- minute: 0,
7674
- meridiem: 'am',
7675
- };
7888
+ hour24 = hour12 + 12; // 1-11 PM = 13-23
7676
7889
  }
7677
7890
  }
7678
7891
  }
7679
- // When showSeconds is false, ignore seconds from the date
7680
- if (!showSeconds) {
7681
- const dateWithoutSeconds = dateObj.second(0).millisecond(0).toISOString();
7682
- console.log('[DateTimePicker] Updating date without seconds:', dateWithoutSeconds);
7683
- updateDateTime(dateWithoutSeconds, timeDataToUse);
7892
+ else if (!is24Hour && !newMeridiem) {
7893
+ // If in 12-hour mode but no meridiem, assume the hour is already in 12-hour format
7894
+ // and default to AM (or keep as is if it's a valid 12-hour value)
7895
+ // This shouldn't happen in normal flow, but handle it gracefully
7896
+ hour24 = newHour;
7897
+ }
7898
+ // If timezone selector is enabled, create date-time without timezone conversion
7899
+ // to ensure the selected timestamp matches the picker values exactly
7900
+ if (showTimezoneSelector) {
7901
+ // Use override if provided, otherwise use state value
7902
+ const offsetToUse = timezoneOffsetOverride ?? timezoneOffset;
7903
+ // Create date-time from the Date object without timezone conversion
7904
+ // Extract year, month, day from the date
7905
+ const year = newDate.getFullYear();
7906
+ const month = newDate.getMonth();
7907
+ const day = newDate.getDate();
7908
+ // Create a date-time string with the exact values from the picker
7909
+ const formattedDateTime = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(hour24).padStart(2, '0')}:${String(newMinute).padStart(2, '0')}:${String(newSecond ?? 0).padStart(2, '0')}`;
7910
+ onChange?.(`${formattedDateTime}${offsetToUse}`);
7911
+ return;
7684
7912
  }
7685
- else {
7686
- const dateWithSeconds = dateObj.toISOString();
7687
- console.log('[DateTimePicker] Updating date with seconds:', dateWithSeconds);
7688
- updateDateTime(dateWithSeconds, timeDataToUse);
7913
+ // Normal mode: use timezone conversion
7914
+ let dateTime = dayjs(newDate)
7915
+ .tz(tz)
7916
+ .hour(hour24)
7917
+ .minute(newMinute)
7918
+ .second(newSecond ?? 0)
7919
+ .millisecond(0);
7920
+ if (!dateTime.isValid()) {
7921
+ onChange?.(undefined);
7922
+ return;
7689
7923
  }
7690
- };
7691
- const handleTimeChange = (timeData) => {
7692
- console.log('[DateTimePicker] handleTimeChange called:', {
7693
- timeData,
7694
- format,
7695
- selectedDate,
7696
- timezone,
7697
- });
7924
+ // Format based on format prop
7698
7925
  if (format === 'iso-date-time') {
7699
- const data = timeData;
7700
- console.log('[DateTimePicker] ISO format - setting 24-hour time:', data);
7701
- setHour24(data.hour);
7702
- setMinute(data.minute);
7703
- if (showSeconds) {
7704
- setSecond(data.second ?? null);
7705
- }
7706
- else {
7707
- // Ignore seconds - always set to null when showSeconds is false
7708
- setSecond(null);
7709
- }
7926
+ onChange?.(dateTime.format('YYYY-MM-DDTHH:mm:ss'));
7710
7927
  }
7711
7928
  else {
7712
- const data = timeData;
7713
- console.log('[DateTimePicker] 12-hour format - setting time:', data);
7714
- setHour12(data.hour);
7715
- setMinute(data.minute);
7716
- setMeridiem(data.meridiem);
7717
- }
7718
- // Use selectedDate if valid, otherwise use effectiveDefaultDate or clear all fields
7719
- if (!selectedDate || !dayjs(selectedDate).isValid()) {
7720
- // If effectiveDefaultDate is available, use it instead of clearing
7721
- if (effectiveDefaultDate && dayjs(effectiveDefaultDate).isValid()) {
7722
- console.log('[DateTimePicker] No valid selectedDate, using effectiveDefaultDate:', effectiveDefaultDate);
7723
- setSelectedDate(effectiveDefaultDate);
7724
- const dateObj = dayjs(effectiveDefaultDate).tz(timezone);
7725
- if (dateObj.isValid()) {
7726
- updateDateTime(dateObj.toISOString(), timeData);
7727
- }
7728
- else {
7729
- console.warn('[DateTimePicker] Invalid effectiveDefaultDate, clearing fields');
7730
- setSelectedDate('');
7731
- setHour12(null);
7732
- setMinute(null);
7733
- setMeridiem(null);
7734
- setHour24(null);
7735
- setSecond(null);
7736
- onChange?.(undefined);
7737
- }
7738
- return;
7739
- }
7740
- else {
7741
- console.log('[DateTimePicker] No valid selectedDate and no effectiveDefaultDate, keeping time values but no date');
7742
- // Keep the time values that were just set, but don't set a date
7743
- // This should rarely happen as effectiveDefaultDate always defaults to today
7744
- setSelectedDate('');
7745
- onChange?.(undefined);
7746
- return;
7747
- }
7929
+ // date-time format with timezone
7930
+ onChange?.(dateTime.format('YYYY-MM-DDTHH:mm:ssZ'));
7748
7931
  }
7749
- const dateObj = dayjs(selectedDate).tz(timezone);
7750
- if (dateObj.isValid()) {
7751
- updateDateTime(dateObj.toISOString(), timeData);
7932
+ };
7933
+ // Handle date selection
7934
+ const handleDateSelected = ({ date, }) => {
7935
+ setSelectedDate(date);
7936
+ updateDateTime(date, hour, minute, second, meridiem);
7937
+ };
7938
+ // Handle time change
7939
+ const handleTimeChange = (newHour, newMinute, newSecond, newMeridiem) => {
7940
+ setHour(newHour);
7941
+ setMinute(newMinute);
7942
+ if (is24Hour) {
7943
+ setSecond(newSecond);
7752
7944
  }
7753
7945
  else {
7754
- console.warn('[DateTimePicker] Invalid date object in handleTimeChange, clearing fields');
7755
- setSelectedDate('');
7756
- setHour12(null);
7757
- setMinute(null);
7758
- setMeridiem(null);
7759
- setHour24(null);
7760
- setSecond(null);
7761
- onChange?.(undefined);
7946
+ setMeridiem(newMeridiem);
7762
7947
  }
7763
- };
7764
- const updateDateTime = (date, timeData) => {
7765
- console.log('[DateTimePicker] updateDateTime called:', {
7766
- date,
7767
- timeData,
7768
- format,
7769
- currentStates: { hour12, minute, meridiem, hour24, second },
7770
- });
7771
- if (!date || date === null || date === undefined) {
7772
- console.log('[DateTimePicker] No date provided, clearing all fields and calling onChange(undefined)');
7773
- setSelectedDate('');
7774
- setHour12(null);
7775
- setMinute(null);
7776
- setMeridiem(null);
7777
- setHour24(null);
7778
- setSecond(null);
7779
- onChange?.(undefined);
7780
- return;
7948
+ if (selectedDate) {
7949
+ updateDateTime(selectedDate, newHour, newMinute, newSecond, newMeridiem);
7781
7950
  }
7782
- // use dayjs to convert the date to the timezone
7783
- const dateObj = dayjs(date).tz(timezone);
7784
- if (!dateObj.isValid()) {
7785
- console.warn('[DateTimePicker] Invalid date object in updateDateTime, clearing fields:', date);
7786
- setSelectedDate('');
7787
- setHour12(null);
7788
- setMinute(null);
7789
- setMeridiem(null);
7790
- setHour24(null);
7791
- setSecond(null);
7792
- onChange?.(undefined);
7793
- return;
7951
+ };
7952
+ // Calendar hook
7953
+ const calendarProps = useCalendar({
7954
+ selected: selectedDate || undefined,
7955
+ date: selectedDate || undefined,
7956
+ minDate,
7957
+ maxDate,
7958
+ monthsToDisplay: 1,
7959
+ onDateSelected: handleDateSelected,
7960
+ });
7961
+ // Generate time options
7962
+ const timeOptions = useMemo(() => {
7963
+ const options = [];
7964
+ // Get start time for comparison if provided
7965
+ let startDateTime = null;
7966
+ let shouldFilterByDate = false;
7967
+ if (startTime && selectedDate) {
7968
+ const startDateObj = dayjs(startTime).tz(tz);
7969
+ const selectedDateObj = dayjs(selectedDate).tz(tz);
7970
+ if (startDateObj.isValid() && selectedDateObj.isValid()) {
7971
+ startDateTime = startDateObj;
7972
+ shouldFilterByDate =
7973
+ startDateObj.format('YYYY-MM-DD') ===
7974
+ selectedDateObj.format('YYYY-MM-DD');
7975
+ }
7794
7976
  }
7795
- const newDate = dateObj.toDate();
7796
- if (format === 'iso-date-time') {
7797
- const data = timeData;
7798
- // Use timeData values if provided, otherwise fall back to current state
7799
- // But if timeData is explicitly provided with nulls, we need to check if all are null
7800
- const h = data !== undefined ? data.hour : hour24;
7801
- const m = data !== undefined ? data.minute : minute;
7802
- // Always ignore seconds when showSeconds is false - set to 0
7803
- const s = showSeconds
7804
- ? data !== undefined
7805
- ? data.second ?? null
7806
- : second ?? 0
7807
- : 0;
7808
- // If all time values are null, clear the value
7809
- if (h === null && m === null && (showSeconds ? s === null : true)) {
7810
- console.log('[DateTimePicker] All time values are null, clearing value');
7811
- onChange?.(undefined);
7812
- return;
7977
+ if (is24Hour) {
7978
+ // Generate 24-hour format options
7979
+ for (let h = 0; h < 24; h++) {
7980
+ for (let m = 0; m < 60; m += 15) {
7981
+ // Filter out times that would result in negative duration
7982
+ if (startDateTime && selectedDate && shouldFilterByDate) {
7983
+ const selectedDateObj = dayjs(selectedDate).tz(tz);
7984
+ const optionDateTime = selectedDateObj
7985
+ .hour(h)
7986
+ .minute(m)
7987
+ .second(0)
7988
+ .millisecond(0);
7989
+ if (optionDateTime.isBefore(startDateTime)) {
7990
+ continue;
7991
+ }
7992
+ }
7993
+ // Calculate duration if startTime is provided
7994
+ let durationText;
7995
+ if (startDateTime && selectedDate) {
7996
+ const selectedDateObj = dayjs(selectedDate).tz(tz);
7997
+ const optionDateTime = selectedDateObj
7998
+ .hour(h)
7999
+ .minute(m)
8000
+ .second(0)
8001
+ .millisecond(0);
8002
+ if (optionDateTime.isValid() &&
8003
+ optionDateTime.isAfter(startDateTime)) {
8004
+ const diffMs = optionDateTime.diff(startDateTime);
8005
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
8006
+ const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
8007
+ const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
8008
+ if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
8009
+ let diffText = '';
8010
+ if (diffHours > 0) {
8011
+ diffText = `${diffHours}h ${diffMinutes}m`;
8012
+ }
8013
+ else if (diffMinutes > 0) {
8014
+ diffText = `${diffMinutes}m ${diffSeconds}s`;
8015
+ }
8016
+ else {
8017
+ diffText = `${diffSeconds}s`;
8018
+ }
8019
+ durationText = `+${diffText}`;
8020
+ }
8021
+ }
8022
+ }
8023
+ const s = showSeconds ? 0 : 0;
8024
+ const timeDisplay = showSeconds
8025
+ ? `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:00`
8026
+ : `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
8027
+ options.push({
8028
+ label: timeDisplay,
8029
+ value: `${h}:${m}:${s}`,
8030
+ hour: h,
8031
+ minute: m,
8032
+ second: s,
8033
+ searchText: timeDisplay,
8034
+ durationText,
8035
+ });
8036
+ }
7813
8037
  }
7814
- console.log('[DateTimePicker] ISO format - setting time on date:', {
7815
- h,
7816
- m,
7817
- s,
7818
- showSeconds,
7819
- });
7820
- if (h !== null)
7821
- newDate.setHours(h);
7822
- if (m !== null)
7823
- newDate.setMinutes(m);
7824
- newDate.setSeconds(s ?? 0);
7825
8038
  }
7826
8039
  else {
7827
- const data = timeData;
7828
- console.log('[DateTimePicker] Processing 12-hour format:', {
7829
- 'data !== undefined': data !== undefined,
7830
- 'data?.hour': data?.hour,
7831
- 'data?.minute': data?.minute,
7832
- 'data?.meridiem': data?.meridiem,
7833
- 'current hour12': hour12,
7834
- 'current minute': minute,
7835
- 'current meridiem': meridiem,
7836
- });
7837
- // Use timeData values if provided, otherwise fall back to current state
7838
- const h = data !== undefined ? data.hour : hour12;
7839
- const m = data !== undefined ? data.minute : minute;
7840
- const mer = data !== undefined ? data.meridiem : meridiem;
7841
- console.log('[DateTimePicker] Resolved time values:', { h, m, mer });
7842
- // If all time values are null, clear the value
7843
- if (h === null && m === null && mer === null) {
7844
- console.log('[DateTimePicker] All time values are null, clearing value');
7845
- onChange?.(undefined);
7846
- return;
8040
+ // Generate 12-hour format options
8041
+ for (let h = 1; h <= 12; h++) {
8042
+ for (let m = 0; m < 60; m += 15) {
8043
+ for (const mer of ['am', 'pm']) {
8044
+ // Convert 12-hour to 24-hour for comparison
8045
+ let hour24 = h;
8046
+ if (mer === 'am' && h === 12)
8047
+ hour24 = 0;
8048
+ else if (mer === 'pm' && h < 12)
8049
+ hour24 = h + 12;
8050
+ // Filter out times that would result in negative duration
8051
+ if (startDateTime && selectedDate && shouldFilterByDate) {
8052
+ const selectedDateObj = dayjs(selectedDate).tz(tz);
8053
+ const optionDateTime = selectedDateObj
8054
+ .hour(hour24)
8055
+ .minute(m)
8056
+ .second(0)
8057
+ .millisecond(0);
8058
+ if (optionDateTime.isBefore(startDateTime)) {
8059
+ continue;
8060
+ }
8061
+ }
8062
+ // Calculate duration if startTime is provided
8063
+ let durationText;
8064
+ if (startDateTime && selectedDate) {
8065
+ const selectedDateObj = dayjs(selectedDate).tz(tz);
8066
+ const optionDateTime = selectedDateObj
8067
+ .hour(hour24)
8068
+ .minute(m)
8069
+ .second(0)
8070
+ .millisecond(0);
8071
+ if (optionDateTime.isValid() &&
8072
+ optionDateTime.isAfter(startDateTime)) {
8073
+ const diffMs = optionDateTime.diff(startDateTime);
8074
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
8075
+ const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
8076
+ const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
8077
+ if (diffHours > 0 || diffMinutes > 0 || diffSeconds > 0) {
8078
+ let diffText = '';
8079
+ if (diffHours > 0) {
8080
+ diffText = `${diffHours}h ${diffMinutes}m`;
8081
+ }
8082
+ else if (diffMinutes > 0) {
8083
+ diffText = `${diffMinutes}m ${diffSeconds}s`;
8084
+ }
8085
+ else {
8086
+ diffText = `${diffSeconds}s`;
8087
+ }
8088
+ durationText = `+${diffText}`;
8089
+ }
8090
+ }
8091
+ }
8092
+ const hourDisplay = h.toString();
8093
+ const minuteDisplay = m.toString().padStart(2, '0');
8094
+ const timeDisplay = `${hourDisplay}:${minuteDisplay} ${mer.toUpperCase()}`;
8095
+ options.push({
8096
+ label: timeDisplay,
8097
+ value: `${h}:${m}:${mer}`,
8098
+ hour: h,
8099
+ minute: m,
8100
+ meridiem: mer,
8101
+ searchText: timeDisplay,
8102
+ durationText,
8103
+ });
8104
+ }
8105
+ }
7847
8106
  }
7848
- console.log('[DateTimePicker] 12-hour format - converting time:', {
7849
- h,
7850
- m,
7851
- mer,
8107
+ // Sort 12-hour options by time
8108
+ return options.sort((a, b) => {
8109
+ const a12 = a;
8110
+ const b12 = b;
8111
+ let hour24A = a12.hour;
8112
+ if (a12.meridiem === 'am' && a12.hour === 12)
8113
+ hour24A = 0;
8114
+ else if (a12.meridiem === 'pm' && a12.hour < 12)
8115
+ hour24A = a12.hour + 12;
8116
+ let hour24B = b12.hour;
8117
+ if (b12.meridiem === 'am' && b12.hour === 12)
8118
+ hour24B = 0;
8119
+ else if (b12.meridiem === 'pm' && b12.hour < 12)
8120
+ hour24B = b12.hour + 12;
8121
+ if (hour24A !== hour24B) {
8122
+ return hour24A - hour24B;
8123
+ }
8124
+ return a12.minute - b12.minute;
7852
8125
  });
7853
- if (h !== null && mer !== null) {
7854
- let hour24 = h;
7855
- if (mer === 'am' && h === 12)
7856
- hour24 = 0;
7857
- else if (mer === 'pm' && h < 12)
7858
- hour24 = h + 12;
7859
- console.log('[DateTimePicker] Converted to 24-hour:', {
7860
- h,
7861
- mer,
7862
- hour24,
7863
- });
7864
- newDate.setHours(hour24);
8126
+ }
8127
+ return options;
8128
+ }, [startTime, selectedDate, tz, is24Hour, showSeconds]);
8129
+ // Time picker combobox setup
8130
+ const itemToString = useMemo(() => {
8131
+ return (item) => {
8132
+ return item.searchText;
8133
+ };
8134
+ }, []);
8135
+ const { contains } = useFilter({ sensitivity: 'base' });
8136
+ const customTimeFilter = useMemo(() => {
8137
+ if (is24Hour) {
8138
+ return contains;
8139
+ }
8140
+ return (itemText, filterText) => {
8141
+ if (!filterText) {
8142
+ return true;
7865
8143
  }
7866
- else {
7867
- console.log('[DateTimePicker] Skipping hour update - h or mer is null:', {
7868
- h,
7869
- mer,
7870
- });
8144
+ const lowerItemText = itemText.toLowerCase();
8145
+ const lowerFilterText = filterText.toLowerCase();
8146
+ if (lowerItemText.includes(lowerFilterText)) {
8147
+ return true;
7871
8148
  }
7872
- if (m !== null) {
7873
- newDate.setMinutes(m);
8149
+ const item = timeOptions.find((opt) => opt.searchText.toLowerCase() === lowerItemText);
8150
+ if (!item || !('meridiem' in item)) {
8151
+ return false;
7874
8152
  }
7875
- else {
7876
- console.log('[DateTimePicker] Skipping minute update - m is null');
8153
+ let hour24 = item.hour;
8154
+ if (item.meridiem === 'am' && item.hour === 12)
8155
+ hour24 = 0;
8156
+ else if (item.meridiem === 'pm' && item.hour < 12)
8157
+ hour24 = item.hour + 12;
8158
+ const hour24Str = hour24.toString().padStart(2, '0');
8159
+ const minuteStr = item.minute.toString().padStart(2, '0');
8160
+ const formats = [
8161
+ `${hour24Str}:${minuteStr}`,
8162
+ `${hour24Str}${minuteStr}`,
8163
+ hour24Str,
8164
+ `${hour24}:${minuteStr}`,
8165
+ hour24.toString(),
8166
+ ];
8167
+ return formats.some((format) => format.toLowerCase().includes(lowerFilterText) ||
8168
+ lowerFilterText.includes(format.toLowerCase()));
8169
+ };
8170
+ }, [timeOptions, is24Hour, contains]);
8171
+ const { collection, filter } = useListCollection({
8172
+ initialItems: timeOptions,
8173
+ itemToString: itemToString,
8174
+ itemToValue: (item) => item.value,
8175
+ filter: customTimeFilter,
8176
+ });
8177
+ // Get current value string for combobox (must match option.value format)
8178
+ const currentTimeValue = useMemo(() => {
8179
+ if (is24Hour) {
8180
+ if (hour === null || minute === null) {
8181
+ return '';
7877
8182
  }
7878
- newDate.setSeconds(0);
7879
- }
7880
- const finalISO = dayjs(newDate).tz(timezone).toISOString();
7881
- console.log('[DateTimePicker] Final ISO string to emit:', {
7882
- newDate: newDate.toISOString(),
7883
- timezone,
7884
- finalISO,
7885
- });
7886
- onChange?.(finalISO);
7887
- };
7888
- const handleClear = () => {
7889
- setSelectedDate('');
7890
- // Reset to default time values instead of clearing them
7891
- if (format === 'iso-date-time') {
7892
- setHour24(defaultTime ? defaultTime.hour ?? 0 : 0);
7893
- setMinute(defaultTime ? defaultTime.minute ?? 0 : 0);
7894
- setSecond(showSeconds
7895
- ? defaultTime
7896
- ? defaultTime.second ?? 0
7897
- : 0
7898
- : null);
8183
+ const s = second ?? 0;
8184
+ return `${hour}:${minute}:${s}`;
7899
8185
  }
7900
8186
  else {
7901
- setHour12(defaultTime ? defaultTime.hour ?? 12 : 12);
7902
- setMinute(defaultTime ? defaultTime.minute ?? 0 : 0);
7903
- setMeridiem(defaultTime ? defaultTime.meridiem ?? 'am' : 'am');
7904
- }
7905
- onChange?.(undefined);
7906
- };
7907
- const isISO = format === 'iso-date-time';
7908
- // Determine minDate: prioritize explicit minDate prop, then fall back to startTime
7909
- const effectiveMinDate = minDate
7910
- ? minDate
7911
- : normalizedStartTime && dayjs(normalizedStartTime).tz(timezone).isValid()
7912
- ? dayjs(normalizedStartTime).tz(timezone).startOf('day').toDate()
7913
- : undefined;
7914
- // Log current state before render
7915
- useEffect(() => {
7916
- console.log('[DateTimePicker] Current state before render:', {
7917
- isISO,
7918
- hour12,
7919
- minute,
7920
- meridiem,
7921
- hour24,
7922
- second,
7923
- selectedDate,
7924
- normalizedStartTime,
7925
- timezone,
7926
- });
7927
- }, [
7928
- isISO,
7929
- hour12,
7930
- minute,
7931
- meridiem,
7932
- hour24,
7933
- second,
7934
- selectedDate,
7935
- normalizedStartTime,
7936
- timezone,
7937
- ]);
7938
- // Compute display text from current state
7939
- const displayText = useMemo(() => {
7940
- if (!selectedDate)
7941
- return null;
7942
- const dateObj = dayjs.tz(selectedDate, timezone);
7943
- if (!dateObj.isValid())
7944
- return null;
7945
- if (isISO) {
7946
- // For ISO format, use hour24, minute, second
7947
- if (hour24 === null || minute === null)
7948
- return null;
7949
- const dateTimeObj = dateObj
7950
- .hour(hour24)
7951
- .minute(minute)
7952
- .second(second ?? 0);
7953
- return dateTimeObj.format(showSeconds ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm');
8187
+ if (hour === null || minute === null || meridiem === null) {
8188
+ return '';
8189
+ }
8190
+ return `${hour}:${minute}:${meridiem}`;
8191
+ }
8192
+ }, [hour, minute, second, meridiem, is24Hour]);
8193
+ // Parse custom time input formats like "1400", "2pm", "14:00", "2:00 PM"
8194
+ const parseCustomTimeInput = (input) => {
8195
+ if (!input || !input.trim()) {
8196
+ return { hour: null, minute: null, second: null, meridiem: null };
8197
+ }
8198
+ const trimmed = input.trim().toLowerCase();
8199
+ // Try parsing 4-digit format without colon: "1400" -> 14:00
8200
+ const fourDigitMatch = trimmed.match(/^(\d{4})$/);
8201
+ if (fourDigitMatch) {
8202
+ const digits = fourDigitMatch[1];
8203
+ const hour = parseInt(digits.substring(0, 2), 10);
8204
+ const minute = parseInt(digits.substring(2, 4), 10);
8205
+ if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) {
8206
+ if (is24Hour) {
8207
+ return { hour, minute, second: 0, meridiem: null };
8208
+ }
8209
+ else {
8210
+ // Convert to 12-hour format
8211
+ let hour12 = hour;
8212
+ let meridiem;
8213
+ if (hour === 0) {
8214
+ hour12 = 12;
8215
+ meridiem = 'am';
8216
+ }
8217
+ else if (hour === 12) {
8218
+ hour12 = 12;
8219
+ meridiem = 'pm';
8220
+ }
8221
+ else if (hour > 12) {
8222
+ hour12 = hour - 12;
8223
+ meridiem = 'pm';
8224
+ }
8225
+ else {
8226
+ hour12 = hour;
8227
+ meridiem = 'am';
8228
+ }
8229
+ return { hour: hour12, minute, second: null, meridiem };
8230
+ }
8231
+ }
7954
8232
  }
7955
- else {
7956
- // For 12-hour format, use hour12, minute, meridiem
7957
- if (hour12 === null || minute === null || meridiem === null)
7958
- return null;
7959
- // Convert to 24-hour format for dayjs
7960
- let hour24Value = hour12;
7961
- if (meridiem === 'am' && hour12 === 12)
7962
- hour24Value = 0;
7963
- else if (meridiem === 'pm' && hour12 < 12)
7964
- hour24Value = hour12 + 12;
7965
- const dateTimeObj = dateObj.hour(hour24Value).minute(minute).second(0);
7966
- return dateTimeObj.format('YYYY-MM-DD hh:mm A');
8233
+ // Try parsing hour with meridiem: "2pm", "14pm", "2am"
8234
+ const hourMeridiemMatch = trimmed.match(/^(\d{1,2})\s*(am|pm)$/);
8235
+ if (hourMeridiemMatch && !is24Hour) {
8236
+ const hour12 = parseInt(hourMeridiemMatch[1], 10);
8237
+ const meridiem = hourMeridiemMatch[2];
8238
+ if (hour12 >= 1 && hour12 <= 12) {
8239
+ return { hour: hour12, minute: 0, second: null, meridiem };
8240
+ }
7967
8241
  }
7968
- }, [
7969
- selectedDate,
7970
- isISO,
7971
- hour12,
7972
- minute,
7973
- meridiem,
7974
- hour24,
7975
- second,
7976
- showSeconds,
7977
- timezone,
7978
- ]);
7979
- const timezoneOffset = useMemo(() => {
7980
- if (!selectedDate)
7981
- return null;
7982
- const dateObj = dayjs.tz(selectedDate, timezone);
7983
- return dateObj.isValid() ? dateObj.format('Z') : null;
7984
- }, [selectedDate, timezone]);
7985
- return (jsxs(Flex, { direction: "column", gap: 2, children: [jsx(DatePickerInput, { value: selectedDate || undefined, onChange: (date) => {
7986
- if (date) {
7987
- handleDateChange(date);
8242
+ // Try parsing 24-hour format with hour only: "14" -> 14:00
8243
+ const hourOnlyMatch = trimmed.match(/^(\d{1,2})$/);
8244
+ if (hourOnlyMatch && is24Hour) {
8245
+ const hour = parseInt(hourOnlyMatch[1], 10);
8246
+ if (hour >= 0 && hour <= 23) {
8247
+ return { hour, minute: 0, second: 0, meridiem: null };
8248
+ }
8249
+ }
8250
+ // Try parsing standard formats: "14:00", "2:00 PM"
8251
+ const time24Pattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/;
8252
+ const match24 = trimmed.match(time24Pattern);
8253
+ if (match24) {
8254
+ const hour24 = parseInt(match24[1], 10);
8255
+ const minute = parseInt(match24[2], 10);
8256
+ const second = match24[3] ? parseInt(match24[3], 10) : 0;
8257
+ if (hour24 >= 0 &&
8258
+ hour24 <= 23 &&
8259
+ minute >= 0 &&
8260
+ minute <= 59 &&
8261
+ second >= 0 &&
8262
+ second <= 59) {
8263
+ if (is24Hour) {
8264
+ return { hour: hour24, minute, second, meridiem: null };
8265
+ }
8266
+ else {
8267
+ // Convert to 12-hour format
8268
+ let hour12 = hour24;
8269
+ let meridiem;
8270
+ if (hour24 === 0) {
8271
+ hour12 = 12;
8272
+ meridiem = 'am';
8273
+ }
8274
+ else if (hour24 === 12) {
8275
+ hour12 = 12;
8276
+ meridiem = 'pm';
8277
+ }
8278
+ else if (hour24 > 12) {
8279
+ hour12 = hour24 - 12;
8280
+ meridiem = 'pm';
7988
8281
  }
7989
8282
  else {
7990
- setSelectedDate('');
7991
- onChange?.(undefined);
8283
+ hour12 = hour24;
8284
+ meridiem = 'am';
8285
+ }
8286
+ return { hour: hour12, minute, second: null, meridiem };
8287
+ }
8288
+ }
8289
+ }
8290
+ // Try parsing 12-hour format: "2:00 PM", "2:00PM"
8291
+ const time12Pattern = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\s*(am|pm)$/;
8292
+ const match12 = trimmed.match(time12Pattern);
8293
+ if (match12 && !is24Hour) {
8294
+ const hour12 = parseInt(match12[1], 10);
8295
+ const minute = parseInt(match12[2], 10);
8296
+ const second = match12[3] ? parseInt(match12[3], 10) : null;
8297
+ const meridiem = match12[4];
8298
+ if (hour12 >= 1 &&
8299
+ hour12 <= 12 &&
8300
+ minute >= 0 &&
8301
+ minute <= 59 &&
8302
+ (second === null || (second >= 0 && second <= 59))) {
8303
+ return { hour: hour12, minute, second, meridiem };
8304
+ }
8305
+ }
8306
+ return { hour: null, minute: null, second: null, meridiem: null };
8307
+ };
8308
+ const handleTimeValueChange = (details) => {
8309
+ if (details.value.length === 0) {
8310
+ handleTimeChange(null, null, null, null);
8311
+ filter('');
8312
+ return;
8313
+ }
8314
+ const selectedValue = details.value[0];
8315
+ const selectedOption = timeOptions.find((opt) => opt.value === selectedValue);
8316
+ if (selectedOption) {
8317
+ filter('');
8318
+ if (is24Hour) {
8319
+ const opt24 = selectedOption;
8320
+ handleTimeChange(opt24.hour, opt24.minute, opt24.second, null);
8321
+ }
8322
+ else {
8323
+ const opt12 = selectedOption;
8324
+ handleTimeChange(opt12.hour, opt12.minute, null, opt12.meridiem);
8325
+ }
8326
+ }
8327
+ };
8328
+ // Track the current input value for Enter key handling
8329
+ const [timeInputValue, setTimeInputValue] = useState('');
8330
+ const handleTimeInputChange = (details) => {
8331
+ // Store the input value and filter
8332
+ setTimeInputValue(details.inputValue);
8333
+ filter(details.inputValue);
8334
+ };
8335
+ const handleTimeInputKeyDown = (e) => {
8336
+ if (e.key === 'Enter') {
8337
+ e.preventDefault();
8338
+ // Use the stored input value
8339
+ const parsed = parseCustomTimeInput(timeInputValue);
8340
+ if (parsed.hour !== null && parsed.minute !== null) {
8341
+ if (is24Hour) {
8342
+ handleTimeChange(parsed.hour, parsed.minute, parsed.second, null);
8343
+ }
8344
+ else {
8345
+ if (parsed.meridiem !== null) {
8346
+ handleTimeChange(parsed.hour, parsed.minute, null, parsed.meridiem);
7992
8347
  }
7993
- }, placeholder: "Select a date", dateFormat: "YYYY-MM-DD", displayFormat: "YYYY-MM-DD", labels: labels, timezone: timezone, minDate: effectiveMinDate, maxDate: maxDate, monthsToDisplay: 1, readOnly: false }), jsxs(Grid, { templateColumns: "1fr auto", alignItems: "center", gap: 2, children: [isISO ? (jsx(IsoTimePicker, { hour: hour24, setHour: setHour24, minute: minute, setMinute: setMinute, second: showSeconds ? second : null, setSecond: showSeconds ? setSecond : () => { }, onChange: handleTimeChange, startTime: normalizedStartTime, selectedDate: selectedDate, timezone: timezone, portalled: portalled, labels: timePickerLabels })) : (jsx(TimePicker$1, { hour: hour12, setHour: setHour12, minute: minute, setMinute: setMinute, meridiem: meridiem, setMeridiem: setMeridiem, onChange: handleTimeChange, startTime: normalizedStartTime, selectedDate: selectedDate, timezone: timezone, portalled: portalled, labels: timePickerLabels })), jsx(Button$1, { onClick: handleClear, size: "sm", variant: "outline", colorScheme: "red", children: jsx(Icon, { as: FaTrash }) })] }), displayText && (jsxs(Flex, { gap: 2, children: [jsx(Text, { fontSize: "sm", color: { base: 'gray.600', _dark: 'gray.600' }, children: displayText }), timezoneOffset && (jsx(Text, { fontSize: "sm", color: { base: 'gray.600', _dark: 'gray.600' }, children: timezoneOffset })), jsx(Text, { fontSize: "sm", color: { base: 'gray.600', _dark: 'gray.600' }, children: timezone })] }))] }));
8348
+ }
8349
+ // Clear the filter and input value after applying
8350
+ filter('');
8351
+ setTimeInputValue('');
8352
+ // Close the popover if value is valid
8353
+ setTimePopoverOpen(false);
8354
+ }
8355
+ }
8356
+ };
8357
+ // Calendar rendering
8358
+ const renderCalendar = () => {
8359
+ const { calendars, getBackProps, getForwardProps, getDateProps } = calendarProps;
8360
+ if (calendars.length === 0)
8361
+ return null;
8362
+ const calendar = calendars[0];
8363
+ return (jsxs(Grid, { gap: 4, children: [jsxs(Grid, { templateColumns: 'repeat(4, auto)', justifyContent: 'center', children: [jsx(Button$1, { variant: 'ghost', ...getBackProps({ offset: 12 }), children: '<<' }), jsx(Button$1, { variant: 'ghost', ...getBackProps(), children: backButtonLabel }), jsx(Button$1, { variant: 'ghost', ...getForwardProps(), children: forwardButtonLabel }), jsx(Button$1, { variant: 'ghost', ...getForwardProps({ offset: 12 }), children: '>>' })] }), jsx(Grid, { justifyContent: 'center', children: jsxs(Text, { children: [monthNamesShort[calendar.month], " ", calendar.year] }) }), jsx(Grid, { templateColumns: 'repeat(7, auto)', justifyContent: 'center', children: [0, 1, 2, 3, 4, 5, 6].map((weekdayNum) => {
8364
+ return (jsx(Text, { textAlign: 'center', fontWeight: "semibold", minW: "40px", children: weekdayNamesShort[weekdayNum] }, `header-${weekdayNum}`));
8365
+ }) }), calendar.weeks.map((week, weekIndex) => (jsx(Grid, { templateColumns: 'repeat(7, auto)', justifyContent: 'center', children: week.map((dateObj, dayIndex) => {
8366
+ if (!dateObj) {
8367
+ return (jsx("div", { style: { minWidth: '40px' } }, `empty-${dayIndex}`));
8368
+ }
8369
+ const { date, selected, selectable, isCurrentMonth } = dateObj;
8370
+ const dateProps = getDateProps({
8371
+ dateObj,
8372
+ });
8373
+ return (jsx(Button$1, { variant: selected ? 'solid' : 'ghost', colorPalette: selected ? 'blue' : undefined, size: "sm", minW: "40px", disabled: !selectable, opacity: isCurrentMonth ? 1 : 0.4, ...dateProps, children: date.getDate() }, `${date.getTime()}`));
8374
+ }) }, `week-${weekIndex}`)))] }));
8375
+ };
8376
+ return (jsxs(Flex, { direction: "row", gap: 2, align: "center", children: [jsxs(Popover.Root, { open: datePopoverOpen, onOpenChange: (e) => setDatePopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsx(Popover.Trigger, { asChild: true, children: jsxs(Button$1, { size: "sm", variant: "outline", onClick: () => setDatePopoverOpen(true), justifyContent: "start", children: [jsx(MdDateRange, {}), dateDisplayText] }) }), portalled ? (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "350px", minH: "25rem", children: jsx(Popover.Body, { p: 4, children: jsxs(Grid, { gap: 4, children: [jsx(InputGroup$1, { endElement: jsxs(Popover.Root, { open: calendarPopoverOpen, onOpenChange: (e) => setCalendarPopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsx(Popover.Trigger, { asChild: true, children: jsx(Button$1, { variant: "ghost", size: "xs", "aria-label": "Open calendar", onClick: () => setCalendarPopoverOpen(true), children: jsx(MdDateRange, {}) }) }), jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "350px", minH: "25rem", children: jsx(Popover.Body, { p: 4, children: renderCalendar() }) }) })] }), children: jsx(Input, { value: dateInputValue, onChange: handleDateInputChange, onBlur: handleDateInputBlur, onKeyDown: handleDateInputKeyDown, placeholder: "YYYY-MM-DD" }) }), showQuickActions && (jsxs(Grid, { templateColumns: "repeat(4, 1fr)", gap: 2, children: [jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getYesterday()), disabled: !isDateValid(getYesterday()), children: quickActionLabels.yesterday }), jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getToday()), disabled: !isDateValid(getToday()), children: quickActionLabels.today }), jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getTomorrow()), disabled: !isDateValid(getTomorrow()), children: quickActionLabels.tomorrow }), jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getPlus7Days()), disabled: !isDateValid(getPlus7Days()), children: quickActionLabels.plus7Days })] }))] }) }) }) }) })) : (jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", children: jsx(Popover.Body, { p: 4, children: jsxs(Grid, { gap: 4, children: [jsx(InputGroup$1, { endElement: jsxs(Popover.Root, { open: calendarPopoverOpen, onOpenChange: (e) => setCalendarPopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsx(Popover.Trigger, { asChild: true, children: jsx(Button$1, { variant: "ghost", size: "xs", "aria-label": "Open calendar", onClick: () => setCalendarPopoverOpen(true), children: jsx(MdDateRange, {}) }) }), jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "350px", minH: "25rem", children: jsx(Popover.Body, { p: 4, children: renderCalendar() }) }) })] }), children: jsx(Input, { value: dateInputValue, onChange: handleDateInputChange, onBlur: handleDateInputBlur, onKeyDown: handleDateInputKeyDown, placeholder: "YYYY-MM-DD" }) }), showQuickActions && (jsxs(Grid, { templateColumns: "repeat(4, 1fr)", gap: 2, children: [jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getYesterday()), disabled: !isDateValid(getYesterday()), children: quickActionLabels.yesterday }), jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getToday()), disabled: !isDateValid(getToday()), children: quickActionLabels.today }), jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getTomorrow()), disabled: !isDateValid(getTomorrow()), children: quickActionLabels.tomorrow }), jsx(Button$1, { size: "sm", variant: "outline", onClick: () => handleQuickActionClick(getPlus7Days()), disabled: !isDateValid(getPlus7Days()), children: quickActionLabels.plus7Days })] }))] }) }) }) }))] }), jsxs(Popover.Root, { open: timePopoverOpen, onOpenChange: (e) => setTimePopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsx(Popover.Trigger, { asChild: true, children: jsxs(Button$1, { size: "sm", variant: "outline", onClick: () => setTimePopoverOpen(true), justifyContent: "start", children: [jsx(BsClock, {}), timeDisplayText] }) }), portalled ? (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "300px", children: jsx(Popover.Body, { p: 4, children: jsx(Grid, { gap: 2, children: jsxs(Combobox.Root, { value: currentTimeValue ? [currentTimeValue] : [], onValueChange: handleTimeValueChange, onInputValueChange: handleTimeInputChange, collection: collection, allowCustomValue: true, children: [jsxs(Combobox.Control, { children: [jsx(InputGroup$1, { startElement: jsx(BsClock, {}), children: jsx(Combobox.Input, { placeholder: timePickerLabels?.placeholder ??
8377
+ (is24Hour ? 'HH:mm' : 'hh:mm AM/PM'), onKeyDown: handleTimeInputKeyDown }) }), jsx(Combobox.IndicatorGroup, { children: jsx(Combobox.Trigger, {}) })] }), jsx(Portal, { disabled: true, children: jsx(Combobox.Positioner, { children: jsxs(Combobox.Content, { children: [jsx(Combobox.Empty, { children: timePickerLabels?.emptyMessage ??
8378
+ 'No time found' }), collection.items.map((item) => {
8379
+ const option = item;
8380
+ return (jsxs(Combobox.Item, { item: item, children: [jsxs(Flex, { justify: "space-between", align: "center", w: "100%", children: [jsx(Text, { children: option.label }), option.durationText && (jsx(Text, { fontSize: "xs", color: "gray.500", children: option.durationText }))] }), jsx(Combobox.ItemIndicator, {})] }, option.value));
8381
+ })] }) }) })] }) }) }) }) }) })) : (jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "300px", children: jsx(Popover.Body, { p: 4, children: jsx(Grid, { gap: 2, children: jsxs(Combobox.Root, { value: currentTimeValue ? [currentTimeValue] : [], onValueChange: handleTimeValueChange, onInputValueChange: handleTimeInputChange, collection: collection, allowCustomValue: true, children: [jsxs(Combobox.Control, { children: [jsx(InputGroup$1, { startElement: jsx(BsClock, {}), children: jsx(Combobox.Input, { placeholder: timePickerLabels?.placeholder ??
8382
+ (is24Hour ? 'HH:mm' : 'hh:mm AM/PM'), onKeyDown: handleTimeInputKeyDown }) }), jsx(Combobox.IndicatorGroup, { children: jsx(Combobox.Trigger, {}) })] }), jsx(Portal, { disabled: true, children: jsx(Combobox.Positioner, { children: jsxs(Combobox.Content, { children: [jsx(Combobox.Empty, { children: timePickerLabels?.emptyMessage ?? 'No time found' }), collection.items.map((item) => {
8383
+ const option = item;
8384
+ return (jsxs(Combobox.Item, { item: item, children: [jsxs(Flex, { justify: "space-between", align: "center", w: "100%", children: [jsx(Text, { children: option.label }), option.durationText && (jsx(Text, { fontSize: "xs", color: "gray.500", children: option.durationText }))] }), jsx(Combobox.ItemIndicator, {})] }, option.value));
8385
+ })] }) }) })] }) }) }) }) }))] }), showTimezoneSelector && (jsxs(Popover.Root, { open: timezonePopoverOpen, onOpenChange: (e) => setTimezonePopoverOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsx(Popover.Trigger, { asChild: true, children: jsx(Button$1, { size: "sm", variant: "outline", onClick: () => setTimezonePopoverOpen(true), justifyContent: "start", children: timezoneDisplayText || 'Select timezone' }) }), portalled ? (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "250px", children: jsx(Popover.Body, { p: 4, children: jsx(Grid, { gap: 2, children: jsxs(Select.Root, { size: "sm", collection: timezoneCollection, value: timezoneOffset ? [timezoneOffset] : [], onValueChange: (e) => {
8386
+ const newOffset = e.value[0];
8387
+ if (newOffset) {
8388
+ setTimezoneOffset(newOffset);
8389
+ // Update date-time with new offset
8390
+ if (selectedDate &&
8391
+ hour !== null &&
8392
+ minute !== null) {
8393
+ updateDateTime(selectedDate, hour, minute, second, meridiem);
8394
+ }
8395
+ // Close popover after selection
8396
+ setTimezonePopoverOpen(false);
8397
+ }
8398
+ }, children: [jsxs(Select.Control, { children: [jsx(Select.Trigger, {}), jsx(Select.IndicatorGroup, { children: jsx(Select.Indicator, {}) })] }), jsx(Select.Positioner, { children: jsx(Select.Content, { children: timezoneCollection.items.map((item) => (jsxs(Select.Item, { item: item, children: [jsx(Select.ItemText, { children: item.label }), jsx(Select.ItemIndicator, {})] }, item.value))) }) })] }) }) }) }) }) })) : (jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "250px", children: jsx(Popover.Body, { p: 4, children: jsx(Grid, { gap: 2, children: jsxs(Select.Root, { size: "sm", collection: timezoneCollection, value: timezoneOffset ? [timezoneOffset] : [], onValueChange: (e) => {
8399
+ const newOffset = e.value[0];
8400
+ if (newOffset) {
8401
+ setTimezoneOffset(newOffset);
8402
+ // Update date-time with new offset (pass it directly to avoid stale state)
8403
+ if (selectedDate &&
8404
+ hour !== null &&
8405
+ minute !== null) {
8406
+ updateDateTime(selectedDate, hour, minute, second, meridiem, newOffset);
8407
+ }
8408
+ // Close popover after selection
8409
+ setTimezonePopoverOpen(false);
8410
+ }
8411
+ }, children: [jsxs(Select.Control, { children: [jsx(Select.Trigger, {}), jsx(Select.IndicatorGroup, { children: jsx(Select.Indicator, {}) })] }), jsx(Select.Positioner, { children: jsx(Select.Content, { children: timezoneCollection.items.map((item) => (jsxs(Select.Item, { item: item, children: [jsx(Select.ItemText, { children: item.label }), jsx(Select.ItemIndicator, {})] }, item.value))) }) })] }) }) }) }) }))] }))] }));
7994
8412
  }
7995
8413
 
7996
8414
  dayjs.extend(utc);
@@ -8004,11 +8422,12 @@ const DateTimePicker = ({ column, schema, prefix, }) => {
8004
8422
  dateFormat = 'YYYY-MM-DD[T]HH:mm:ssZ', } = schema;
8005
8423
  const isRequired = required?.some((columnId) => columnId === column);
8006
8424
  const colLabel = formI18n.colLabel;
8007
- const [open, setOpen] = useState(false);
8425
+ useState(false);
8008
8426
  const selectedDate = watch(colLabel);
8009
- const displayDate = selectedDate && dayjs(selectedDate).tz(timezone).isValid()
8427
+ selectedDate && dayjs(selectedDate).tz(timezone).isValid()
8010
8428
  ? dayjs(selectedDate).tz(timezone).format(displayDateFormat)
8011
8429
  : '';
8430
+ // Set default date on mount if no value exists
8012
8431
  const dateTimePickerLabelsConfig = {
8013
8432
  monthNamesShort: dateTimePickerLabels?.monthNamesShort ?? [
8014
8433
  'January',
@@ -8050,9 +8469,7 @@ const DateTimePicker = ({ column, schema, prefix, }) => {
8050
8469
  }
8051
8470
  }, timezone: timezone, labels: dateTimePickerLabelsConfig, timePickerLabels: timePickerLabels }));
8052
8471
  return (jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
8053
- gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: jsxs(Popover.Root, { open: open, onOpenChange: (e) => setOpen(e.open), closeOnInteractOutside: true, autoFocus: false, children: [jsx(Popover.Trigger, { asChild: true, children: jsxs(Button, { size: "sm", variant: "outline", onClick: () => {
8054
- setOpen(true);
8055
- }, justifyContent: 'start', children: [jsx(MdDateRange, {}), displayDate || ''] }) }), insideDialog ? (jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "350px", minH: "10rem", children: jsx(Popover.Body, { children: dateTimePickerContent }) }) })) : (jsx(Portal, { children: jsx(Popover.Positioner, { children: jsx(Popover.Content, { width: "fit-content", minW: "350px", minH: "10rem", children: jsx(Popover.Body, { children: dateTimePickerContent }) }) }) }))] }) }));
8472
+ gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: dateTimePickerContent }));
8056
8473
  };
8057
8474
 
8058
8475
  const SchemaRenderer = ({ schema, prefix, column, }) => {