Skip to content

Plan Técnico: Rediseño Tab Clínico

Documento de diseño: clinical-tab-redesign.md


Resumen de Cambios

Arquitectura Actual

ClinicalTab.vue
├── OdontogramChart.vue
│   ├── TreatmentBar.vue (mezcla diagnósticos + tratamientos)
│   ├── TimelineSlider.vue
│   └── ToothQuadrant.vue (x4)
└── Treatment Plans Section (inline)
    ├── TreatmentPlanMiniCard.vue
    └── TreatmentPlanDetail.vue (modal)

Arquitectura Propuesta

ClinicalTab.vue
├── ClinicalModeToggle.vue (3 tabs)

├── [Modo Histórico] HistoryMode.vue
│   ├── TimelineSlider.vue (existente, reusar)
│   ├── OdontogramChart.vue (mode="view-only")
│   └── ChangesList.vue (nuevo)

├── [Modo Diagnóstico] DiagnosisMode.vue
│   ├── OdontogramChart.vue (mode="diagnosis")
│   ├── TreatmentBar.vue (mode="diagnosis") ← REUTILIZADO
│   ├── ConditionsList.vue (nuevo)
│   └── DiagnosisCTA.vue (nuevo, contextual)

└── [Modo Planes] PlansMode.vue
    ├── PlansListView.vue (lista de planes)
    │   ├── TreatmentPlanMiniCard.vue (existente)
    │   └── OdontogramMini.vue (nuevo, resumen)
    └── PlanDetailView.vue (vista expandida)
        ├── OdontogramChart.vue (mode="planning", con highlighting)
        ├── PlanTreatmentList.vue (nuevo, con hover linking)
        └── TreatmentBar.vue (mode="planning") ← REUTILIZADO

Nota: TreatmentBar.vue se reutiliza con prop mode en lugar de crear componente separado.


Fase 1: Preparación y Refactoring Base

1.1 Separar Catálogo de Tratamientos

Problema: TreatmentBar.vue usa useTreatmentCatalog() que mezcla diagnósticos y tratamientos.

Solución: Añadir campo is_diagnosis al catálogo o usar categorías existentes.

Archivo: frontend/app/composables/useTreatmentCatalog.ts

typescript
// Añadir computed para filtrar
const diagnosticTreatments = computed(() =>
  catalog.value.filter(t => DIAGNOSTIC_CATEGORIES.includes(t.category))
)

const therapeuticTreatments = computed(() =>
  catalog.value.filter(t => !DIAGNOSTIC_CATEGORIES.includes(t.category))
)

const DIAGNOSTIC_CATEGORIES = ['diagnostic', 'existing_condition']

Backend (si necesario): Verificar que TreatmentCatalogItem tenga categoría que distinga diagnósticos.

Archivos a modificar:

  • frontend/app/composables/useTreatmentCatalog.ts
  • Posiblemente: backend/app/modules/treatment_catalog/models.py (añadir is_diagnosis bool)

1.2 Extender OdontogramChart con Modos

Archivo: frontend/app/components/odontogram/OdontogramChart.vue

Cambios:

typescript
// Props actuales
mode?: 'full' | 'view-only' | 'planning'

// Props nuevos
mode?: 'full' | 'view-only' | 'diagnosis' | 'planning'
highlightedTeeth?: number[]  // Para hover linking
onToothHover?: (toothNumber: number | null) => void
planId?: string  // Para modo planning, filtrar tratamientos del plan

Comportamiento por modo:

ModoBarraEditableTimelineHighlighting
view-onlyNoNoNo
diagnosisDiagnosisBarSí (solo diagnósticos)NoNo
planningTreatmentBarSí (solo terapéuticos)No

1.3 Crear Hook para Draft Plans

Archivo: frontend/app/composables/useTreatmentPlans.ts

Añadir:

typescript
const draftPlans = computed(() =>
  plans.value.filter(p => p.status === 'draft')
)

async function fetchDraftPlans(patientId: string) {
  return fetchPlans({ patient_id: patientId, status: 'draft' })
}

Fase 2: Componentes Nuevos

2.1 ClinicalModeToggle.vue

Path: frontend/app/components/clinical/ClinicalModeToggle.vue

