diff --git a/src/app/(agent)/agent/edit/components/DynamicField.tsx b/src/app/(agent)/agent/edit/components/DynamicField.tsx
index 0279051..e566b43 100644
--- a/src/app/(agent)/agent/edit/components/DynamicField.tsx
+++ b/src/app/(agent)/agent/edit/components/DynamicField.tsx
@@ -56,6 +56,7 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
onChange={(val: string) => handleChange(val)}
required={field.isRequired}
error={error}
+ maxLength={field.validation?.maxLength}
/>
);
@@ -68,6 +69,8 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
onChange={(val: string) => handleChange(val)}
required={field.isRequired}
rows={4}
+ maxLength={field.validation?.maxLength}
+ error={error}
/>
);
@@ -95,6 +98,8 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
onChange={(val: string) => handleChange(val)}
required={field.isRequired}
error={error}
+ min={field.validation?.minDate}
+ max={field.validation?.maxDate}
/>
);
diff --git a/src/app/(agent)/agent/edit/components/FormInput.tsx b/src/app/(agent)/agent/edit/components/FormInput.tsx
index 90ec3f1..80ac951 100644
--- a/src/app/(agent)/agent/edit/components/FormInput.tsx
+++ b/src/app/(agent)/agent/edit/components/FormInput.tsx
@@ -9,11 +9,12 @@ interface FormInputProps {
disabled?: boolean;
required?: boolean;
error?: string;
- min?: number;
- max?: number;
+ min?: number | string;
+ max?: number | string;
+ maxLength?: number;
}
-export function FormInput({ label, value, onChange, placeholder, type = 'text', disabled, required, error, min, max }: FormInputProps) {
+export function FormInput({ label, value, onChange, placeholder, type = 'text', disabled, required, error, min, max, maxLength }: FormInputProps) {
return (
{/* Stats Section */}
-
+
{data.stats.map((stat, index) => (
-
+
-
-
- {stat.boldText}
-
-
-
- {stat.normalText}
-
-
+
+ {stat.boldText}{' '}
+ {stat.normalText}
+
))}
diff --git a/src/services/profile-sections.service.ts b/src/services/profile-sections.service.ts
index 207401e..26470ba 100644
--- a/src/services/profile-sections.service.ts
+++ b/src/services/profile-sections.service.ts
@@ -36,6 +36,8 @@ export interface FieldValidation {
minLength?: number;
maxLength?: number;
pattern?: string;
+ minDate?: string; // ISO date (YYYY-MM-DD) for DATE fields
+ maxDate?: string; // ISO date (YYYY-MM-DD) for DATE fields
allowedFormats?: string[];
maxFileSize?: number;
}
diff --git a/src/utils/profileDataMapper.ts b/src/utils/profileDataMapper.ts
index 0e1ee5f..42dfd0f 100644
--- a/src/utils/profileDataMapper.ts
+++ b/src/utils/profileDataMapper.ts
@@ -291,22 +291,11 @@ export function mapFieldValuesToAvailability(fieldValues: FieldValueResponse[]):
return trimmed;
};
- // Drop stale/orphan options. Backend returns `valueLabel[i]` equal to the raw `value[i]`
- // when no option match is found (admin renamed/removed the option). Those are orphans —
- // skip them entirely so old selections don't linger on the public profile.
- const resolvedLabels: string[] = [];
- rawValues.forEach((val, i) => {
- const raw = String(val || '').trim();
- if (!raw) return;
- const fromBackend = labelValues[i];
- const hasResolvedLabel =
- fromBackend &&
- typeof fromBackend === 'string' &&
- fromBackend.trim() &&
- fromBackend !== raw; // backend-resolved label must DIFFER from raw value
- if (!hasResolvedLabel) return; // orphan — drop
- resolvedLabels.push(formatIfSnakeCase(fromBackend));
- });
+ // Backend now strips orphan entries and returns only resolvable option labels,
+ // so we trust valueLabel as-is.
+ const resolvedLabels: string[] = labelValues
+ .filter((l): l is string => typeof l === 'string' && l.trim().length > 0)
+ .map(formatIfSnakeCase);
// Deduplicate by normalized comparison (case/whitespace insensitive)
const seen = new Set
();
diff --git a/src/utils/validateFieldValue.ts b/src/utils/validateFieldValue.ts
new file mode 100644
index 0000000..adb51ce
--- /dev/null
+++ b/src/utils/validateFieldValue.ts
@@ -0,0 +1,92 @@
+import type { ProfileField } from '@/services/profile-sections.service';
+
+/**
+ * Returns an error message if the given value fails validation for the field,
+ * or null if it passes. Enforces:
+ * - required
+ * - TEXT/TEXTAREA: minLength, maxLength, pattern
+ * - NUMBER/RANGE: min, max
+ * - DATE: minDate, maxDate
+ * - CHECKBOX_GROUP / MULTI_SELECT / TAG_INPUT / RADIO / SELECT: required non-empty
+ */
+export function validateFieldValue(field: ProfileField, value: unknown): string | null {
+ const v = field.validation ?? {};
+ const fieldName = field.name || 'This field';
+
+ const isEmpty =
+ value === undefined ||
+ value === null ||
+ value === '' ||
+ (Array.isArray(value) && value.length === 0);
+
+ if (field.isRequired && isEmpty) {
+ return `${fieldName} is required`;
+ }
+ if (isEmpty) return null; // optional + empty → OK
+
+ switch (field.fieldType) {
+ case 'TEXT':
+ case 'TEXTAREA': {
+ const str = String(value);
+ if (typeof v.minLength === 'number' && str.length < v.minLength) {
+ return `${fieldName} must be at least ${v.minLength} characters`;
+ }
+ if (typeof v.maxLength === 'number' && str.length > v.maxLength) {
+ return `${fieldName} must be no more than ${v.maxLength} characters`;
+ }
+ if (v.pattern && typeof v.pattern === 'string') {
+ try {
+ if (!new RegExp(v.pattern).test(str)) {
+ return `${fieldName} has an invalid format`;
+ }
+ } catch {
+ // Invalid regex in admin config — silently skip
+ }
+ }
+ return null;
+ }
+
+ case 'NUMBER':
+ case 'RANGE': {
+ const n = typeof value === 'number' ? value : parseFloat(String(value));
+ if (!isFinite(n)) return `${fieldName} must be a number`;
+ if (typeof v.min === 'number' && n < v.min) {
+ return `${fieldName} must be at least ${v.min}`;
+ }
+ if (typeof v.max === 'number' && n > v.max) {
+ return `${fieldName} must be no more than ${v.max}`;
+ }
+ return null;
+ }
+
+ case 'DATE': {
+ const str = String(value).slice(0, 10); // YYYY-MM-DD
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(str)) {
+ return `${fieldName} must be a valid date`;
+ }
+ if (v.minDate && str < v.minDate) {
+ return `${fieldName} must be on or after ${v.minDate}`;
+ }
+ if (v.maxDate && str > v.maxDate) {
+ return `${fieldName} must be on or before ${v.maxDate}`;
+ }
+ return null;
+ }
+
+ case 'TAG_INPUT': {
+ // string items — apply maxLength per item if defined
+ if (Array.isArray(value) && typeof v.maxLength === 'number') {
+ const tooLong = (value as unknown[]).find(
+ (item) => typeof item === 'string' && item.length > v.maxLength!,
+ );
+ if (tooLong) {
+ return `Each ${fieldName.toLowerCase()} entry must be no more than ${v.maxLength} characters`;
+ }
+ }
+ return null;
+ }
+
+ default:
+ return null;
+ }
+}