@sd-angular/core 19.0.44 → 19.0.46

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.
Files changed (66) hide show
  1. package/README.md +6 -0
  2. package/assets/scss/core/form.scss +21 -11
  3. package/assets/scss/core/view.scss +24 -0
  4. package/assets/scss/sd-core.scss +1 -0
  5. package/assets/scss/themes/_material-tokens.scss +56 -0
  6. package/assets/scss/themes/default.scss +10 -0
  7. package/components/form-generic/src/components/form-builder/components/expression-builder/expression-builder.component.d.ts +1 -0
  8. package/components/form-generic/src/models/form-generic-expression.model.d.ts +10 -4
  9. package/components/form-generic/src/pipes/component-viewed.pipe.d.ts +1 -0
  10. package/components/import-excel/src/pipes/column-transform.pipe.d.ts +1 -0
  11. package/components/inform/src/inform.component.d.ts +12 -3
  12. package/components/table/src/services/table-export/table-export.service.d.ts +1 -1
  13. package/components/table/src/services/table-filter/table-filter.model.d.ts +7 -0
  14. package/components/table/src/services/table-local/table-local-sort.util.d.ts +8 -2
  15. package/components/table/src/services/table-local/table-local.util.d.ts +6 -1
  16. package/components/table/src/table.component.d.ts +1 -1
  17. package/components/timeline/pipes/view-date.pipe.d.ts +1 -0
  18. package/configurations/src/sd-core.configuration.d.ts +7 -0
  19. package/fesm2022/sd-angular-core-components-form-generic.mjs +44 -31
  20. package/fesm2022/sd-angular-core-components-form-generic.mjs.map +1 -1
  21. package/fesm2022/sd-angular-core-components-import-excel.mjs +19 -5
  22. package/fesm2022/sd-angular-core-components-import-excel.mjs.map +1 -1
  23. package/fesm2022/sd-angular-core-components-inform.mjs +16 -2
  24. package/fesm2022/sd-angular-core-components-inform.mjs.map +1 -1
  25. package/fesm2022/sd-angular-core-components-section.mjs +2 -2
  26. package/fesm2022/sd-angular-core-components-section.mjs.map +1 -1
  27. package/fesm2022/sd-angular-core-components-table.mjs +174 -66
  28. package/fesm2022/sd-angular-core-components-table.mjs.map +1 -1
  29. package/fesm2022/sd-angular-core-components-timeline.mjs +4 -1
  30. package/fesm2022/sd-angular-core-components-timeline.mjs.map +1 -1
  31. package/fesm2022/sd-angular-core-components-view.mjs +2 -2
  32. package/fesm2022/sd-angular-core-components-view.mjs.map +1 -1
  33. package/fesm2022/sd-angular-core-configurations.mjs.map +1 -1
  34. package/fesm2022/sd-angular-core-forms-checkbox.mjs +2 -2
  35. package/fesm2022/sd-angular-core-forms-checkbox.mjs.map +1 -1
  36. package/fesm2022/sd-angular-core-forms-date-range.mjs +15 -2
  37. package/fesm2022/sd-angular-core-forms-date-range.mjs.map +1 -1
  38. package/fesm2022/sd-angular-core-forms-date.mjs +11 -1
  39. package/fesm2022/sd-angular-core-forms-date.mjs.map +1 -1
  40. package/fesm2022/sd-angular-core-forms-datetime.mjs +43 -9
  41. package/fesm2022/sd-angular-core-forms-datetime.mjs.map +1 -1
  42. package/fesm2022/sd-angular-core-forms-models.mjs +6 -0
  43. package/fesm2022/sd-angular-core-forms-models.mjs.map +1 -1
  44. package/fesm2022/sd-angular-core-forms-radio.mjs +2 -2
  45. package/fesm2022/sd-angular-core-forms-radio.mjs.map +1 -1
  46. package/fesm2022/sd-angular-core-forms-switch.mjs +2 -2
  47. package/fesm2022/sd-angular-core-forms-switch.mjs.map +1 -1
  48. package/fesm2022/sd-angular-core-forms-textarea.mjs +2 -2
  49. package/fesm2022/sd-angular-core-forms-textarea.mjs.map +1 -1
  50. package/fesm2022/sd-angular-core-pipes.mjs +26 -10
  51. package/fesm2022/sd-angular-core-pipes.mjs.map +1 -1
  52. package/fesm2022/sd-angular-core-services-date-time.mjs +143 -0
  53. package/fesm2022/sd-angular-core-services-date-time.mjs.map +1 -0
  54. package/fesm2022/sd-angular-core-services.mjs +1 -0
  55. package/fesm2022/sd-angular-core-services.mjs.map +1 -1
  56. package/fesm2022/sd-angular-core-utilities-models.mjs.map +1 -1
  57. package/forms/datetime/src/datetime-time-zone.adapter.d.ts +15 -0
  58. package/forms/models/src/sd-strict-date-adapter.d.ts +3 -0
  59. package/package.json +17 -13
  60. package/pipes/src/time-different.pipe.d.ts +1 -0
  61. package/services/date-time/index.d.ts +1 -0
  62. package/services/date-time/src/date-time.service.d.ts +54 -0
  63. package/services/date-time/src/public-api.d.ts +1 -0
  64. package/services/index.d.ts +1 -0
  65. package/utilities/extensions/src/string.extension.d.ts +1 -0
  66. package/utilities/models/src/icon.model.d.ts +2 -0
