feat: Implement Phase 1 multi-child backend infrastructure
Database Migrations: - V017: Add child display preferences (display_color, sort_order, nickname) - V018: Create multi_child_preferences table for UI settings - V019: Add bulk operation tracking to activities table Backend Enhancements: - Updated Child entity with display_color, sort_order, nickname fields - Auto-assign colors to children based on family order (8-color palette) - Allow custom color selection during child creation/update - Added getFamilyStatistics() endpoint for UI view mode decisions - Added userHasAccessToChild() helper for authorization - Updated all API responses to include display fields DTOs: - Added displayColor (hex validation) and nickname to CreateChildDto - UpdateChildDto inherits new fields automatically API: - GET /api/v1/children/family/:familyId/statistics - Family stats - All child endpoints now return displayColor, sortOrder, nickname - Custom colors validated with regex pattern Color Palette: Pink (#FF6B9D), Teal (#4ECDC4), Yellow (#FFD93D), Mint (#95E1D3) Lavender (#C7CEEA), Orange (#FF8C42), Green (#A8E6CF), Purple (#B8B8FF) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,15 @@ export class Child {
|
|||||||
@Column({ name: 'photo_alt', type: 'text', nullable: true })
|
@Column({ name: 'photo_alt', type: 'text', nullable: true })
|
||||||
photoAlt?: string;
|
photoAlt?: string;
|
||||||
|
|
||||||
|
@Column({ name: 'display_color', length: 7, default: '#FF6B9D' })
|
||||||
|
displayColor: string;
|
||||||
|
|
||||||
|
@Column({ name: 'sort_order', type: 'integer', default: 0 })
|
||||||
|
sortOrder: number;
|
||||||
|
|
||||||
|
@Column({ length: 50, nullable: true })
|
||||||
|
nickname?: string;
|
||||||
|
|
||||||
@Column({ name: 'medical_info', type: 'jsonb', default: {} })
|
@Column({ name: 'medical_info', type: 'jsonb', default: {} })
|
||||||
medicalInfo: Record<string, any>;
|
medicalInfo: Record<string, any>;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
-- Migration V017: Add child display preferences
|
||||||
|
-- Created: 2025-10-04
|
||||||
|
-- Description: Adds display_color, sort_order, and nickname fields for multi-child UI support
|
||||||
|
|
||||||
|
-- Add display preferences columns to children table
|
||||||
|
ALTER TABLE children
|
||||||
|
ADD COLUMN display_color VARCHAR(7) DEFAULT '#FF6B9D',
|
||||||
|
ADD COLUMN sort_order INTEGER DEFAULT 0,
|
||||||
|
ADD COLUMN nickname VARCHAR(50);
|
||||||
|
|
||||||
|
-- Update existing children with colors based on creation order within each family
|
||||||
|
WITH numbered_children AS (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
family_id,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY family_id ORDER BY created_at) - 1 AS row_num
|
||||||
|
FROM children
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
)
|
||||||
|
UPDATE children c
|
||||||
|
SET
|
||||||
|
display_color = CASE (nc.row_num % 8)
|
||||||
|
WHEN 0 THEN '#FF6B9D' -- Pink
|
||||||
|
WHEN 1 THEN '#4ECDC4' -- Teal
|
||||||
|
WHEN 2 THEN '#FFD93D' -- Yellow
|
||||||
|
WHEN 3 THEN '#95E1D3' -- Mint
|
||||||
|
WHEN 4 THEN '#C7CEEA' -- Lavender
|
||||||
|
WHEN 5 THEN '#FF8C42' -- Orange
|
||||||
|
WHEN 6 THEN '#A8E6CF' -- Green
|
||||||
|
WHEN 7 THEN '#B8B8FF' -- Purple
|
||||||
|
END,
|
||||||
|
sort_order = nc.row_num
|
||||||
|
FROM numbered_children nc
|
||||||
|
WHERE c.id = nc.id;
|
||||||
|
|
||||||
|
-- Create composite index for efficient family queries with sorting
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_children_family_sort
|
||||||
|
ON children(family_id, sort_order, deleted_at);
|
||||||
|
|
||||||
|
-- Add comments for documentation
|
||||||
|
COMMENT ON COLUMN children.display_color IS 'Hex color code for consistent UI display across charts and cards';
|
||||||
|
COMMENT ON COLUMN children.sort_order IS 'Display order within family (0-based, typically by birth order)';
|
||||||
|
COMMENT ON COLUMN children.nickname IS 'Optional shortened name for voice commands and quick selection';
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
-- Migration V018: Create multi-child UI preferences table
|
||||||
|
-- Created: 2025-10-04
|
||||||
|
-- Description: Stores user preferences for multi-child UI behavior and default selections
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS multi_child_preferences (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
user_id VARCHAR(20) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- View Mode Preferences
|
||||||
|
view_mode VARCHAR(20) DEFAULT 'auto', -- 'tabs', 'cards', 'auto'
|
||||||
|
|
||||||
|
-- Default Child Selection
|
||||||
|
default_child_id VARCHAR(20) REFERENCES children(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Per-Screen Last Selected Child
|
||||||
|
last_selected_per_screen JSONB DEFAULT '{}'::jsonb,
|
||||||
|
-- Example: {"track_feeding": "child_id", "track_sleep": "child_id", "analytics": "child_id"}
|
||||||
|
|
||||||
|
-- Comparison Preferences
|
||||||
|
comparison_default_children JSONB DEFAULT '[]'::jsonb,
|
||||||
|
-- Example: ["child_id_1", "child_id_2"]
|
||||||
|
|
||||||
|
comparison_default_metric VARCHAR(50) DEFAULT 'sleep-patterns',
|
||||||
|
-- Options: 'sleep-patterns', 'feeding-frequency', 'growth-curves', 'activities'
|
||||||
|
|
||||||
|
comparison_date_range VARCHAR(20) DEFAULT '7days',
|
||||||
|
-- Options: '7days', '30days', '90days'
|
||||||
|
|
||||||
|
-- Bulk Operation Preferences
|
||||||
|
show_bulk_confirm BOOLEAN DEFAULT TRUE,
|
||||||
|
remember_last_child BOOLEAN DEFAULT TRUE,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
UNIQUE(user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create index for quick user lookups
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_multi_child_preferences_user
|
||||||
|
ON multi_child_preferences(user_id);
|
||||||
|
|
||||||
|
-- Create index for default child lookups
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_multi_child_preferences_default_child
|
||||||
|
ON multi_child_preferences(default_child_id)
|
||||||
|
WHERE default_child_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Create trigger to update updated_at timestamp
|
||||||
|
CREATE OR REPLACE FUNCTION update_multi_child_preferences_updated_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER multi_child_preferences_updated_at_trigger
|
||||||
|
BEFORE UPDATE ON multi_child_preferences
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_multi_child_preferences_updated_at();
|
||||||
|
|
||||||
|
-- Insert default preferences for existing users with 2+ children
|
||||||
|
INSERT INTO multi_child_preferences (user_id, view_mode)
|
||||||
|
SELECT DISTINCT fm.user_id, 'auto'
|
||||||
|
FROM family_members fm
|
||||||
|
INNER JOIN children c ON fm.family_id = c.family_id
|
||||||
|
WHERE c.deleted_at IS NULL
|
||||||
|
GROUP BY fm.user_id, fm.family_id
|
||||||
|
HAVING COUNT(DISTINCT c.id) >= 2
|
||||||
|
ON CONFLICT (user_id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Add comments for documentation
|
||||||
|
COMMENT ON TABLE multi_child_preferences IS 'Stores user preferences for multi-child UI behavior';
|
||||||
|
COMMENT ON COLUMN multi_child_preferences.view_mode IS 'Dashboard view mode: tabs (1-3 kids), cards (4+), or auto';
|
||||||
|
COMMENT ON COLUMN multi_child_preferences.default_child_id IS 'Default child for quick actions';
|
||||||
|
COMMENT ON COLUMN multi_child_preferences.last_selected_per_screen IS 'JSON object mapping screen paths to last selected child ID';
|
||||||
|
COMMENT ON COLUMN multi_child_preferences.comparison_default_children IS 'JSON array of child IDs to compare by default';
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
-- Migration V019: Add bulk operation tracking for activities
|
||||||
|
-- Created: 2025-10-04
|
||||||
|
-- Description: Adds columns to track activities logged simultaneously for multiple children
|
||||||
|
|
||||||
|
-- Add bulk operation tracking columns to activities table
|
||||||
|
ALTER TABLE activities
|
||||||
|
ADD COLUMN bulk_operation_id UUID,
|
||||||
|
ADD COLUMN siblings_count INTEGER DEFAULT 1;
|
||||||
|
|
||||||
|
-- Create index for bulk operation queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activities_bulk_operation
|
||||||
|
ON activities(bulk_operation_id)
|
||||||
|
WHERE bulk_operation_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Add foreign key constraint for child_id if not already exists
|
||||||
|
-- This ensures all activities are linked to valid children
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_name = 'fk_activities_child'
|
||||||
|
AND table_name = 'activities'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE activities
|
||||||
|
ADD CONSTRAINT fk_activities_child
|
||||||
|
FOREIGN KEY (child_id)
|
||||||
|
REFERENCES children(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Optimize index for multi-child queries (child_id, type, started_at)
|
||||||
|
-- Note: activities table uses started_at instead of timestamp
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activities_multi_child_query
|
||||||
|
ON activities(child_id, type, started_at DESC);
|
||||||
|
|
||||||
|
-- Create composite index for efficient filtering by multiple children
|
||||||
|
-- This supports queries like: WHERE child_id IN ('id1', 'id2', 'id3')
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activities_child_started_at
|
||||||
|
ON activities(child_id, started_at DESC);
|
||||||
|
|
||||||
|
-- Add comments for documentation
|
||||||
|
COMMENT ON COLUMN activities.bulk_operation_id IS 'UUID linking activities logged simultaneously for multiple children';
|
||||||
|
COMMENT ON COLUMN activities.siblings_count IS 'Total number of children this activity was logged for (for UI context)';
|
||||||
@@ -51,6 +51,9 @@ export class ChildrenController {
|
|||||||
gender: child.gender,
|
gender: child.gender,
|
||||||
photoUrl: child.photoUrl,
|
photoUrl: child.photoUrl,
|
||||||
photoAlt: child.photoAlt,
|
photoAlt: child.photoAlt,
|
||||||
|
displayColor: child.displayColor,
|
||||||
|
sortOrder: child.sortOrder,
|
||||||
|
nickname: child.nickname,
|
||||||
medicalInfo: child.medicalInfo,
|
medicalInfo: child.medicalInfo,
|
||||||
createdAt: child.createdAt,
|
createdAt: child.createdAt,
|
||||||
},
|
},
|
||||||
@@ -58,6 +61,23 @@ export class ChildrenController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('family/:familyId/statistics')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getFamilyStatistics(
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
@Param('familyId') familyId: string,
|
||||||
|
) {
|
||||||
|
const stats = await this.childrenService.getFamilyStatistics(
|
||||||
|
user.sub,
|
||||||
|
familyId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: stats,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async findAll(@CurrentUser() user: any, @Query('familyId') familyId: string) {
|
async findAll(@CurrentUser() user: any, @Query('familyId') familyId: string) {
|
||||||
@@ -82,6 +102,9 @@ export class ChildrenController {
|
|||||||
gender: child.gender,
|
gender: child.gender,
|
||||||
photoUrl: child.photoUrl,
|
photoUrl: child.photoUrl,
|
||||||
photoAlt: child.photoAlt,
|
photoAlt: child.photoAlt,
|
||||||
|
displayColor: child.displayColor,
|
||||||
|
sortOrder: child.sortOrder,
|
||||||
|
nickname: child.nickname,
|
||||||
medicalInfo: child.medicalInfo,
|
medicalInfo: child.medicalInfo,
|
||||||
createdAt: child.createdAt,
|
createdAt: child.createdAt,
|
||||||
})),
|
})),
|
||||||
@@ -105,6 +128,9 @@ export class ChildrenController {
|
|||||||
gender: child.gender,
|
gender: child.gender,
|
||||||
photoUrl: child.photoUrl,
|
photoUrl: child.photoUrl,
|
||||||
photoAlt: child.photoAlt,
|
photoAlt: child.photoAlt,
|
||||||
|
displayColor: child.displayColor,
|
||||||
|
sortOrder: child.sortOrder,
|
||||||
|
nickname: child.nickname,
|
||||||
medicalInfo: child.medicalInfo,
|
medicalInfo: child.medicalInfo,
|
||||||
createdAt: child.createdAt,
|
createdAt: child.createdAt,
|
||||||
},
|
},
|
||||||
@@ -151,6 +177,9 @@ export class ChildrenController {
|
|||||||
gender: child.gender,
|
gender: child.gender,
|
||||||
photoUrl: child.photoUrl,
|
photoUrl: child.photoUrl,
|
||||||
photoAlt: child.photoAlt,
|
photoAlt: child.photoAlt,
|
||||||
|
displayColor: child.displayColor,
|
||||||
|
sortOrder: child.sortOrder,
|
||||||
|
nickname: child.nickname,
|
||||||
medicalInfo: child.medicalInfo,
|
medicalInfo: child.medicalInfo,
|
||||||
createdAt: child.createdAt,
|
createdAt: child.createdAt,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,17 @@ import { UpdateChildDto } from './dto/update-child.dto';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ChildrenService {
|
export class ChildrenService {
|
||||||
|
private readonly CHILD_COLORS = [
|
||||||
|
'#FF6B9D', // Pink
|
||||||
|
'#4ECDC4', // Teal
|
||||||
|
'#FFD93D', // Yellow
|
||||||
|
'#95E1D3', // Mint
|
||||||
|
'#C7CEEA', // Lavender
|
||||||
|
'#FF8C42', // Orange
|
||||||
|
'#A8E6CF', // Green
|
||||||
|
'#B8B8FF', // Purple
|
||||||
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Child)
|
@InjectRepository(Child)
|
||||||
private childRepository: Repository<Child>,
|
private childRepository: Repository<Child>,
|
||||||
@@ -40,11 +51,25 @@ export class ChildrenService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get family's existing children count to determine color and sort order
|
||||||
|
const existingChildren = await this.childRepository.find({
|
||||||
|
where: { familyId, deletedAt: IsNull() },
|
||||||
|
order: { createdAt: 'ASC' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortOrder = existingChildren.length;
|
||||||
|
|
||||||
|
// Use custom color if provided, otherwise auto-assign
|
||||||
|
const displayColor = createChildDto.displayColor ||
|
||||||
|
this.CHILD_COLORS[sortOrder % this.CHILD_COLORS.length];
|
||||||
|
|
||||||
// Create child
|
// Create child
|
||||||
const child = this.childRepository.create({
|
const child = this.childRepository.create({
|
||||||
...createChildDto,
|
...createChildDto,
|
||||||
familyId,
|
familyId,
|
||||||
birthDate: new Date(createChildDto.birthDate),
|
birthDate: new Date(createChildDto.birthDate),
|
||||||
|
displayColor,
|
||||||
|
sortOrder,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.childRepository.save(child);
|
return await this.childRepository.save(child);
|
||||||
@@ -135,6 +160,15 @@ export class ChildrenService {
|
|||||||
if (updateChildDto.photoUrl !== undefined) {
|
if (updateChildDto.photoUrl !== undefined) {
|
||||||
child.photoUrl = updateChildDto.photoUrl;
|
child.photoUrl = updateChildDto.photoUrl;
|
||||||
}
|
}
|
||||||
|
if (updateChildDto.photoAlt !== undefined) {
|
||||||
|
child.photoAlt = updateChildDto.photoAlt;
|
||||||
|
}
|
||||||
|
if (updateChildDto.displayColor !== undefined) {
|
||||||
|
child.displayColor = updateChildDto.displayColor;
|
||||||
|
}
|
||||||
|
if (updateChildDto.nickname !== undefined) {
|
||||||
|
child.nickname = updateChildDto.nickname;
|
||||||
|
}
|
||||||
if (updateChildDto.medicalInfo !== undefined) {
|
if (updateChildDto.medicalInfo !== undefined) {
|
||||||
child.medicalInfo = updateChildDto.medicalInfo;
|
child.medicalInfo = updateChildDto.medicalInfo;
|
||||||
}
|
}
|
||||||
@@ -215,4 +249,90 @@ export class ChildrenService {
|
|||||||
.orderBy('child.birthDate', 'DESC')
|
.orderBy('child.birthDate', 'DESC')
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get family statistics for multi-child UI decisions
|
||||||
|
*/
|
||||||
|
async getFamilyStatistics(
|
||||||
|
userId: string,
|
||||||
|
familyId: string,
|
||||||
|
): Promise<{
|
||||||
|
totalChildren: number;
|
||||||
|
viewMode: 'tabs' | 'cards';
|
||||||
|
ageRange: { youngest: number; oldest: number } | null;
|
||||||
|
genderDistribution: { male: number; female: number; other: number };
|
||||||
|
}> {
|
||||||
|
// Verify user has access to family
|
||||||
|
const membership = await this.familyMemberRepository.findOne({
|
||||||
|
where: { userId, familyId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
throw new ForbiddenException('You are not a member of this family');
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = await this.findAll(userId, familyId);
|
||||||
|
|
||||||
|
if (children.length === 0) {
|
||||||
|
return {
|
||||||
|
totalChildren: 0,
|
||||||
|
viewMode: 'tabs',
|
||||||
|
ageRange: null,
|
||||||
|
genderDistribution: { male: 0, female: 0, other: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate ages in months
|
||||||
|
const ages = children.map((child) => {
|
||||||
|
const now = new Date();
|
||||||
|
const birthDate = new Date(child.birthDate);
|
||||||
|
return (
|
||||||
|
(now.getFullYear() - birthDate.getFullYear()) * 12 +
|
||||||
|
(now.getMonth() - birthDate.getMonth())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gender distribution
|
||||||
|
const genderDistribution = children.reduce(
|
||||||
|
(acc, child) => {
|
||||||
|
if (child.gender === 'male') acc.male++;
|
||||||
|
else if (child.gender === 'female') acc.female++;
|
||||||
|
else acc.other++;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ male: 0, female: 0, other: 0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalChildren: children.length,
|
||||||
|
viewMode: children.length <= 3 ? 'tabs' : 'cards',
|
||||||
|
ageRange: {
|
||||||
|
youngest: Math.min(...ages),
|
||||||
|
oldest: Math.max(...ages),
|
||||||
|
},
|
||||||
|
genderDistribution,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has access to a specific child
|
||||||
|
*/
|
||||||
|
async userHasAccessToChild(
|
||||||
|
userId: string,
|
||||||
|
childId: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const child = await this.childRepository.findOne({
|
||||||
|
where: { id: childId, deletedAt: IsNull() },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!child) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const membership = await this.familyMemberRepository.findOne({
|
||||||
|
where: { userId, familyId: child.familyId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!membership;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
IsEnum,
|
IsEnum,
|
||||||
MinLength,
|
MinLength,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
|
Matches,
|
||||||
|
Length,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
export enum Gender {
|
export enum Gender {
|
||||||
@@ -36,6 +38,18 @@ export class CreateChildDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
photoAlt?: string;
|
photoAlt?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^#[0-9A-F]{6}$/i, {
|
||||||
|
message: 'displayColor must be a valid hex color code (e.g., #FF6B9D)',
|
||||||
|
})
|
||||||
|
displayColor?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 50)
|
||||||
|
nickname?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
medicalInfo?: Record<string, any>;
|
medicalInfo?: Record<string, any>;
|
||||||
|
|||||||
Reference in New Issue
Block a user