diff --git a/frontend/src/app/features/parents/children-tab.component.ts b/frontend/src/app/features/parents/children-tab.component.ts index c007fef..cb78d10 100644 --- a/frontend/src/app/features/parents/children-tab.component.ts +++ b/frontend/src/app/features/parents/children-tab.component.ts @@ -3,17 +3,18 @@ import { FormsModule } from '@angular/forms'; import { ParentApiService } from '../../core/parent-api.service'; import { I18nService } from '../../core/i18n.service'; import { ChildSummary } from '../../core/models'; +import { EmojiPickerComponent } from '../../shared/emoji-picker.component'; +import { ColorPickerComponent } from '../../shared/color-picker.component'; /** Pestaña Niños: dar de alta y de baja perfiles de hijo en la familia. */ @Component({ selector: 'app-children-tab', - imports: [FormsModule], + imports: [FormsModule, EmojiPickerComponent, ColorPickerComponent], template: `

Nuevo niño/a

-
-
-
- @for (m of mascots; track m) { - - } -
+
-
-
- @for (c of colors; track c) { - - } -
+
-
- +
@@ -100,14 +79,7 @@ import { ChildSummary } from '../../core/models'; styles: [ ` .field { margin-bottom: var(--space-4); } - .field__label { - display: flex; - align-items: center; - gap: 6px; - font-weight: 700; - color: var(--text-1); - margin-bottom: 6px; - } + .field__label { display: flex; align-items: center; gap: 6px; font-weight: 700; color: var(--text-1); margin-bottom: 6px; } .field__opt { font-weight: 400; color: var(--text-3); } .field__info { cursor: help; color: var(--text-4); font-size: 0.85rem; } .field__input { @@ -121,58 +93,19 @@ import { ChildSummary } from '../../core/models'; width: 100%; box-sizing: border-box; } - .grid2 { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--space-4); - } - @media (max-width: 520px) { - .grid2 { grid-template-columns: 1fr; } - } - .picker { display: flex; flex-wrap: wrap; gap: 8px; } - .emoji { - all: unset; - cursor: pointer; - width: 48px; - height: 48px; - border-radius: 14px; - display: flex; - align-items: center; - justify-content: center; - font-size: 26px; - background: var(--surface-soft); - border: 2px solid transparent; - transition: transform 0.1s, border-color 0.15s; - } - .emoji:hover { transform: scale(1.08); } - .emoji--sel { border-color: var(--accent-blue); background: color-mix(in srgb, var(--accent-blue) 12%, #fff); } - .swatch { - all: unset; - cursor: pointer; - width: 40px; - height: 40px; - border-radius: 50%; - box-shadow: var(--shadow-card); - border: 3px solid transparent; - transition: transform 0.1s; - } - .swatch:hover { transform: scale(1.1); } - .swatch--sel { border-color: var(--text-strong); transform: scale(1.1); } + .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); } + @media (max-width: 520px) { .grid2 { grid-template-columns: 1fr; } } .field__add { margin-top: var(--space-2); } `, ], }) export class ChildrenTabComponent { - /** Avisa al panel de que la lista de niños cambió (para refrescar el selector). */ @Output() changed = new EventEmitter(); private readonly api = inject(ParentApiService); protected readonly i18n = inject(I18nService); - /** Emojis de mascota disponibles para elegir. */ protected readonly mascots = ['🦊', '🐢', '🦉', '🐶', '🐱', '🐰', '🦁', '🐼', '🐸', '🐨', '🐵', '🦄', '🐯', '🐧']; - /** Paleta de acento del handoff. */ - protected readonly colors = ['#F2A65A', '#5B8DEF', '#A78BD0', '#7FBF6B', '#5BC0BE', '#F4C95D', '#EC8FA4']; protected readonly children = signal([]); protected readonly mascot = signal(''); @@ -189,7 +122,6 @@ export class ChildrenTabComponent { this.api.listChildren().subscribe((list) => this.children.set(list)); } - /** Se puede dar de alta cuando hay nombre, mascota y una edad válida. */ canAdd(): boolean { return this.name.trim().length > 0 && this.mascot() !== '' && !!this.age && this.age > 0; } @@ -218,7 +150,6 @@ export class ChildrenTabComponent { } remove(c: ChildSummary): void { - // Borra el niño y todo lo suyo (horario, rutinas, tareas, monedero...). this.api.deleteChild(c.id).subscribe(() => { this.reload(); this.changed.emit(); diff --git a/frontend/src/app/features/parents/materials-tab.component.ts b/frontend/src/app/features/parents/materials-tab.component.ts index c383264..51f3c87 100644 --- a/frontend/src/app/features/parents/materials-tab.component.ts +++ b/frontend/src/app/features/parents/materials-tab.component.ts @@ -3,23 +3,38 @@ import { FormsModule } from '@angular/forms'; import { ParentApiService } from '../../core/parent-api.service'; import { I18nService } from '../../core/i18n.service'; import { ActivityView, MaterialView } from '../../core/models'; +import { EmojiPickerComponent } from '../../shared/emoji-picker.component'; +import { ColorPickerComponent } from '../../shared/color-picker.component'; /** Pestaña Materiales: catálogo de materiales y actividades (con su material). */ @Component({ selector: 'app-materials-tab', - imports: [FormsModule], + imports: [FormsModule, EmojiPickerComponent, ColorPickerComponent], template: `

Nuevo material

-
- - - - - +
+
+ + +
+
+ + +
-
+
+ + +
+
+ + +
+ + +
@for (m of materials(); track m.id) {
{{ m.icon }} @@ -35,26 +50,38 @@ import { ActivityView, MaterialView } from '../../core/models';

Nueva actividad

-
- - - - +
+
+ + +
+
+ + +
-

Material que conlleva:

-
- @for (m of materials(); track m.id) { - - } +
+ +
-
- +
+ +
+
+ +
+ @for (m of materials(); track m.id) { + + } @empty { + Crea materiales primero ↑ + } +
+
+
@@ -65,8 +92,7 @@ import { ActivityView, MaterialView } from '../../core/models';
{{ a.icon }} - {{ i18n.label(a.labelEs, a.labelCa) }} - — {{ materialNames(a) }} + {{ i18n.label(a.labelEs, a.labelCa) }} — {{ materialNames(a) }}
@@ -76,22 +102,40 @@ import { ActivityView, MaterialView } from '../../core/models';
`, + styles: [ + ` + .field { margin-bottom: var(--space-4); } + .field__label { display: flex; align-items: center; gap: 6px; font-weight: 700; color: var(--text-1); margin-bottom: 6px; } + .field__info { cursor: help; color: var(--text-4); font-size: 0.85rem; } + .field__input { + font-family: var(--font-body); font-size: 1rem; padding: 12px 14px; + border: 2px solid var(--border-2); border-radius: var(--radius-sm); + background: var(--surface); color: var(--text-strong); width: 100%; box-sizing: border-box; + } + .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); } + @media (max-width: 520px) { .grid2 { grid-template-columns: 1fr; } } + .picker { display: flex; flex-wrap: wrap; gap: 8px; } + `, + ], }) export class MaterialsTabComponent { private readonly api = inject(ParentApiService); protected readonly i18n = inject(I18nService); + protected readonly materialIcons = ['✏️', '📘', '📏', '🎵', '📓', '👕', '👟', '🧖', '💧', '📖', '📒', '🍎', '🎒', '🖍️', '✂️', '📐', '🧮', '🎨']; + protected readonly activityIcons = ['🤸', '🎵', '📘', '📖', '🎨', '⚽', '🔬', '🧮', '🌍', '🎭', '💻', '🏃']; + protected readonly materials = signal([]); protected readonly activities = signal([]); protected mIcon = ''; protected mEs = ''; protected mCa = ''; - protected mColor = '#5b8def'; + protected mColor = '#5B8DEF'; protected aIcon = ''; protected aEs = ''; protected aCa = ''; - protected aColor = '#7fbf6b'; + protected aColor = '#7FBF6B'; protected selectedMaterials = new Set(); constructor() { diff --git a/frontend/src/app/features/parents/rewards-tab.component.ts b/frontend/src/app/features/parents/rewards-tab.component.ts index 1822c1e..02c340b 100644 --- a/frontend/src/app/features/parents/rewards-tab.component.ts +++ b/frontend/src/app/features/parents/rewards-tab.component.ts @@ -4,11 +4,13 @@ import { ApiService } from '../../core/api.service'; import { ParentApiService } from '../../core/parent-api.service'; import { I18nService } from '../../core/i18n.service'; import { RewardAdminView } from '../../core/models'; +import { EmojiPickerComponent } from '../../shared/emoji-picker.component'; +import { ColorPickerComponent } from '../../shared/color-picker.component'; /** Pestaña Recompensas: catálogo de premios + gamificación y ajustes del niño. */ @Component({ selector: 'app-rewards-tab', - imports: [FormsModule], + imports: [FormsModule, EmojiPickerComponent, ColorPickerComponent], template: `
@@ -38,16 +40,33 @@ import { RewardAdminView } from '../../core/models';

Nuevo premio

-
- - - - - - +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
@@ -67,6 +86,20 @@ import { RewardAdminView } from '../../core/models';
`, + styles: [ + ` + .field { margin-bottom: var(--space-4); } + .field__label { display: flex; align-items: center; gap: 6px; font-weight: 700; color: var(--text-1); margin-bottom: 6px; } + .field__info { cursor: help; color: var(--text-4); font-size: 0.85rem; } + .field__input { + font-family: var(--font-body); font-size: 1rem; padding: 12px 14px; + border: 2px solid var(--border-2); border-radius: var(--radius-sm); + background: var(--surface); color: var(--text-strong); width: 100%; box-sizing: border-box; + } + .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); } + @media (max-width: 520px) { .grid2 { grid-template-columns: 1fr; } } + `, + ], }) export class RewardsTabComponent { @Input({ required: true }) set childId(value: number) { @@ -100,11 +133,12 @@ export class RewardsTabComponent { protected ttsEnabled = true; protected dyslexiaFont = true; + protected readonly rewardIcons = ['🎮', '🍕', '🛝', '🍿', '🌙', '🦖', '🍦', '🎬', '🧸', '🎨', '⚽', '🚲', '🎁', '🍫', '🐾', '🎡']; protected icon = ''; protected labelEs = ''; protected labelCa = ''; protected cost: number | null = null; - protected color = '#5b8def'; + protected color = '#5B8DEF'; constructor() { this.reload(); diff --git a/frontend/src/app/features/parents/routines-tab.component.ts b/frontend/src/app/features/parents/routines-tab.component.ts index a4fadf3..ac9b57f 100644 --- a/frontend/src/app/features/parents/routines-tab.component.ts +++ b/frontend/src/app/features/parents/routines-tab.component.ts @@ -3,6 +3,8 @@ import { FormsModule } from '@angular/forms'; import { ParentApiService } from '../../core/parent-api.service'; import { I18nService } from '../../core/i18n.service'; import { RoutineView } from '../../core/models'; +import { EmojiPickerComponent } from '../../shared/emoji-picker.component'; +import { ColorPickerComponent } from '../../shared/color-picker.component'; const DAYS: { key: string; es: string; ca: string }[] = [ { key: 'MONDAY', es: 'Lunes', ca: 'Dilluns' }, @@ -15,7 +17,7 @@ const DAYS: { key: string; es: string; ca: string }[] = [ /** Pestaña Rutinas de tarde por día de la semana, de un niño. */ @Component({ selector: 'app-routines-tab', - imports: [FormsModule], + imports: [FormsModule, EmojiPickerComponent, ColorPickerComponent], template: `
@@ -35,13 +37,25 @@ const DAYS: { key: string; es: string; ca: string }[] = [

Nueva rutina

-
- - - - - +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
@@ -60,6 +74,20 @@ const DAYS: { key: string; es: string; ca: string }[] = [
`, + styles: [ + ` + .field { margin-bottom: var(--space-4); } + .field__label { display: flex; align-items: center; gap: 6px; font-weight: 700; color: var(--text-1); margin-bottom: 6px; } + .field__info { cursor: help; color: var(--text-4); font-size: 0.85rem; } + .field__input { + font-family: var(--font-body); font-size: 1rem; padding: 12px 14px; + border: 2px solid var(--border-2); border-radius: var(--radius-sm); + background: var(--surface); color: var(--text-strong); width: 100%; box-sizing: border-box; + } + .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); } + @media (max-width: 520px) { .grid2 { grid-template-columns: 1fr; } } + `, + ], }) export class RoutinesTabComponent { @Input({ required: true }) set childId(value: number) { @@ -78,10 +106,11 @@ export class RoutinesTabComponent { this.routines().filter((r) => r.dayOfWeek === this.day()), ); + protected readonly routineIcons = ['🎒', '🥪', '📝', '🎹', '🍽️', '🛁', '🦷', '📚', '🧹', '🐕', '🛏️', '🎨', '⚽', '🧺']; protected icon = ''; protected labelEs = ''; protected labelCa = ''; - protected color = '#a78bd0'; + protected color = '#A78BD0'; private reload(): void { this.api.listRoutines(this._childId).subscribe((l) => this.routines.set(l)); diff --git a/frontend/src/app/shared/color-picker.component.ts b/frontend/src/app/shared/color-picker.component.ts new file mode 100644 index 0000000..5bb54d1 --- /dev/null +++ b/frontend/src/app/shared/color-picker.component.ts @@ -0,0 +1,44 @@ +import { Component, model } from '@angular/core'; + +/** Selector de color de acento: paleta del handoff, clicable. Two-way con [(value)]. */ +@Component({ + selector: 'app-color-picker', + imports: [], + template: ` +
+ @for (c of colors; track c) { + + } +
+ `, + styles: [ + ` + .picker { display: flex; flex-wrap: wrap; gap: 8px; } + .swatch { + all: unset; + cursor: pointer; + width: 36px; + height: 36px; + border-radius: 50%; + box-shadow: var(--shadow-card); + border: 3px solid transparent; + transition: transform 0.1s; + } + .swatch:hover { transform: scale(1.1); } + .swatch--sel { border-color: var(--text-strong); transform: scale(1.1); } + `, + ], +}) +export class ColorPickerComponent { + /** Color seleccionado (hex). Por defecto el primero de la paleta. */ + readonly value = model('#F2A65A'); + + protected readonly colors = ['#F2A65A', '#5B8DEF', '#A78BD0', '#7FBF6B', '#5BC0BE', '#F4C95D', '#EC8FA4']; +} diff --git a/frontend/src/app/shared/emoji-picker.component.ts b/frontend/src/app/shared/emoji-picker.component.ts new file mode 100644 index 0000000..a5bdd2c --- /dev/null +++ b/frontend/src/app/shared/emoji-picker.component.ts @@ -0,0 +1,43 @@ +import { Component, input, model } from '@angular/core'; + +/** Selector de emoji clicable a partir de una lista de opciones. Two-way con [(value)]. */ +@Component({ + selector: 'app-emoji-picker', + imports: [], + template: ` +
+ @for (e of options(); track e) { + + } +
+ `, + styles: [ + ` + .picker { display: flex; flex-wrap: wrap; gap: 8px; } + .emoji { + all: unset; + cursor: pointer; + width: 48px; + height: 48px; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + font-size: 26px; + background: var(--surface-soft); + border: 2px solid transparent; + transition: transform 0.1s, border-color 0.15s; + } + .emoji:hover { transform: scale(1.08); } + .emoji--sel { border-color: var(--accent-blue); background: color-mix(in srgb, var(--accent-blue) 12%, #fff); } + `, + ], +}) +export class EmojiPickerComponent { + /** Emoji seleccionado. */ + readonly value = model(''); + /** Lista de emojis ofrecidos. */ + readonly options = input([]); +}