vue
<script setup lang="ts">
type ClinicalMode = 'history' | 'diagnosis' | 'plans'

const props = defineProps<{
  modelValue: ClinicalMode
}>()

const emit = defineEmits<{
  'update:modelValue': [mode: ClinicalMode]
}>()

const modes = [
  { value: 'history', label: 'clinical.modes.history', icon: 'i-lucide-history' },
  { value: 'diagnosis', label: 'clinical.modes.diagnosis', icon: 'i-lucide-stethoscope' },
  { value: 'plans', label: 'clinical.modes.plans', icon: 'i-lucide-clipboard-list' },
]
</script>

<template>
  <div class="flex gap-1 p-1 bg-gray-100 dark:bg-gray-800 rounded-lg">
    <UButton
      v-for="mode in modes"
      :key="mode.value"
      :variant="modelValue === mode.value ? 'solid' : 'ghost'"
      :icon="mode.icon"
      @click="emit('update:modelValue', mode.value)"
    >
      {{ $t(mode.label) }}
    </UButton>
  </div>
</template>

2.2 HistoryMode.vue

Path: frontend/app/components/clinical/HistoryMode.vue

vue
<script setup lang="ts">
const props = defineProps<{
  patientId: string
}>()

const {
  timelineDates,
  viewingDate,
  fetchTimeline,
  fetchOdontogramAtDate,
  historicalTeeth,
  historicalTreatments
} = useOdontogram()

// Cargar timeline al montar
onMounted(() => fetchTimeline(props.patientId))

// Computed para cambios en fecha seleccionada
const changesAtDate = computed(() => {
  // Filtrar tratamientos/cambios de la fecha seleccionada
  return historicalTreatments.value.filter(t =>
    t.recorded_at?.startsWith(viewingDate.value)
  )
})
</script>

<template>
  <div class="space-y-4">
    <!-- Timeline Slider -->
    <UCard>
      <TimelineSlider
        :dates="timelineDates"
        :selected-date="viewingDate"
        @select="fetchOdontogramAtDate(patientId, $event)"
      />
    </UCard>

    <!-- Odontograma (solo lectura) -->
    <UCard>
      <template #header>
        <div class="flex items-center justify-between">
          <span>{{ $t('clinical.history.stateAt') }} {{ viewingDate }}</span>
        </div>
      </template>
      <OdontogramChart
        :patient-id="patientId"
        mode="view-only"
      />
    </UCard>

    <!-- Cambios en esta fecha -->
    <UCard v-if="changesAtDate.length">
      <template #header>
        {{ $t('clinical.history.changesOnDate') }}
      </template>
      <ChangesList :changes="changesAtDate" />
    </UCard>
  </div>
</template>

2.3 DiagnosisMode.vue

Path: frontend/app/components/clinical/DiagnosisMode.vue

vue
<script setup lang="ts">
const props = defineProps<{
  patientId: string
}>()

const emit = defineEmits<{
  'create-plan': []
  'continue-plan': [planId: string]
}>()

const { treatments, fetchTreatments } = useOdontogram()
const { plans, fetchPatientPlans } = useTreatmentPlans()

// Filtrar solo diagnósticos (status = 'existing')
const conditions = computed(() =>
  treatments.value.filter(t => t.status === 'existing')
)

// Planes en borrador para CTA contextual
const draftPlans = computed(() =>
  plans.value.filter(p => p.status === 'draft')
)

onMounted(() => {
  fetchTreatments(props.patientId)
  fetchPatientPlans(props.patientId)
})
</script>

<template>
  <div class="space-y-4">
    <!-- Odontograma con barra de diagnósticos -->
    <UCard>
      <OdontogramChart
        :patient-id="patientId"
        mode="diagnosis"
      />
    </UCard>

    <!-- Lista de condiciones registradas -->
    <UCard>
      <template #header>
        {{ $t('clinical.diagnosis.registeredConditions') }}
      </template>
      <ConditionsList :conditions="conditions" />
    </UCard>

    <!-- CTA Contextual -->
    <DiagnosisCTA
      :draft-plans="draftPlans"
      @create="emit('create-plan')"
      @continue="emit('continue-plan', $event)"
    />
  </div>