@@ -7,8 +7,8 @@ import * as i7 from '@angular/material/sort';
7
7
  import { MatSort, MatSortModule } from '@angular/material/sort';
8
8
  import * as i6 from '@angular/material/table';
9
9
  import { MatTable, MatTableModule } from '@angular/material/table';
10
- import * as i1$1 from '@sd-angular/core/services';
11
- import { SdStorageService, SdExcelService, SdNotifyService } from '@sd-angular/core/services';
10
+ import { ɵSD_LOCAL_DATE_TIME as _SD_LOCAL_DATE_TIME, SdDateTimeService } from '@sd-angular/core/services/date-time';
11
+ import { SdNotifyService } from '@sd-angular/core/services/notify';
12
12
  import { Subject, fromEvent, Subscription, isObservable, firstValueFrom } from 'rxjs';
13
13
  import { debounceTime, startWith, map, switchMap, filter } from 'rxjs/operators';
14
14
  import { DateUtilities, StringUtilities } from '@sd-angular/core/utilities';
@@ -35,8 +35,8 @@ import { SdReadChannel, cloneReadRequest } from '@sd-angular/core/utilities/read
35
35
  import { SdQuickAction } from '@sd-angular/core/components/quick-action';
36
36
  import { SdTooltipDirective, SdScrollDirective, SdDesktopDirective, SdMobileDirective, SdHoverCopyDirective } from '@sd-angular/core/directives';
37
37
  import { SdSafeHtmlPipe, SdFormatNumberPipe } from '@sd-angular/core/pipes';
38
- import { Utilities, BrowserUtilities } from '@sdcorejs/utils/fns';
39
- import { DateUtilities as DateUtilities$1, ArrayUtilities, NumberUtilities } from '@sd-angular/core/utilities/extensions';
38
+ import { Utilities, BrowserUtilities, NumberUtilities as NumberUtilities$1 } from '@sdcorejs/utils/fns';
39
+ import { ArrayUtilities, NumberUtilities } from '@sd-angular/core/utilities/extensions';
40
40
  import { SdBadge } from '@sd-angular/core/components/badge';
41
41
  import { SdOperator } from '@sd-angular/core/components/operator';
42
42
  import { SdInput, SdInputNumber, SdSelect, SdDate, SdDatetime, SdDateRange as SdDateRange$1, SdCheckbox, SdSwitch } from '@sd-angular/core/forms';
@@ -51,10 +51,13 @@ import { MatButtonModule } from '@angular/material/button';
51
51
  import { MatSlideToggleModule } from '@angular/material/slide-toggle';
52
52
  import { SdModal } from '@sd-angular/core/components/modal';
53
53
  import { SdConfirmService } from '@sd-angular/core/services/confirm';
54
+ import * as i1$1 from '@sd-angular/core/services';
55
+ import { SdStorageService } from '@sd-angular/core/services';
54
56
  import * as i2$1 from '@angular/material/tooltip';
55
57
  import { MatTooltipModule } from '@angular/material/tooltip';
56
58
  import * as i5$1 from '@angular/material/chips';
57
59
  import { MatChipsModule } from '@angular/material/chips';
60
+ import { SdExcelService } from '@sd-angular/core/services/excel';
58
61
 
