pcm-shared-components 2.1.386 → 2.1.387

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.
@@ -78,6 +78,7 @@ export const AnimalVehicleDetailsEditor = props => {
78
78
  items,
79
79
  fieldConfig,
80
80
  isFieldAnswered,
81
+ isFieldDrifted,
81
82
  onFieldChange,
82
83
  onAddItem,
83
84
  onRemoveItem,
@@ -112,6 +113,15 @@ export const AnimalVehicleDetailsEditor = props => {
112
113
  onRemoveItem(pendingRemoveId);
113
114
  setPendingRemoveId(null);
114
115
  };
116
+
117
+ // A drifted field (its stored value no longer matches this field's current type/options -- see
118
+ // isFieldDrifted) renders read-only instead of feeding a mismatched value into a live
119
+ // type-specific control (e.g. never shove a raw string into a date picker). "Replace" reveals
120
+ // the normal control for that one field, EMPTY rather than pre-filled with the drifted value, so
121
+ // the only way the drifted value leaves storage is a deliberate fresh answer overwriting it on
122
+ // save. Keyed per item+field so revealing one animal's drifted field never affects another.
123
+ const [revealedDriftKeys, setRevealedDriftKeys] = useState(() => new Set());
124
+ const revealDrift = revealKey => setRevealedDriftKeys(prev => new Set(prev).add(revealKey));
115
125
  const [selectedIndex, setSelectedIndex] = useState(0);
116
126
  // Growing the list (Add) jumps to the newly-added last item, since that's the one the host just
117
127
  // asked for and the one that needs filling in. Shrinking it (Remove) just clamps the current
@@ -260,12 +270,64 @@ export const AnimalVehicleDetailsEditor = props => {
260
270
  gridAutoFlow: 'row dense'
261
271
  }
262
272
  }, fieldConfig.map(field => {
263
- const value = selectedItem.values?.[field.key] ?? '';
273
+ const rawValue = selectedItem.values?.[field.key] ?? '';
274
+ const revealKey = `${selectedItem.id}_${field.key}`;
275
+ const wasDrifted = isFieldDrifted(field, rawValue);
276
+ const showAsDrifted = wasDrifted && !revealedDriftKeys.has(revealKey);
277
+ // Once a drifted field is revealed, the live control gets an EMPTY value, never
278
+ // the mismatched raw one -- a drifted answer is never editable-in-place, only
279
+ // replaceable. Not drifted (or already revealed): behaves exactly as before.
280
+ const value = showAsDrifted ? rawValue : wasDrifted ? field.type === 'select_multiple' ? [] : '' : rawValue;
264
281
  const showError = field.required && !isFieldAnswered(field.type, value);
265
282
  const labelText = field.required ? `${field.label} *` : `${field.label} (${i18next.t('common.app.optional')})`;
266
283
  const wrapperClassName = ['select', 'textarea', 'select_multiple', 'yes_no'].includes(field.type) ? 'col-span-full' : undefined;
267
284
  let control;
268
- if (field.type === 'number') {
285
+ if (showAsDrifted) {
286
+ // Generic stringify -- this renders whatever type the field used to be, which
287
+ // may no longer match field.type at all, so it can't reuse any type-specific
288
+ // control (a boolean/array/string all need to display as plain read text here).
289
+ const displayText = typeof rawValue === 'boolean' ? rawValue ? i18next.t('common.app.yes') : i18next.t('common.app.no') : Array.isArray(rawValue) ? rawValue.join(', ') : String(rawValue);
290
+ control = /*#__PURE__*/React.createElement("div", {
291
+ className: "flex flex-col",
292
+ style: {
293
+ gap: 4
294
+ }
295
+ }, /*#__PURE__*/React.createElement(Paper, {
296
+ variant: "outlined",
297
+ className: "flex items-center",
298
+ sx: {
299
+ gap: 6,
300
+ px: 1.5,
301
+ py: 1,
302
+ bgcolor: 'action.hover'
303
+ }
304
+ }, /*#__PURE__*/React.createElement(WarningAmberIcon, {
305
+ fontSize: "small",
306
+ color: "warning"
307
+ }), /*#__PURE__*/React.createElement(Typography, {
308
+ variant: "body2",
309
+ sx: {
310
+ flex: 1,
311
+ wordBreak: 'break-word'
312
+ }
313
+ }, displayText)), /*#__PURE__*/React.createElement("div", {
314
+ className: "flex items-center",
315
+ style: {
316
+ gap: 4
317
+ }
318
+ }, /*#__PURE__*/React.createElement(Typography, {
319
+ variant: "caption",
320
+ color: "text.secondary"
321
+ }, i18next.t('animal.field_drifted_hint')), /*#__PURE__*/React.createElement(Button, {
322
+ size: "small",
323
+ onClick: () => revealDrift(revealKey),
324
+ sx: {
325
+ textTransform: 'none',
326
+ minWidth: 0,
327
+ p: 0
328
+ }
329
+ }, i18next.t('animal.field_drifted_replace'))));
330
+ } else if (field.type === 'number') {
269
331
  control = /*#__PURE__*/React.createElement(TextField, {
270
332
  name: field.key,
271
333
  value: value,
@@ -407,7 +469,7 @@ export const AnimalVehicleDetailsEditor = props => {
407
469
  }
408
470
  return /*#__PURE__*/React.createElement("div", {
409
471
  className: wrapperClassName,
410
- key: `${selectedItem.id}_${field.key}`,
472
+ key: revealKey,
411
473
  "data-field-invalid": showError ? 'true' : undefined
412
474
  }, /*#__PURE__*/React.createElement(Typography, {
413
475
  variant: "body2",
@@ -416,7 +478,7 @@ export const AnimalVehicleDetailsEditor = props => {
416
478
  mb: 0.5,
417
479
  color: showError ? 'error.main' : 'text.primary'
418
480
  }
419
- }, labelText), control, field.type === 'yes_no' && showError && /*#__PURE__*/React.createElement(Typography, {
481
+ }, labelText), control, field.type === 'yes_no' && !showAsDrifted && showError && /*#__PURE__*/React.createElement(Typography, {
420
482
  variant: "caption",
421
483
  color: "error",
422
484
  className: "block mt-4"
@@ -470,6 +532,7 @@ AnimalVehicleDetailsEditor.defaultProps = {
470
532
  onFieldChange: () => {},
471
533
  onAddItem: () => {},
472
534
  onRemoveItem: () => {},
535
+ isFieldDrifted: () => false,
473
536
  canAddMore: true,
474
537
  maxItems: 0,
475
538
  className: 'w-full',
@@ -498,6 +561,11 @@ AnimalVehicleDetailsEditor.propTypes = {
498
561
  /** (type, value) => boolean. Injected rather than owned internally, so each host app's existing
499
562
  * "has this field actually been answered" rule (yes_no's `false` counts, etc.) stays authoritative. */
500
563
  isFieldAnswered: PropTypes.func.isRequired,
564
+ /** (field, rawValue) => boolean. Optional -- defaults to always-false (no drift concept at all,
565
+ * e.g. vehicle fields, which have no admin config to drift). When true for a field/value pair,
566
+ * that field renders read-only with a warning + "replace" action instead of feeding a value that
567
+ * no longer matches the field's current type/options into a live type-specific control. */
568
+ isFieldDrifted: PropTypes.func,
501
569
  /** (itemId, fieldKey, value) => void */
502
570
  onFieldChange: PropTypes.func,
503
571
  /** () => void -- host owns the actual array/count mutation. */
@@ -0,0 +1,83 @@
1
+ // A lot's admin-configurable animal fields (label, type, options, required, unit) can change at
2
+ // any time, but a reservation's field_values were entered under whatever the config looked like
3
+ // at that moment. This classifies one stored (field, rawValue) pair against the field's CURRENT
4
+ // live config, so a caller can tell three states apart instead of collapsing "answered under a
5
+ // since-changed config" into either "answered" (misleading -- the value may no longer make sense
6
+ // under today's type/options) or "unanswered" (wrong -- silently hides real historical data).
7
+ //
8
+ // This is intentionally a superset of the isAnimalFieldAnswered every host app already owns: that
9
+ // function only asks "is this non-empty" (its job is required-field validation), this one also
10
+ // asks "does it still make sense under today's config" (its job is drift detection). Every
11
+ // per-type completeness threshold below matches isAnimalFieldAnswered exactly (including the
12
+ // legacy 'text' 2-char minimum, deliberately not extended to other types -- see the identical rule
13
+ // there), so a value that was always genuinely incomplete still classifies as 'unanswered', not
14
+ // 'drifted' -- drift only fires for content that's substantial but doesn't fit the current type's
15
+ // shape (or, for select/select_multiple, isn't in the current option list -- the one check
16
+ // isAnimalFieldAnswered can't make since it isn't given the field's options). This mirrors -- does
17
+ // not replace -- the same per-type shape rules the PHP validators enforce server-side
18
+ // (validate_reservation_edit.php / validate_reservation_new.php), so a value the backend would now
19
+ // blank on a genuine edit is exactly the value this flags as 'drifted' on display.
20
+ //
21
+ // fieldConfig is `null` when the field has been fully retired (no longer in the lot's live config
22
+ // at all) -- there's no type left to hold a value to, so any real content at all is 'drifted'.
23
+
24
+ // Generic "is there real content here, regardless of shape" check -- used only to tell 'drifted'
25
+ // (wrong shape, but something was clearly entered) apart from 'unanswered' (nothing meaningful was
26
+ // ever entered) once a type-specific branch has already ruled out "answered".
27
+ const hasWrongShapeContent = rawValue => {
28
+ if (typeof rawValue === 'boolean') return true;
29
+ if (Array.isArray(rawValue)) return rawValue.length > 0;
30
+ if (typeof rawValue === 'string') return rawValue.trim().length > 0;
31
+ return rawValue !== null && rawValue !== undefined;
32
+ };
33
+ export const classifyAnimalFieldValue = (fieldConfig, rawValue) => {
34
+ if (!fieldConfig) {
35
+ return hasWrongShapeContent(rawValue) ? 'drifted' : 'unanswered';
36
+ }
37
+ const {
38
+ type,
39
+ options
40
+ } = fieldConfig;
41
+ if (type === 'yes_no') {
42
+ if (typeof rawValue === 'boolean') return 'answered';
43
+ return hasWrongShapeContent(rawValue) ? 'drifted' : 'unanswered';
44
+ }
45
+ if (type === 'select_multiple') {
46
+ if (Array.isArray(rawValue)) {
47
+ if (rawValue.length === 0) return 'unanswered';
48
+ const allowed = Array.isArray(options) ? options : [];
49
+ return rawValue.every(v => allowed.includes(v)) ? 'answered' : 'drifted';
50
+ }
51
+ return hasWrongShapeContent(rawValue) ? 'drifted' : 'unanswered';
52
+ }
53
+ if (type === 'select') {
54
+ if (typeof rawValue === 'string' && rawValue !== '') {
55
+ const allowed = Array.isArray(options) ? options : [];
56
+ return allowed.includes(rawValue) ? 'answered' : 'drifted';
57
+ }
58
+ return hasWrongShapeContent(rawValue) ? 'drifted' : 'unanswered';
59
+ }
60
+ if (type === 'number') {
61
+ if (typeof rawValue === 'string' && rawValue.length >= 1) {
62
+ return !Number.isNaN(Number(rawValue)) ? 'answered' : 'drifted';
63
+ }
64
+ return hasWrongShapeContent(rawValue) ? 'drifted' : 'unanswered';
65
+ }
66
+ if (type === 'date') {
67
+ if (typeof rawValue === 'string' && rawValue.length >= 1) {
68
+ return /^\d{4}-\d{2}-\d{2}$/.test(rawValue) ? 'answered' : 'drifted';
69
+ }
70
+ return hasWrongShapeContent(rawValue) ? 'drifted' : 'unanswered';
71
+ }
72
+ if (type === 'textarea') {
73
+ if (typeof rawValue === 'string' && rawValue.length >= 1) return 'answered';
74
+ return hasWrongShapeContent(rawValue) ? 'drifted' : 'unanswered';
75
+ }
76
+ // Legacy 'text' (and the fallback for a missing/unrecognized type): 2-char minimum, matching
77
+ // isAnimalFieldAnswered exactly -- a single stray keystroke was never a real answer, config
78
+ // drift or not, so it's 'unanswered' rather than 'drifted'.
79
+ if (typeof rawValue === 'string' && rawValue.length >= 2) return 'answered';
80
+ if (typeof rawValue === 'string' && rawValue.length === 1) return 'unanswered';
81
+ return hasWrongShapeContent(rawValue) ? 'drifted' : 'unanswered';
82
+ };
83
+ export default classifyAnimalFieldValue;
package/dist/index.js CHANGED
@@ -37,4 +37,5 @@ import VerticalImageTile from './components/Images/VerticalImageTile';
37
37
  import EventCard from './components/Cards/EventCard';
38
38
  import ImageAssets from './components/Icons/ImageAssets';
39
39
  import AnimalVehicleDetailsEditor from './components/Reservation/AnimalVehicleDetailsEditor';
40
- export { TestButton, CodeHighlight, CustomFab, HelpButton, GenericDialogWindow, PcSharedComponentThemeInheritor, ImageCanvasDraw, PCMScrollbar, TwoColumnDisplay, SimpleTab, HrefLink, GenericAppBar, ReusablePopupMenu, SearchBar, TillButton, ProductItemButton, DropdownButton, translationResources, coreLocals, currencySymbol, DateRangeCalendar, DateCalendar, SimpleLabel, CurrencyTextInput, ImageStack, PlatformPaymentPlans, PlatformPlanTitles, PlatformPlanPrices, LoadingBackdrop, SimpleAccordion, PricingCalendar, FancyDate, HorizontalImageTile, VerticalImageTile, EventCard, ImageAssets, AnimalVehicleDetailsEditor };
40
+ import { classifyAnimalFieldValue } from './components/Reservation/animalFieldDrift';
41
+ export { TestButton, CodeHighlight, CustomFab, HelpButton, GenericDialogWindow, PcSharedComponentThemeInheritor, ImageCanvasDraw, PCMScrollbar, TwoColumnDisplay, SimpleTab, HrefLink, GenericAppBar, ReusablePopupMenu, SearchBar, TillButton, ProductItemButton, DropdownButton, translationResources, coreLocals, currencySymbol, DateRangeCalendar, DateCalendar, SimpleLabel, CurrencyTextInput, ImageStack, PlatformPaymentPlans, PlatformPlanTitles, PlatformPlanPrices, LoadingBackdrop, SimpleAccordion, PricingCalendar, FancyDate, HorizontalImageTile, VerticalImageTile, EventCard, ImageAssets, AnimalVehicleDetailsEditor, classifyAnimalFieldValue };
@@ -36,7 +36,10 @@
36
36
  "complete_required_fields": "Complete {{count}} required field",
37
37
  "complete_required_fields_plural": "Complete {{count}} required fields",
38
38
  "remove_confirm_title": "Remove this animal?",
39
- "remove_confirm_message": "All information entered for this animal will be lost. This can't be undone."
39
+ "remove_confirm_message": "All information entered for this animal will be lost. This can't be undone.",
40
+ "field_drifted_hint": "This answer no longer matches this question's current setup.",
41
+ "field_drifted_replace": "Enter a new answer",
42
+ "field_removed": "This question no longer exists in this lot's setup."
40
43
  },
41
44
  "app": {
42
45
  "common": {
@@ -2460,7 +2463,11 @@
2460
2463
  "unit_label": "Unit",
2461
2464
  "unit_hint": "Shown after the number, e.g. kg",
2462
2465
  "delete_field_confirm_title": "Delete this field?",
2463
- "delete_field_confirm_text": "\"{{label}}\" will be removed from the list. Answers guests already gave for it on existing reservations aren't deleted, but the field won't be asked again."
2466
+ "delete_field_confirm_text": "\"{{label}}\" will be removed from the list. Answers guests already gave for it on existing reservations aren't deleted, but the field won't be asked again.",
2467
+ "change_type_confirm_title": "Change field type?",
2468
+ "change_type_confirm_text": "{{count}} reservation(s) already have an answer for \"{{label}}\". Changing its type may make that answer display incorrectly. Continue?",
2469
+ "remove_option_confirm_title": "Remove option?",
2470
+ "remove_option_confirm_text": "{{count}} reservation(s) have an answer using an option you're removing from \"{{label}}\". Their answer will no longer match this field's choices. Continue?"
2464
2471
  },
2465
2472
  "edit_lot": "Edit Lot",
2466
2473
  "lot_id_exist": "Lot number already exist",
@@ -36,7 +36,10 @@
36
36
  "complete_required_fields": "Complete {{count}} campo requerido",
37
37
  "complete_required_fields_plural": "Complete {{count}} campos requeridos",
38
38
  "remove_confirm_title": "¿Eliminar este animal?",
39
- "remove_confirm_message": "Toda la información ingresada para este animal se perderá. Esto no se puede deshacer."
39
+ "remove_confirm_message": "Toda la información ingresada para este animal se perderá. Esto no se puede deshacer.",
40
+ "field_drifted_hint": "Esta respuesta ya no coincide con la configuración actual de esta pregunta.",
41
+ "field_drifted_replace": "Ingrese una nueva respuesta",
42
+ "field_removed": "Esta pregunta ya no existe en la configuración de este lote."
40
43
  },
41
44
  "app": {
42
45
  "common": {
@@ -2460,7 +2463,11 @@
2460
2463
  "unit_label": "Unidad",
2461
2464
  "unit_hint": "Se muestra después del número, ej. kg",
2462
2465
  "delete_field_confirm_title": "¿Eliminar este campo?",
2463
- "delete_field_confirm_text": "\"{{label}}\" será eliminado de la lista. Las respuestas que los huéspedes ya han proporcionado no se eliminarán, pero el campo no se volverá a solicitar."
2466
+ "delete_field_confirm_text": "\"{{label}}\" será eliminado de la lista. Las respuestas que los huéspedes ya han proporcionado no se eliminarán, pero el campo no se volverá a solicitar.",
2467
+ "change_type_confirm_title": "¿Cambiar el tipo de campo?",
2468
+ "change_type_confirm_text": "{{count}} reserva(s) ya tienen una respuesta para \"{{label}}\". Cambiar su tipo puede hacer que esa respuesta se muestre incorrectamente. ¿Continuar?",
2469
+ "remove_option_confirm_title": "¿Eliminar la opción?",
2470
+ "remove_option_confirm_text": "{{count}} reserva(s) tienen una respuesta usando una opción que está eliminando de \"{{label}}\". Su respuesta ya no coincidirá con las opciones de este campo. ¿Continuar?"
2464
2471
  },
2465
2472
  "edit_lot": "Editar lote",
2466
2473
  "lot_id_exist": "El número de lote ya existe",
@@ -36,7 +36,10 @@
36
36
  "complete_required_fields": "Complétez {{count}} champ requis",
37
37
  "complete_required_fields_plural": "Complétez {{count}} champs requis",
38
38
  "remove_confirm_title": "Supprimer cet animal ?",
39
- "remove_confirm_message": "Toutes les informations saisies pour cet animal seront perdues. Cela ne peut pas être annulé."
39
+ "remove_confirm_message": "Toutes les informations saisies pour cet animal seront perdues. Cela ne peut pas être annulé.",
40
+ "field_drifted_hint": "Cette réponse ne correspond plus à la configuration actuelle de cette question.",
41
+ "field_drifted_replace": "Entrez une nouvelle réponse",
42
+ "field_removed": "Cette question n'existe plus dans la configuration de ce terrain."
40
43
  },
41
44
  "app": {
42
45
  "common": {
@@ -2460,7 +2463,11 @@
2460
2463
  "unit_label": "Unité",
2461
2464
  "unit_hint": "Affiché après le nombre, ex. kg",
2462
2465
  "delete_field_confirm_title": "Supprimer ce champ ?",
2463
- "delete_field_confirm_text": "« {{label}} » sera supprimé de la liste. Les réponses que les clients ont déjà fournies ne seront pas supprimées, mais le champ ne sera plus demandé."
2466
+ "delete_field_confirm_text": "« {{label}} » sera supprimé de la liste. Les réponses que les clients ont déjà fournies ne seront pas supprimées, mais le champ ne sera plus demandé.",
2467
+ "change_type_confirm_title": "Changer le type de champ ?",
2468
+ "change_type_confirm_text": "{{count}} réservation(s) ont déjà une réponse pour « {{label}} ». Modifier son type peut faire afficher incorrectement cette réponse. Continuer ?",
2469
+ "remove_option_confirm_title": "Supprimer l'option ?",
2470
+ "remove_option_confirm_text": "{{count}} réservation(s) ont une réponse utilisant une option que vous supprimez de « {{label}} ». Leur réponse ne correspondra plus aux choix de ce champ. Continuer ?"
2464
2471
  },
2465
2472
  "edit_lot": "Editer le lot",
2466
2473
  "lot_id_exist": "Le numéro de lot existe déjà",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pcm-shared-components",
3
- "version": "2.1.386",
3
+ "version": "2.1.387",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "babel": {