</template>

2.4 Modificar TreatmentBar.vue (añadir modo)

Path: frontend/app/components/odontogram/TreatmentBar.vue

Cambio: Añadir prop mode para reutilizar en diagnóstico y planificación.

vue
<script setup lang="ts">
// NUEVOS props
const props = defineProps<{
  mode?: 'diagnosis' | 'planning' | 'full'  // NUEVO
  selectedTreatment?: TreatmentType | null
  selectedStatus: TreatmentStatus
  selectedPlanId?: string | null
  patientId?: string
  treatmentPlans?: TreatmentPlan[]
  disabled?: boolean
}>()

// Categorías de diagnóstico
const DIAGNOSTIC_CATEGORIES = ['diagnostic', 'existing_condition', 'caries', 'periodontal']

// Filtrar categorías según modo
const visibleCategories = computed(() => {
  const all = categories.value
  if (props.mode === 'diagnosis') {
    return all.filter(c => DIAGNOSTIC_CATEGORIES.includes(c.id))
  }
  if (props.mode === 'planning') {
    return all.filter(c => !DIAGNOSTIC_CATEGORIES.includes(c.id))
  }
  return all // 'full' = todo (comportamiento actual)
})

// Status forzado en modo diagnóstico
const effectiveStatus = computed(() =>
  props.mode === 'diagnosis' ? 'existing' : props.selectedStatus
)

// Controles visibles según modo
const showStatusToggle = computed(() => props.mode !== 'diagnosis')
const showPlanSelector = computed(() =>
  props.mode !== 'diagnosis' && effectiveStatus.value === 'planned'
)
</script>

<template>
  <div class="treatment-bar">
    <!-- Status toggle: oculto en modo diagnóstico -->
    <div v-if="showStatusToggle" class="status-toggle">
      <!-- ... toggle existing/planned ... -->
    </div>

    <!-- Plan selector: solo en planning con status=planned -->
    <div v-if="showPlanSelector" class="plan-selector">
      <!-- ... dropdown planes ... -->
    </div>

    <!-- Category tabs: filtradas por modo -->
    <div class="category-tabs">
      <UButton
        v-for="cat in visibleCategories"
        :key="cat.id"
        <!-- ... -->
      />
    </div>

    <!-- Treatment grid: sin cambios -->
  </div>
</template>

Uso en DiagnosisMode:

vue
<TreatmentBar
  mode="diagnosis"
  :patient-id="patientId"
  @treatment-select="handleDiagnosisSelect"
/>

Uso en PlanDetailView:

vue
<TreatmentBar
  mode="planning"
  :patient-id="patientId"
  :plan-id="plan.id"
  :treatment-plans="plans"
  @treatment-select="handleTreatmentSelect"
/>

2.5 DiagnosisCTA.vue

Path: frontend/app/components/clinical/DiagnosisCTA.vue

vue
<script setup lang="ts">
const props = defineProps<{
  draftPlans: TreatmentPlan[]
}>()

const emit = defineEmits<{
  'create': []
  'continue': [planId: string]
}>()

const selectedDraftId = ref<string>('')
</script>

<template>
  <UCard class="bg-primary-50 dark:bg-primary-900/20 border-primary-200">
    <div class="flex items-center justify-between">
      <div class="flex items-center gap-2">
        <UIcon name="i-lucide-lightbulb" class="text-primary-500" />
        <span>{{ $t('clinical.diagnosis.readyToCreatePlan') }}</span>
      </div>

      <!-- Sin borradores: Crear plan -->
      <UButton
        v-if="draftPlans.length === 0"
        color="primary"
        @click="emit('create')"
      >
        {{ $t('clinical.diagnosis.createPlan') }}
      </UButton>

      <!-- 1 borrador: Continuar ese plan -->
      <UButton
        v-else-if="draftPlans.length === 1"
        color="primary"
        @click="emit('continue', draftPlans[0].id)"
      >
        {{ $t('clinical.diagnosis.continuePlan', { name: draftPlans[0].title }) }}
      </UButton>

      <!-- N borradores: Dropdown -->
      <div v-else class="flex items-center gap-2">
        <USelectMenu
          v-model="selectedDraftId"
          :options="draftPlans"
          option-attribute="title"
          value-attribute="id"
          :placeholder="$t('clinical.diagnosis.selectPlan')"
        />
        <UButton
          color="primary"
          :disabled="!selectedDraftId"
          @click="emit('continue', selectedDraftId)"
        >
          {{ $t('clinical.diagnosis.continue') }}
        </UButton>
      </div>
    </div>
  </UCard>