59
62
  class SdTableCellDefDirective {
60
63
  static ngTemplateContextGuard(_dir, _ctx) {
@@ -164,8 +167,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImpo
164
167
  }]
165
168
  }] });
166
169
 
170
+ /**
171
+ * Machine-zone date math, exactly the payload sent before time zones: the local bounds of
172
+ * `SdDateTimeService`, and a datetime still read by `new Date(value)`.
173
+ */
174
+ const LOCAL_FILTER_DATES = { ..._SD_LOCAL_DATE_TIME, parse: value => new Date(value) };
175
+ const hasFilterValue = (value) => value !== undefined && value !== null && value !== '';
167
176
  const SdConvertToPagingReq = (filterRequest, args) => {
168
177
  const { externalFilters, columns, fieldMapping } = args;
178
+ const dates = args.dateTime && args.dateTime.timeZone !== 'local' ? args.dateTime : LOCAL_FILTER_DATES;
169
179
  const req = {
170
180
  filters: [],
171
181
  orders: args?.orders || [],
@@ -209,14 +219,14 @@ const SdConvertToPagingReq = (filterRequest, args) => {
209
219
  filters.push({
210
220
  field,
211
221
  operator: 'GREATER_OR_EQUAL',
212
- data: DateUtilities.begin(value?.from).toISOString(),
222
+ data: dates.begin(value?.from).toISOString(),
213
223
  });
214
224
  }
215
225
  if (value?.to) {
216
226
  filters.push({
217
227
  field,
218
228
  operator: 'LESS_THAN',
219
- data: DateUtilities.begin(DateUtilities.addDays(value?.to, 1)).toISOString(),
229
+ data: dates.beginOfNextDay(value?.to).toISOString(),
220
230
  });
221
231
  }
222
232
  }
@@ -227,14 +237,14 @@ const SdConvertToPagingReq = (filterRequest, args) => {
227
237
  filters.push({
228
238
  field,
229
239
  operator: 'GREATER_OR_EQUAL',
230
- data: DateUtilities.begin(value?.from).toISOString(),
240
+ data: dates.begin(value?.from).toISOString(),
231
241
  });
232
242
  }
233
243
  if (value?.to) {
234
244
  filters.push({
235
245
  field,
236
246
  operator: 'LESS_THAN',
237
- data: DateUtilities.begin(DateUtilities.addDays(value?.to, 1)).toISOString(),
247
+ data: dates.beginOfNextDay(value?.to).toISOString(),
238
248
  });
239
249
  }
240
250
  }
@@ -244,14 +254,14 @@ const SdConvertToPagingReq = (filterRequest, args) => {
244
254
  filters.push({
245
255
  field,
246
256
  operator: 'GREATER_OR_EQUAL',
247
- data: DateUtilities.begin(value).toISOString(),
257
+ data: dates.begin(value).toISOString(),
248
258
  });
249
259
  }
250
260
  if (externalFilter.defaultOperator === 'LESS_OR_EQUAL') {
251
261
  filters.push({
252
262
  field,
253
263
  operator: 'LESS_THAN',
254
- data: DateUtilities.begin(DateUtilities.addDays(value, 1)).toISOString(),
264
+ data: dates.beginOfNextDay(value).toISOString(),
255
265
  });
256
266
  }
257
267
  }
@@ -260,14 +270,14 @@ const SdConvertToPagingReq = (filterRequest, args) => {
260
270
  filters.push({
261
271
  field,
262
272
  operator: 'GREATER_OR_EQUAL',
263
- data: new Date(value).toISOString(),
273
+ data: dates.parse(value).toISOString(),
264
274
  });
265
275
  }
266
276
  if (externalFilter.defaultOperator === 'LESS_OR_EQUAL') {
267
277
  filters.push({
268
278
  field,
269
279
  operator: 'LESS_OR_EQUAL',
270
- data: new Date(value).toISOString(),
280
+ data: dates.parse(value).toISOString(),
271
281
  });
272
282
  }
273
283
  }
@@ -288,14 +298,14 @@ const SdConvertToPagingReq = (filterRequest, args) => {
288
298
  filters.push({
289
299
  field,
290
300
  operator: 'GREATER_OR_EQUAL',
291
- data: DateUtilities.begin(value?.from).toISOString(),
301
+ data: dates.begin(value?.from).toISOString(),
292
302
  });
293
303
  }
