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.
- package/dist/components/Reservation/AnimalVehicleDetailsEditor/index.js +72 -4
- package/dist/components/Reservation/animalFieldDrift.js +83 -0
- package/dist/index.js +2 -1
- package/dist/languages/en/translations.json +9 -2
- package/dist/languages/es/translations.json +9 -2
- package/dist/languages/fr/translations.json +9 -2
- package/package.json +1 -1
|
@@ -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
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
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à",
|