</template>

2.6 PlansMode.vue

Path: frontend/app/components/clinical/PlansMode.vue

vue
<script setup lang="ts">
const props = defineProps<{
  patientId: string
  initialPlanId?: string  // Para abrir directamente un plan (desde CTA diagnóstico)
}>()

const { plans, fetchPatientPlans, fetchPlan } = useTreatmentPlans()

// Vista: 'list' o 'detail'
const view = ref<'list' | 'detail'>('list')
const selectedPlan = ref<TreatmentPlanDetail | null>(null)

async function openPlanDetail(planId: string) {
  selectedPlan.value = await fetchPlan(planId)
  view.value = 'detail'
}

function backToList() {
  view.value = 'list'
  selectedPlan.value = null
}

onMounted(async () => {
  await fetchPatientPlans(props.patientId)
  if (props.initialPlanId) {
    openPlanDetail(props.initialPlanId)
  }
})

watch(() => props.initialPlanId, (newId) => {
  if (newId) openPlanDetail(newId)
})
</script>

<template>
  <!-- Vista Lista -->
  <PlansListView
    v-if="view === 'list'"
    :plans="plans"
    :patient-id="patientId"
    @view-plan="openPlanDetail"
    @create-plan="showCreateModal = true"
  />

  <!-- Vista Detalle -->
  <PlanDetailView
    v-else
    :plan="selectedPlan"
    :patient-id="patientId"
    @back="backToList"
    @updated="openPlanDetail(selectedPlan.id)"
  />
</template>

2.7 PlanDetailView.vue (Vista Expandida con Hover Linking)

Path: frontend/app/components/clinical/PlanDetailView.vue

vue
<script setup lang="ts">
const props = defineProps<{
  plan: TreatmentPlanDetail
  patientId: string
}>()

// Hover linking state
const hoveredToothNumber = ref<number | null>(null)
const hoveredItemId = ref<string | null>(null)

// Teeth que tienen tratamientos en este plan
const planTeeth = computed(() => {
  return [...new Set(
    props.plan.items
      .filter(item => item.tooth_number)
      .map(item => item.tooth_number)
  )]
})

// Items del diente hovered
const highlightedItems = computed(() => {
  if (!hoveredToothNumber.value) return []
  return props.plan.items
    .filter(item => item.tooth_number === hoveredToothNumber.value)
    .map(item => item.id)
})

// Dientes del item hovered
const highlightedTeeth = computed(() => {
  if (!hoveredItemId.value) return []
  const item = props.plan.items.find(i => i.id === hoveredItemId.value)
  return item?.tooth_number ? [item.tooth_number] : []
})
</script>