294
304
  if (value?.to) {
295
305
  filters.push({
296
306
  field,
297
307
  operator: 'LESS_THAN',
298
- data: DateUtilities.begin(DateUtilities.addDays(value?.to, 1)).toISOString(),
308
+ data: dates.beginOfNextDay(value?.to).toISOString(),
299
309
  });
300
310
  }
301
311
  }
@@ -337,8 +347,8 @@ const SdConvertToPagingReq = (filterRequest, args) => {
337
347
  field,
338
348
  operator: 'BETWEEN',
339
349
  data: {
340
- from: DateUtilities.begin(value?.from).toISOString(),
341
- to: DateUtilities.end(value?.to).toISOString(),
350
+ from: dates.begin(value?.from).toISOString(),
351
+ to: dates.end(value?.to).toISOString(),
342
352
  },
343
353
  });
344
354
  }
@@ -346,14 +356,14 @@ const SdConvertToPagingReq = (filterRequest, args) => {
346
356
  filters.push({
347
357
  field,
348
358
  operator: 'GREATER_OR_EQUAL',
349
- data: DateUtilities.begin(value?.from).toISOString(),
359
+ data: dates.begin(value?.from).toISOString(),
350
360
  });
351
361
  }
352
362
  else if (value?.to) {
353
363
  filters.push({
354
364
  field,
355
365
  operator: 'LESS_THAN',
356
- data: DateUtilities.begin(DateUtilities.addDays(value?.to, 1)).toISOString(),
366
+ data: dates.beginOfNextDay(value?.to).toISOString(),
357
367
  });
358
368
  }
359
369
  }
@@ -363,13 +373,39 @@ const SdConvertToPagingReq = (filterRequest, args) => {
363
373
  field,
364
374
  operator: 'BETWEEN',
365
375
  data: {
366
- from: DateUtilities.begin(value).toISOString(),
367
- to: DateUtilities.end(value).toISOString(),
376
+ from: dates.begin(value).toISOString(),
377
+ to: dates.end(value).toISOString(),
368
378
  },
369
379
  });
370
380
  }
371
381
  }
372
382
  }