<template>
  <div class="space-y-4">
    <!-- Header con navegación -->
    <div class="flex items-center justify-between">
      <UButton variant="ghost" icon="i-lucide-arrow-left" @click="emit('back')">
        {{ $t('clinical.plans.backToList') }}
      </UButton>
      <h2 class="text-lg font-semibold">{{ plan.title }}</h2>
      <div class="flex gap-2">
        <UButton v-if="plan.status === 'draft'" @click="activatePlan">
          {{ $t('clinical.plans.activate') }}
        </UButton>
        <UButton @click="generateBudget">
          {{ $t('clinical.plans.generateBudget') }}
        </UButton>
      </div>
    </div>

    <!-- Layout dos columnas -->
    <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
      <!-- Columna izquierda: Odontograma -->
      <UCard>
        <OdontogramChart
          :patient-id="patientId"
          mode="planning"
          :plan-id="plan.id"
          :highlighted-teeth="highlightedTeeth"
          @tooth-hover="hoveredToothNumber = $event"
        />
      </UCard>

      <!-- Columna derecha: Lista de tratamientos -->
      <UCard>
        <template #header>
          <div class="flex items-center justify-between">
            <span>{{ $t('clinical.plans.treatments') }}</span>
            <UButton size="sm" icon="i-lucide-plus">
              {{ $t('clinical.plans.addTreatment') }}
            </UButton>
          </div>
        </template>

        <PlanTreatmentList
          :items="plan.items"
          :highlighted-items="highlightedItems"
          @item-hover="hoveredItemId = $event"
          @item-complete="handleComplete"
          @item-remove="handleRemove"
        />

        <!-- Total -->
        <template #footer>
          <div class="flex justify-between font-semibold">
            <span>{{ $t('clinical.plans.total') }}</span>
            <span>{{ formatCurrency(planTotal) }}</span>
          </div>
        </template>
      </UCard>
    </div>

    <!-- Barra de tratamientos (abajo) -->
    <UCard>
      <TreatmentBar
        :patient-id="patientId"
        :plan-id="plan.id"
        mode="therapeutic-only"
        @treatment-applied="refreshPlan"
      />
    </UCard>
  </div>
</template>

2.8 PlanTreatmentList.vue (con Hover Linking)

Path: frontend/app/components/clinical/PlanTreatmentList.vue

vue
<script setup lang="ts">
const props = defineProps<{
  items: PlannedTreatmentItem[]
  highlightedItems: string[]
}>()

const emit = defineEmits<{
  'item-hover': [itemId: string | null]
  'item-complete': [itemId: string]
  'item-remove': [itemId: string]
}>()

const pendingItems = computed(() => props.items.filter(i => i.status === 'pending'))
const completedItems = computed(() => props.items.filter(i => i.status === 'completed'))
</script>

<template>
  <div class="space-y-2">
    <!-- Pendientes -->
    <div
      v-for="(item, index) in pendingItems"
      :key="item.id"
      class="p-3 rounded-lg border transition-colors"
      :class="{
        'bg-yellow-50 border-yellow-300': highlightedItems.includes(item.id),
        'hover:bg-gray-50': !highlightedItems.includes(item.id)
      }"
      @mouseenter="emit('item-hover', item.id)"
      @mouseleave="emit('item-hover', null)"
    >
      <div class="flex items-center justify-between">
        <div class="flex items-center gap-3">
          <span class="text-gray-400">{{ index + 1 }}.</span>
          <div>
            <div class="font-medium">{{ item.catalog_item?.name || item.description }}</div>
            <div v-if="item.tooth_number" class="text-sm text-gray-500">
              {{ $t('clinical.tooth') }} {{ item.tooth_number }}
              <span v-if="item.surfaces?.length">({{ item.surfaces.join(', ') }})</span>
            </div>
          </div>
        </div>
        <div class="flex items-center gap-2">
          <span class="font-medium">{{ formatCurrency(item.price) }}</span>
          <UButton size="xs" variant="ghost" icon="i-lucide-check" @click="emit('item-complete', item.id)" />
          <UButton size="xs" variant="ghost" color="red" icon="i-lucide-trash-2" @click="emit('item-remove', item.id)" />
        </div>
      </div>
    </div>

    <!-- Completados (colapsable) -->
    <UAccordion v-if="completedItems.length" :items="[{ label: `Completados (${completedItems.length})`, slot: 'completed' }]">
      <template #completed>
        <div v-for="item in completedItems" :key="item.id" class="p-2 text-gray-500 line-through">
          {{ item.catalog_item?.name }} - {{ $t('clinical.tooth') }} {{ item.tooth_number }}
        </div>
      </template>
    </UAccordion>
  </div>
</template>

Fase 3: Refactoring ClinicalTab Principal

3.1 Nuevo ClinicalTab.vue

Path: frontend/app/components/patient/ClinicalTab.vue

vue
<script setup lang="ts">
import type { ClinicalMode } from '~/types'

const props = defineProps<{
  patientId: string
  readonly?: boolean
}>()

const route = useRoute()
const router = useRouter()

// Modo actual (sync con query param)
const currentMode = ref<ClinicalMode>('diagnosis')

// Plan a abrir (para transición desde diagnóstico)
const targetPlanId = ref<string | null>(null)

// Sync mode con URL
watch(currentMode, (mode) => {
  router.replace({ query: { ...route.query, clinicalMode: mode } })
})

onMounted(() => {
  const queryMode = route.query.clinicalMode as ClinicalMode
  if (queryMode && ['history', 'diagnosis', 'plans'].includes(queryMode)) {
    currentMode.value = queryMode
  }
})

// Handlers para transiciones entre modos
function handleCreatePlan() {
  // Abrir modal de crear plan, luego ir a planes
  showPlanModal.value = true
}

function handleContinuePlan(planId: string) {
  targetPlanId.value = planId
  currentMode.value = 'plans'
}

function handlePlanCreated(plan: TreatmentPlan) {
  targetPlanId.value = plan.id
  currentMode.value = 'plans'
}
</script>

<template>
  <div class="space-y-4">
    <!-- Toggle de modos -->
    <ClinicalModeToggle v-model="currentMode" />

    <!-- Contenido según modo -->
    <HistoryMode
      v-if="currentMode === 'history'"
      :patient-id="patientId"
    />

    <DiagnosisMode
      v-else-if="currentMode === 'diagnosis'"
      :patient-id="patientId"
      :readonly="readonly"
      @create-plan="handleCreatePlan"
      @continue-plan="handleContinuePlan"
    />

    <PlansMode
      v-else
      :patient-id="patientId"
      :initial-plan-id="targetPlanId"
      :readonly="readonly"
    />

    <!-- Modal crear plan (compartido) -->
    <TreatmentPlanModal
      v-model="showPlanModal"
      :patient-id="patientId"
      @saved="handlePlanCreated"
    />
  </div>
</template>

Fase 4: Modificaciones Backend (Mínimas)

4.1 Endpoint para Cambios por Fecha

El timeline ya existe pero necesitamos los cambios específicos de una fecha.

Archivo: backend/app/modules/odontogram/router.py

python
@router.get("/patients/{patient_id}/odontogram/changes")
async def get_changes_at_date(
    patient_id: UUID,
    date: str = Query(..., description="Date in YYYY-MM-DD format"),
    ctx: ClinicContext = Depends(get_clinic_context),
    _: None = Depends(require_permission("odontogram.read")),
    db: AsyncSession = Depends(get_db),
) -> ApiResponse[list[HistoryEntryWithUser]]:
    """Get all odontogram changes that occurred on a specific date."""
    changes = await OdontogramService.get_changes_at_date(
        db, ctx.clinic_id, patient_id, date
    )
    return ApiResponse(data=changes)

Archivo: backend/app/modules/odontogram/service.py

python
@staticmethod
async def get_changes_at_date(
    db: AsyncSession,
    clinic_id: UUID,
    patient_id: UUID,
    date_str: str,
) -> list[OdontogramHistory]:
    """Get all changes that occurred on a specific date."""
    target_date = datetime.strptime(date_str, "%Y-%m-%d").date()

    result = await db.execute(
        select(OdontogramHistory)
        .join(ToothRecord)
        .where(
            ToothRecord.clinic_id == clinic_id,
            ToothRecord.patient_id == patient_id,
            func.date(OdontogramHistory.changed_at) == target_date,
        )
        .order_by(OdontogramHistory.changed_at)
    )
    return result.scalars().all()

4.2 Verificar Catálogo de Tratamientos

Revisar: backend/app/modules/treatment_catalog/models.py

Asegurar que las categorías permitan distinguir diagnósticos de tratamientos:

python
class TreatmentCategory(str, Enum):
    # Diagnósticos / Condiciones existentes
    DIAGNOSTIC = "diagnostic"
    EXISTING_CONDITION = "existing_condition"

    # Terapéuticos
    PREVENTIVE = "preventive"
    RESTORATIVE = "restorative"
    ENDODONTIC = "endodontic"
    PROSTHETIC = "prosthetic"
    SURGICAL = "surgical"
    ORTHODONTIC = "orthodontic"
    PERIODONTAL_TREATMENT = "periodontal_treatment"

Si no existe esta distinción, añadir campo is_diagnosis: bool al modelo.


Fase 5: Internacionalización