383
+ else if (column.type === 'number' && typeof value === 'object' && !Array.isArray(value) && ('from' in value || 'to' in value)) {
384
+ // why: the split-number filter emits an inclusive `{ from, to }`; a missing end leaves that side open, and 0 is a bound.
385
+ const hasFrom = hasFilterValue(value.from);
386
+ const hasTo = hasFilterValue(value.to);
387
+ if (hasFrom && hasTo) {
388
+ filters.push({
389
+ field,
390
+ operator: 'BETWEEN',
391
+ data: { from: value.from, to: value.to },
392
+ });
393
+ }
394
+ else if (hasFrom) {
395
+ filters.push({
396
+ field,
397
+ operator: 'GREATER_OR_EQUAL',
398
+ data: value.from,
399
+ });
400
+ }
401
+ else if (hasTo) {
402
+ filters.push({
403
+ field,
404
+ operator: 'LESS_OR_EQUAL',
405
+ data: value.to,
406
+ });
407
+ }
408
+ }
373
409
  else {
374
410
  if (Array.isArray(value)) {
375
411
  if (value.length) {
@@ -2658,6 +2694,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImpo
2658
2694
  class TableExportService {
2659
2695
  #excelService = inject(SdExcelService);
2660
2696
  #i18n = inject(I18nService);
2697
+ // why: date cells are written as the wall clock of ISdCoreConfiguration.timeZone, like the table shows them.
2698
+ #dateTime = inject(SdDateTimeService);
2661
2699
  // ==========================================
2662
2700
  // SIGNAL STATE (Component sẽ bind trực tiếp vào đây)
2663
2701
  // ==========================================
@@ -2788,13 +2826,13 @@ class TableExportService {
2788
2826
  }
2789
2827
  }
2790
2828
  else if (column.type === 'date') {
2791
- obj[fieldStr] = DateUtilities$1.toFormat(itemValue, 'yyyy/MM/dd');
2829
+ obj[fieldStr] = this.#dateTime.toFormat(itemValue, 'yyyy/MM/dd');
2792
2830
  }
2793
2831
  else if (column.type === 'datetime') {
2794
- obj[fieldStr] = DateUtilities$1.toFormat(itemValue, 'yyyy/MM/dd HH:mm:ss');
2832
+ obj[fieldStr] = this.#dateTime.toFormat(itemValue, 'yyyy/MM/dd HH:mm:ss');
2795
2833
  }
2796
2834
  else if (column.type === 'time') {
2797
- obj[fieldStr] = DateUtilities$1.toFormat(itemValue, 'HH:mm:ss');
2835
+ obj[fieldStr] = this.#dateTime.toFormat(itemValue, 'HH:mm:ss');
2798
2836
  }
2799
2837
  else if (column.type === 'values' || column.type === 'lazy-values') {
2800
2838
  const vals = (Array.isArray(itemValue) ? itemValue : [itemValue]).filter(e => !!e?.toString());
@@ -2930,6 +2968,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImpo
2930
2968
  class TableFormatService {
2931
2969
  // Inject các pipe/service dùng để format dữ liệu
2932
2970
  #formatNumberPipe = inject(SdFormatNumberPipe);
2971
+ // why: date cells show the wall clock of ISdCoreConfiguration.timeZone.
2972
+ #dateTime = inject(SdDateTimeService);
2933
2973
  // ==========================================
2934
2974
  // PUBLIC METHODS
2935
2975
  // ==========================================
@@ -3157,8 +3197,8 @@ class TableFormatService {
3157
3197
  return text || EMPTY_STR;
3158
3198
  }
3159
3199
  #formatDateDisplay(value, type) {
3160
- const date = DateUtilities$1.toFormat(value, 'dd/MM/yyyy');
3161
- const time = DateUtilities$1.toFormat(value, 'HH:mm:ss');
3200
+ const date = this.#dateTime.toFormat(value, 'dd/MM/yyyy');
3201
+ const time = this.#dateTime.toFormat(value, 'HH:mm:ss');
3162
3202
  if (type === 'datetime') {
3163
3203
  return time && date ? `<div class="T14R">${date}<span class="T14R text-black400 ml-4">${time}</span></div>` : '';
3164
3204
  }
@@ -3219,7 +3259,7 @@ const buildColumnWidthMap = (column) => {
3219
3259
  return result;
3220
3260
  };
3221
3261
 
3222
- const sortValue = (value, type) => {
3262
+ const sortValue = (value, type, dateTime) => {
3223
3263
  if (value === null || value === undefined || value === '')
3224
3264
  return null;
3225
3265
  if (type === 'number') {
@@ -3227,15 +3267,21 @@ const sortValue = (value, type) => {
3227
3267
  return Number.isNaN(number) ? null : number;
3228
3268
  }
3229
3269
  if (type === 'date' || type === 'datetime' || type === 'time') {
3230
- const time = value instanceof Date ? value.getTime() : typeof value === 'string' || typeof value === 'number' ? new Date(value).getTime() : NaN;
3270
+ // why: a time of day has no zone, so only date/datetime values are read in the configured zone.
3271
+ const zone = type !== 'time' && dateTime && dateTime.timeZone !== 'local' ? dateTime : undefined;
3272
+ const parse = (input) => (zone ? (zone.parse(input)?.getTime() ?? NaN) : new Date(input).getTime());
3273
+ const time = value instanceof Date ? value.getTime() : typeof value === 'string' || typeof value === 'number' ? parse(value) : NaN;
3231
3274
  return Number.isNaN(time) ? null : time;
3232
3275
  }
3233
3276
  return String(value);
3234
3277
  };
3235
- /** Internal local-sort comparison; intentionally not exported from the table entry point. */
3236
- const compareLocalValues = (a, b, type, direction) => {
3237
- const first = sortValue(a, type);
3238
- const second = sortValue(b, type);
3278
+ /**
3279
+ * Internal local-sort comparison; intentionally not exported from the table entry point.
3280
+ * `dateTime` reads zone-less dates in `ISdCoreConfiguration.timeZone`; omitted or `'local'` keeps the machine zone.
3281
+ */
3282
+ const compareLocalValues = (a, b, type, direction, dateTime) => {
3283
+ const first = sortValue(a, type, dateTime);
3284
+ const second = sortValue(b, type, dateTime);
3239
3285
  // why: xếp rỗng trước khi xét chiều để DESC vẫn giữ rỗng ở cuối và không đảo các dòng bằng nhau.
3240
3286
  if (first === null)
3241
3287
  return second === null ? 0 : 1;
@@ -3248,26 +3294,100 @@ const compareLocalValues = (a, b, type, direction) => {
3248
3294
  /* eslint-disable @typescript-eslint/no-explicit-any */
3249
3295
  const isTableItem = (item) => !!item && typeof item === 'object' && 'data' in item && 'meta' in item;
3250
3296
  const resolveItemData = (item) => (isTableItem(item) ? item.data : item);
3297
+ const hasValue = (value) => value !== undefined && value !== null && value !== '';
3251
3298
  const hasActiveColumnFilter = (rawColumnFilter = {}, columns = []) => columns.some(col => {
3252
3299
  const v = rawColumnFilter[col.field];
3253
3300
  if (Array.isArray(v))
3254
3301
  return v.length > 0;
3302
+ // why: a split-number bound of 0 is a value, not an empty end.
3255
3303
  if (v && typeof v === 'object')
3256
- return !!v.from || !!v.to;
3257
- return v !== undefined && v !== null && v !== '';
3304
+ return hasValue(v.from) || hasValue(v.to);
3305
+ return hasValue(v);
3258
3306
  });
3259
- const matchesColumnFilter = (data, columns = [], rawColumnFilter = {}) => {
3307
+ // why: the same fallback SdConvertToPagingReq uses, so a local table filters like its server twin.
3308
+ const resolveOperator = (column, columnOperator) => columnOperator[column.field] || column.filter?.operator?.default;
3309
+ // Both sides are already trimmed and lowercased; no operator keeps the historical CONTAIN.
3310
+ const matchesText = (value, operator, operand) => {
3311
+ switch (operator) {
3312
+ case 'EQUAL':
3313
+ return value === operand;
3314
+ case 'NOT_EQUAL':
3315
+ return value !== operand;
3316
+ case 'NOT_CONTAIN':
3317
+ return !value.includes(operand);
3318
+ case 'START_WITH':
3319
+ return value.startsWith(operand);
3320
+ case 'NOT_START_WITH':
3321
+ return !value.startsWith(operand);
3322
+ case 'END_WITH':
3323
+ return value.endsWith(operand);
3324
+ case 'NOT_END_WITH':
3325
+ return !value.endsWith(operand);
3326
+ default:
3327
+ return value.includes(operand);
3328
+ }
3329
+ };
3330
+ const NUMBER_PREFIX_OPERATORS = {
3331
+ '>=': 'GREATER_OR_EQUAL',
3332
+ '<=': 'LESS_OR_EQUAL',
3333
+ '>': 'GREATER_THAN',
3334
+ '<': 'LESS_THAN',
3335
+ };
3336
+ const compareNumber = (value, operator, operand) => {
3337
+ switch (operator) {
3338
+ case 'NOT_EQUAL':
3339
+ return value !== operand;
3340
+ case 'GREATER_THAN':
3341
+ return value > operand;
3342
+ case 'GREATER_OR_EQUAL':
3343
+ return value >= operand;
3344
+ case 'LESS_THAN':
3345
+ return value < operand;
3346
+ case 'LESS_OR_EQUAL':
3347
+ return value <= operand;
3348
+ default:
3349
+ return value === operand;
3350
+ }
3351
+ };
3352
+ const toFiniteNumber = (value) => NumberUtilities$1.parseFiniteNumber(value, { trim: true });
3353
+ const matchesNumberFilter = (rawValue, filter, operator) => {
3354
+ // why: the split-number filter emits `{ from, to }`; a missing end leaves that side open.
3355
+ if (filter && typeof filter === 'object' && !Array.isArray(filter)) {
3356
+ const from = toFiniteNumber(filter.from);
3357
+ const to = toFiniteNumber(filter.to);
3358
+ if (from === null && to === null)
3359
+ return true;
3360
+ const value = toFiniteNumber(rawValue);
3361
+ return value !== null && (from === null || value >= from) && (to === null || value <= to);
3362
+ }
3363
+ // why: a typed prefix such as `>=5` is explicit, so it wins over the operator dropdown.
3364
+ const prefixed = typeof filter === 'string' ? /^\s*(>=|<=|>|<)(.*)$/.exec(filter) : null;
3365
+ const operand = toFiniteNumber(prefixed ? prefixed[2] : filter);
3366
+ if (operand === null)
3367
+ return true;
3368
+ const value = toFiniteNumber(rawValue);
3369
+ return value !== null && compareNumber(value, prefixed ? NUMBER_PREFIX_OPERATORS[prefixed[1]] : operator, operand);
3370
+ };
3371
+ const matchesColumnFilter = (data, columns = [], rawColumnFilter = {}, dateTime, columnOperator = {}) => {
3372
+ const zoned = dateTime && dateTime.timeZone !== 'local' ? dateTime : undefined;
3260
3373
  for (const column of columns) {
3261
3374
  const { field, type } = column;
3262
- const filterValue = (rawColumnFilter[field] || '').toString().trim().toLowerCase();
3263
3375
  const rawColVal = resolveFieldValue(data, field);
3376
+ // why: numbers bypass the text normalisation below, which reads a filter or row value of 0 as empty.
3377
+ if (type === 'number') {
3378
+ if (!matchesNumberFilter(rawColVal, rawColumnFilter[field], resolveOperator(column, columnOperator))) {
3379
+ return false;
3380
+ }
3381
+ continue;
3382
+ }
3383
+ const filterValue = (rawColumnFilter[field] || '').toString().trim().toLowerCase();
3264
3384
  const columnValue = (rawColVal || '').toString().trim().toLowerCase();
3265
3385
  if (filterValue) {
3266
3386
  if (!columnValue && type !== 'datetime' && type !== 'date' && type !== 'time') {
3267
3387
  return false;
3268
3388
  }
3269
3389
  if (type === 'string') {
3270
- if (columnValue.indexOf(filterValue) === -1) {
3390
+ if (!matchesText(columnValue, resolveOperator(column, columnOperator), filterValue)) {
3271
3391
  return false;
3272
3392
  }
3273
3393
  }
@@ -3289,30 +3409,6 @@ const matchesColumnFilter = (data, columns = [], rawColumnFilter = {}) => {
3289
3409
  return false;
3290
3410
  }
3291
3411
  }
3292
- else if (type === 'number') {
3293
- const fValue = +filterValue.replace('>=', '').replace('<=', '').replace('>', '').replace('<', '');
3294
- const cValue = +columnValue;
3295
- if (fValue || fValue === 0) {
3296
- if (!cValue && cValue !== 0) {
3297
- return false;
3298
- }
3299
- if (filterValue.indexOf('>=') > -1 && cValue < fValue) {
3300
- return false;
3301
- }
3302
- else if (filterValue.indexOf('<=') > -1 && cValue > fValue) {
3303
- return false;
3304
- }
3305
- else if (filterValue.indexOf('<') > -1 && cValue >= fValue) {
3306
- return false;
3307
- }
3308
- else if (filterValue.indexOf('>') > -1 && cValue <= fValue) {
3309
- return false;
3310
- }
3311
- else if (cValue !== fValue) {
3312
- return false;
3313
- }
3314
- }
3315
- }
3316
3412
  else if (type === 'boolean') {
3317
3413
  if ((filterValue === '1' || filterValue === 'true') && columnValue !== '1' && columnValue !== 'true') {
3318
3414
  return false;
@@ -3324,13 +3420,16 @@ const matchesColumnFilter = (data, columns = [], rawColumnFilter = {}) => {
3324
3420
  else if (type === 'datetime' || type === 'date' || type === 'time') {
3325
3421
  const from = rawColumnFilter[field]?.from ?? rawColumnFilter[field];
3326
3422
  const to = rawColumnFilter[field]?.to ?? rawColumnFilter[field];
3327
- const fromDate = DateUtilities$1.begin(from);
3328
- const toDate = DateUtilities$1.end(to);
3423
+ // why: a time of day has no zone, so only date/datetime rows are read in the configured zone.
3424
+ const zone = type === 'time' ? undefined : zoned;
3425
+ const fromDate = (zone ?? _SD_LOCAL_DATE_TIME).begin(from);
3426
+ const toDate = (zone ?? _SD_LOCAL_DATE_TIME).end(to);
3329
3427
  if (fromDate || toDate) {
3330
3428
  if (!columnValue) {
3331
3429
  return false;
3332
3430
  }
3333
- const columnTime = new Date(columnValue).getTime();
3431
+ // why: the zone branch parses the raw row value; `columnValue` is lowercased for text matching.
3432
+ const columnTime = zone ? (zone.parse(rawColVal)?.getTime() ?? NaN) : new Date(columnValue).getTime();
3334
3433
  const fromDateTime = fromDate?.getTime() || null;
3335
3434
  const toDateTime = toDate?.getTime() || null;
3336
3435
  if (fromDateTime && fromDateTime > columnTime) {
@@ -3348,7 +3447,7 @@ const matchesColumnFilter = (data, columns = [], rawColumnFilter = {}) => {
3348
3447
  const filterLocalItems = (localItems, option, filterInfo) => {
3349
3448
  const { columns } = option;
3350
3449
  const { rawColumnFilter, orderBy, orderDirection, pageSize, pageNumber } = filterInfo;
3351
- const matchesData = (data) => matchesColumnFilter(data, columns, rawColumnFilter);
3450
+ const matchesData = (data) => matchesColumnFilter(data, columns, rawColumnFilter, option.dateTime, filterInfo.columnOperator);
3352
3451
  const treeOpt = option.tree;
3353
3452
  const treeSearch = option.type === 'local' && treeOpt?.loadType === 'static' && hasActiveColumnFilter(rawColumnFilter, columns);
3354
3453
  const treeSearchPredicate = treeSearch ? matchesData : undefined;
@@ -3362,7 +3461,7 @@ const filterLocalItems = (localItems, option, filterInfo) => {
3362
3461
  items.sort((tableItemCurrent, tableItemNext) => {
3363
3462
  const dataVal = resolveFieldValue(resolveItemData(tableItemCurrent), field);
3364
3463
  const nextVal = resolveFieldValue(resolveItemData(tableItemNext), field);
3365
- return compareLocalValues(dataVal, nextVal, type, orderDirection);
3464
+ return compareLocalValues(dataVal, nextVal, type, orderDirection, option.dateTime);
3366
3465
  });
3367
3466
  }
3368
3467
  }
@@ -3853,6 +3952,8 @@ class SdTable {
3853
3952
  #i18n = inject(I18nService);
3854
3953
  #tableFormatService = inject(TableFormatService);
3855
3954
  #tableExportService = inject(TableExportService);
3955
+ // why: filters and sort read dates in ISdCoreConfiguration.timeZone; the pure helpers take it as an argument.
3956
+ #dateTime = inject(SdDateTimeService);
3856
3957
  // Expose state signals từ TableExportService cho template binding.
3857
3958
  exporting = this.#tableExportService.exporting;
3858
3959
  exportTitle = this.#tableExportService.exportTitle;
@@ -4146,7 +4247,7 @@ class SdTable {
4146
4247
  Object.assign(cur, next);
4147
4248
  };
4148
4249
  #filterLocal = (localItems, filterInfo) => {
4149
- const result = filterLocalItems(localItems, this.tableOption(), filterInfo);
4250
+ const result = filterLocalItems(localItems, { ...this.tableOption(), dateTime: this.#dateTime }, filterInfo);
4150
4251
  this.#treeSearchPredicate = result.treeSearchPredicate;
4151
4252
  return {
4152
4253
  items: result.items,
@@ -4240,6 +4341,7 @@ class SdTable {
4240
4341
  const pagingReq = SdConvertToPagingReq(snapshot, {
4241
4342
  columns: opt.columns,
4242
4343
  externalFilters: opt.filter?.externalFilters,
4344
+ dateTime: this.#dateTime,
4243
4345
  });
4244
4346
  const optionRevision = this.#optionRevision;
4245
4347
  const sourceOption = this.option();
@@ -4449,6 +4551,7 @@ class SdTable {
4449
4551
  const pagingReq = SdConvertToPagingReq(filterReq, {
4450
4552
  columns: opt.columns,
4451
4553
  externalFilters: opt.filter?.externalFilters,
4554
+ dateTime: this.#dateTime,
4452
4555
  });
4453
4556
  const result = opt.items(filterReq, pagingReq);
4454
4557
  return await result;
@@ -4827,6 +4930,11 @@ class SdTable {
4827
4930
  if (operator == null)
4828
4931
  return;
4829
4932
  this.tableOption()?.filter?.operatorChange?.(column, operator);
4933
+ // why: a new operator changes what the current value means, so filter again; an empty cell only keeps the operator.
4934
+ if (hasActiveColumnFilter(this.columnFilter, [column]))
4935
+ this.onFilterChange();
4936
+ else
4937
+ this.onFilterCommit();
4830
4938
  };
4831
4939
  trackBy = (index, item) => {
4832
4940
  return item.meta.id;