5.1 Claves de Traducción

Archivo: frontend/i18n/locales/es.json

json
{
  "clinical": {
    "modes": {
      "history": "Histórico",
      "diagnosis": "Diagnóstico",
      "plans": "Planes"
    },
    "history": {
      "stateAt": "Estado a fecha",
      "changesOnDate": "Cambios en esta fecha",
      "noChanges": "Sin cambios registrados"
    },
    "diagnosis": {
      "registeredConditions": "Condiciones registradas",
      "readyToCreatePlan": "¿Diagnóstico completo?",
      "createPlan": "Crear Plan de Tratamiento",
      "continuePlan": "Continuar Plan \"{name}\"",
      "selectPlan": "Seleccionar plan",
      "continue": "Continuar"
    },
    "plans": {
      "backToList": "Volver a planes",
      "treatments": "Tratamientos del Plan",
      "addTreatment": "Añadir Tratamiento",
      "total": "Total",
      "activate": "Activar Plan",
      "generateBudget": "Generar Presupuesto"
    },
    "tooth": "Diente"
  }
}

Orden de Implementación

Sprint 1: Base

  1. [ ] Separar catálogo (diagnósticos vs terapéuticos) en useTreatmentCatalog.ts
  2. [ ] Crear ClinicalModeToggle.vue
  3. [ ] Extender props de OdontogramChart.vue (mode, highlightedTeeth, onToothHover)
  4. [ ] Modificar TreatmentBar.vue (añadir prop mode, filtrar categorías)
  5. [ ] Crear HistoryMode.vue + ChangesList.vue

Sprint 2: Diagnóstico

  1. [ ] Crear ConditionsList.vue
  2. [ ] Crear DiagnosisCTA.vue
  3. [ ] Crear DiagnosisMode.vue

Sprint 3: Planes

  1. [ ] Crear PlansListView.vue
  2. [ ] Crear PlanTreatmentList.vue (con hover linking)
  3. [ ] Crear PlanDetailView.vue
  4. [ ] Crear PlansMode.vue

Sprint 4: Integración

  1. [ ] Refactorizar ClinicalTab.vue
  2. [ ] Añadir endpoint backend cambios por fecha
  3. [ ] Tests e2e
  4. [ ] Traducciones

Archivos a Crear (9 componentes)

frontend/app/components/clinical/
├── ClinicalModeToggle.vue
├── HistoryMode.vue
├── ChangesList.vue
├── DiagnosisMode.vue
├── ConditionsList.vue
├── DiagnosisCTA.vue
├── PlansMode.vue
├── PlansListView.vue
├── PlanDetailView.vue
└── PlanTreatmentList.vue

Archivos a Modificar

frontend/app/components/
├── patient/ClinicalTab.vue (refactor completo)
├── odontogram/OdontogramChart.vue (añadir props: mode, highlightedTeeth, onToothHover)
└── odontogram/TreatmentBar.vue (añadir prop mode, filtrar categorías según modo)

frontend/app/composables/
├── useTreatmentCatalog.ts (añadir filtros)
└── useTreatmentPlans.ts (añadir draftPlans)

backend/app/modules/odontogram/
├── router.py (añadir endpoint changes)
└── service.py (añadir get_changes_at_date)

Riesgos y Mitigaciones

RiesgoImpactoMitigación
Catálogo no distingue diagnósticosAltoRevisar primero, añadir campo si necesario
Performance hover linkingMedioDebounce en eventos hover
Complejidad estado entre modosMedioUsar composables compartidos
Regresiones en funcionalidad existenteAltoTests antes de refactor

Criterios de Aceptación

  1. Histórico: Usuario puede navegar timeline y ver estado pasado (solo lectura)
  2. Diagnóstico: Usuario puede registrar condiciones, ver lista, CTA contextual funciona
  3. Planes: Vista lista y detalle, hover linking funciona, añadir tratamientos desde odontograma
  4. Transiciones: Flujo diagnóstico → crear/continuar plan funciona sin fricciones
  5. Responsive: Funciona en desktop, tablet y móvil
  6. Permisos: Respeta permisos existentes (odontogram.read/write, treatmentPlans.read/write)