From 8adba0ba68a4724e4f687f2bd2b216bee84a7070 Mon Sep 17 00:00:00 2001 From: EfeDeveloper Date: Tue, 1 Sep 2026 18:54:03 -0500 Subject: [PATCH] fix: hide month-init CTA after template initialization --- .../.openspec.yaml | 2 + .../design.md | 56 ++++++++++++++ .../proposal.md | 27 +++++++ .../specs/dashboard-experience/spec.md | 24 ++++++ .../specs/monthly-expenses/spec.md | 23 ++++++ .../tasks.md | 18 +++++ openspec/specs/dashboard-experience/spec.md | 23 ++++++ openspec/specs/monthly-expenses/spec.md | 22 ++++++ src/main.ts | 1 + src/stores/expenseStore.ts | 5 ++ src/views/DashboardView.vue | 73 +++++++++---------- src/views/PendingView.vue | 29 +++++++- 12 files changed, 265 insertions(+), 38 deletions(-) create mode 100644 openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/.openspec.yaml create mode 100644 openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/design.md create mode 100644 openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/proposal.md create mode 100644 openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/specs/dashboard-experience/spec.md create mode 100644 openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/specs/monthly-expenses/spec.md create mode 100644 openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/tasks.md diff --git a/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/.openspec.yaml b/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/.openspec.yaml new file mode 100644 index 0000000..b4b3ece --- /dev/null +++ b/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-01 diff --git a/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/design.md b/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/design.md new file mode 100644 index 0000000..a3d0e37 --- /dev/null +++ b/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/design.md @@ -0,0 +1,56 @@ +## Context + +See proposal.md for motivation. The current app already distinguishes `MonthlyExpense.source` between `template` and `manual`, and `generateMonthlyExpenses()` is idempotent by template-period id. The gap is UI state: the top-level initialization affordance should be hidden in the empty month state and only shown once the period already has expenses. The new requirement also needs a transient toast/alert when the user clicks "Iniciar mes" without templates. + +Concrete files likely to change: +- `src/views/DashboardView.vue` +- `src/views/PendingView.vue` +- `src/stores/expenseStore.ts` +- `src/services/expenseService.ts` + +## Goals / Non-Goals + +**Goals:** +- Derive the top-level month-initialization CTA from whether the period has any expenses. +- Hide the top-level CTA on the empty dashboard / empty Gastos state. +- Surface a click-triggered no-templates toast/alert on dashboard and Gastos. +- Preserve the current Firestore model and idempotent template generation flow. + +**Non-Goals:** +- No new collections, fields, or backend endpoints. +- No changes to template generation payload shape. +- No changes to monthly expense creation rules for manual expenses. +- No redesign of the dashboard beyond the visibility and placement of the initialization action. + +## Decisions + +1. **Use existing expense data as the source of truth for initialization state** + - Decision: compute `hasExpensesInPeriod` from the loaded monthly expenses and use that to control the top-level CTA visibility. + - Rationale: the new behavior is based on the presence of any expense, not on whether the month has been materialized from templates. + - Alternatives considered: keeping the CTA visible until a template-sourced expense exists. Rejected because it conflicts with the requested empty-state behavior. + +2. **Expose the action in more than one visible surface** + - Decision: show the top-level CTA only when there is at least one expense, and add a small template-empty notice in Dashboard and Gastos. + - Rationale: this matches the requested empty-state simplification while still keeping the generation path discoverable. + - Alternatives considered: keeping a permanent notice visible in the header or empty state. Rejected because the request prefers feedback only on click. + +3. **Keep generation idempotent and unchanged** + - Decision: reuse `generateMonthlyExpenses(periodKey)` as-is for the actual materialization logic. + - Rationale: the backend already handles repeated initialization safely through deterministic IDs. + - Alternatives considered: a new service method for "re-initialize". Rejected because it would be redundant and increase maintenance cost. + +## Risks / Trade-offs + +- [Risk] Users may still see an initialization CTA even when it will create zero new expenses because the template set is empty. → Mitigation: preserve the existing empty-template feedback from the current generation flow. +- [Risk] The UI may briefly reflect stale initialization state before expenses finish loading. → Mitigation: derive the state only after the month expenses request resolves and keep a loading skeleton/disabled action while pending. +- [Risk] The click-triggered no-templates toast could be missed by the user if it is too subtle. → Mitigation: use the app's standard toast severity and copy that explains the next step clearly. + +## Migration Plan + +- No data migration is needed. +- Ship the UI condition change first, then verify that manual expense creation no longer removes the initialization action. +- Rollback is straightforward: revert the visibility condition and any derived state helper. + +## Open Questions + +None. diff --git a/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/proposal.md b/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/proposal.md new file mode 100644 index 0000000..088d011 --- /dev/null +++ b/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/proposal.md @@ -0,0 +1,27 @@ +## Why + +Hoy el estado del mes se infiere demasiado pronto: al crear un gasto manual desde Gastos, el dashboard deja de mostrar la acción de iniciar el mes aunque todavía no existan instancias generadas desde plantillas. Eso bloquea un flujo válido y deja al usuario sin una forma obvia de materializar sus gastos recurrentes del período. + +## What Changes + +- Ocultar la acción superior de inicializar el mes cuando el período no tenga gastos todavía. +- Mostrar la acción superior de inicializar el mes cuando ya exista al menos un gasto del período, incluso si es adicional/manual. +- Mostrar un toast o alert solo al intentar iniciar el mes cuando no existan plantillas creadas. +- Conservar la separación entre ExpenseTemplate y MonthlyExpense: las plantillas siguen siendo la fuente para generar gastos mensuales idempotentes. + +## Non-goals + +- No cambiar el modelo de datos Firestore ni las colecciones existentes. +- No alterar el payload de ExpenseTemplate ni de MonthlyExpense. +- No impedir la creación de gastos manuales mientras el mes esté sin inicializar. +- No introducir nuevas dependencias ni una nueva fuente de verdad para el período. + +## Capabilities + +### Modified Capabilities +- `dashboard-experience`: ajustar el estado y la visibilidad de la acción de “Iniciar mes” para que dependa de la existencia de al menos un gasto del período y no aparezca en el estado vacío. +- `monthly-expenses`: mantener una acción de inicialización accesible en la vista de Gastos cuando ya exista al menos un gasto del período, y mostrar una notificación solo cuando el usuario intenta iniciar el mes sin plantillas creadas. + +## Impact + +Touchpoints de UI en Dashboard y Gastos/Pendientes, más la lógica derivada que distingue gastos manuales de instancias de plantilla. No se esperan cambios en rutas, colecciones ni en la forma en que se guardan ExpenseTemplate o MonthlyExpense. diff --git a/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/specs/dashboard-experience/spec.md b/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/specs/dashboard-experience/spec.md new file mode 100644 index 0000000..de44be9 --- /dev/null +++ b/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/specs/dashboard-experience/spec.md @@ -0,0 +1,24 @@ +## ADDED Requirements + +### Requirement: Month initialization CTA is hidden on an empty dashboard +The dashboard SHALL show the top-level "Iniciar mes" action only when the current period already has at least one MonthlyExpense. When the current period has no expenses, the dashboard SHALL show only the empty-state card for that month and SHALL NOT show the top-level initialization action. + +#### Scenario: Empty month hides the top CTA +- **GIVEN** the current period has no MonthlyExpense documents +- **WHEN** the user opens the dashboard +- **THEN** the dashboard shows the empty-state card for the month +- **AND** the top-level "Iniciar mes" action is not shown + +#### Scenario: Any expense makes the top CTA visible +- **GIVEN** the current period has at least one MonthlyExpense document +- **WHEN** the user opens the dashboard +- **THEN** the dashboard shows the top-level "Iniciar mes" action + +### Requirement: Dashboard warns only when starting without templates +The dashboard SHALL notify the user only when they attempt to start the month and no ExpenseTemplate documents exist. The dashboard SHALL NOT show a permanent no-templates notice in the header or empty state. + +#### Scenario: Clicking Iniciar mes without templates shows a warning +- **GIVEN** the user has no ExpenseTemplate documents +- **WHEN** they click "Iniciar mes" from the dashboard +- **THEN** the dashboard shows a warning that there are no templates created +- **AND** the dashboard does not start the month diff --git a/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/specs/monthly-expenses/spec.md b/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/specs/monthly-expenses/spec.md new file mode 100644 index 0000000..f826430 --- /dev/null +++ b/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/specs/monthly-expenses/spec.md @@ -0,0 +1,23 @@ +## ADDED Requirements + +### Requirement: Gastos keeps month initialization accessible once the period has expenses +The Gastos view SHALL keep the initialization action available after the first load when the current period already has at least one MonthlyExpense document. When the current period has no expenses, the view SHALL not show that top-level initialization action. + +#### Scenario: Empty month hides the action +- **GIVEN** the current period has no MonthlyExpense documents +- **WHEN** the user opens Gastos +- **THEN** the top-level "Iniciar mes" action is not shown + +#### Scenario: Any expense makes the action visible +- **GIVEN** the current period has at least one MonthlyExpense document +- **WHEN** the user opens Gastos +- **THEN** the top-level "Iniciar mes" action is visible + +### Requirement: Gastos warns only when starting without templates +The Gastos view SHALL notify the user only when they attempt to start the month and no ExpenseTemplate documents exist. The view SHALL NOT show a permanent no-templates notice in the header or the empty state. + +#### Scenario: Clicking Iniciar mes without templates shows a warning +- **GIVEN** the user has no ExpenseTemplate documents +- **WHEN** they click "Iniciar mes" from Gastos +- **THEN** the view shows a warning that there are no templates created +- **AND** the month is not started diff --git a/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/tasks.md b/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/tasks.md new file mode 100644 index 0000000..4a522bb --- /dev/null +++ b/openspec/changes/archive/2026-09-01-allow-month-initialization-after-manual-expenses/tasks.md @@ -0,0 +1,18 @@ +## 1. State derivation + +- [x] 1.1 Add a derived check for whether the current period has any expenses, and verify the empty month hides the top initialization action. +- [x] 1.2 Add a loaded-state check for template lists, and verify click-time template warnings only fire after templates have loaded. + +## 2. Dashboard entry point + +- [x] 2.1 [ui] Update DashboardView so the top "Iniciar mes" affordance is hidden on an empty month, and verify the empty dashboard shows the card action instead. +- [x] 2.2 [ui] Show a toast when Dashboard "Iniciar mes" is clicked without templates, and verify no persistent warning is rendered in the header or card. + +## 3. Gastos entry point + +- [x] 3.1 [ui] Update PendingView so the top initialization action is hidden on an empty month, and verify it appears after any expense exists. +- [x] 3.2 [ui] Show a toast when Gastos "Iniciar mes" is clicked without templates, and verify no persistent warning is rendered. + +## 4. Validation + +- [x] 4.1 [qa] Run a browser smoke check on the dashboard and /pending flow to verify the empty month hides the top CTA, the card button remains available, the CTA appears after a manual expense, and no-templates feedback is toast-only. diff --git a/openspec/specs/dashboard-experience/spec.md b/openspec/specs/dashboard-experience/spec.md index d37b61e..b50477e 100644 --- a/openspec/specs/dashboard-experience/spec.md +++ b/openspec/specs/dashboard-experience/spec.md @@ -65,3 +65,26 @@ The dashboard SHALL render pending expenses under the "Próximos por pagar" sect - **GIVEN** a pending expense is listed under "Próximos por pagar" - **WHEN** the user checks the checkbox - **THEN** the expense status transitions to paid and all connected dashboard metrics update reactively + +### Requirement: Month initialization CTA is hidden on an empty dashboard +The dashboard SHALL show the top-level "Iniciar mes" action only when the current period already has at least one MonthlyExpense. When the current period has no expenses, the dashboard SHALL show only the empty-state card for that month and SHALL NOT show the top-level initialization action. + +#### Scenario: Empty month hides the top CTA +- **GIVEN** the current period has no MonthlyExpense documents +- **WHEN** the user opens the dashboard +- **THEN** the dashboard shows the empty-state card for the month +- **AND** the top-level "Iniciar mes" action is not shown + +#### Scenario: Any expense makes the top CTA visible +- **GIVEN** the current period has at least one MonthlyExpense document +- **WHEN** the user opens the dashboard +- **THEN** the dashboard shows the top-level "Iniciar mes" action + +### Requirement: Dashboard warns only when starting without templates +The dashboard SHALL notify the user only when they attempt to start the month and no ExpenseTemplate documents exist. The dashboard SHALL NOT show a permanent no-templates notice in the header or empty state. + +#### Scenario: Clicking Iniciar mes without templates shows a warning +- **GIVEN** the user has no ExpenseTemplate documents +- **WHEN** they click "Iniciar mes" from the dashboard +- **THEN** the dashboard shows a warning that there are no templates created +- **AND** the dashboard does not start the month diff --git a/openspec/specs/monthly-expenses/spec.md b/openspec/specs/monthly-expenses/spec.md index c28d3ef..36a3108 100644 --- a/openspec/specs/monthly-expenses/spec.md +++ b/openspec/specs/monthly-expenses/spec.md @@ -184,3 +184,25 @@ Historial SHALL show the same search field used on Gastos (`ListSearchBar` behav - **WHEN** they want a different month - **THEN** they change it in the calendar popover - **AND** there is no dedicated month scroller with back and forward buttons + +### Requirement: Gastos keeps month initialization accessible once the period has expenses +The Gastos view SHALL keep the initialization action available after the first load when the current period already has at least one MonthlyExpense document. When the current period has no expenses, the view SHALL not show that top-level initialization action. + +#### Scenario: Empty month hides the action +- **GIVEN** the current period has no MonthlyExpense documents +- **WHEN** the user opens Gastos +- **THEN** the top-level "Iniciar mes" action is not shown + +#### Scenario: Any expense makes the action visible +- **GIVEN** the current period has at least one MonthlyExpense document +- **WHEN** the user opens Gastos +- **THEN** the top-level "Iniciar mes" action is visible + +### Requirement: Gastos warns only when starting without templates +The Gastos view SHALL notify the user only when they attempt to start the month and no ExpenseTemplate documents exist. The view SHALL NOT show a permanent no-templates notice in the header or the empty state. + +#### Scenario: Clicking Iniciar mes without templates shows a warning +- **GIVEN** the user has no ExpenseTemplate documents +- **WHEN** they click "Iniciar mes" from Gastos +- **THEN** the view shows a warning that there are no templates created +- **AND** the month is not started diff --git a/src/main.ts b/src/main.ts index e516154..3ba7560 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,6 +2,7 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import router from './router' import { useAuthStore } from '@/stores/authStore' +import 'vue-sonner/style.css' import './style.css' import App from './App.vue' diff --git a/src/stores/expenseStore.ts b/src/stores/expenseStore.ts index 26cd8e6..e568494 100644 --- a/src/stores/expenseStore.ts +++ b/src/stores/expenseStore.ts @@ -22,6 +22,10 @@ export const useExpenseStore = defineStore('expenses', () => { const pendingExpenses = computed(() => expenses.value.filter((e) => e.status === 'pending')) + const hasMonthInitializedFromTemplates = computed(() => + expenses.value.some((e) => e.source !== 'manual'), + ) + const summary = computed(() => { const paid = paidExpenses.value const pending = pendingExpenses.value @@ -110,6 +114,7 @@ export const useExpenseStore = defineStore('expenses', () => { currentPeriod, paidExpenses, pendingExpenses, + hasMonthInitializedFromTemplates, summary, fetchExpenses, toggleStatus, diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 2d76b11..d5b319a 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -2,6 +2,7 @@ import { computed, onMounted, ref } from 'vue' import { RouterLink } from 'vue-router' import { useExpenseStore } from '@/stores/expenseStore' +import { useTemplateStore } from '@/stores/templateStore' import { useBudgetStore } from '@/stores/budgetStore' import { getCurrentPeriodKey, @@ -25,6 +26,7 @@ import type { SaveBudgetInput } from '@/types/budget' import { Skeleton } from '@/components/ui/skeleton' import { Card, CardContent } from '@/components/ui/card' import { Button } from '@/components/ui/button' +import { toast } from 'vue-sonner' import { AlertDialog, AlertDialogAction, @@ -38,11 +40,11 @@ import { import { PackageOpen, Clock, RefreshCw, Smile } from '@lucide/vue' const store = useExpenseStore() +const templateStore = useTemplateStore() const budgetStore = useBudgetStore() const period = getCurrentPeriodKey() const periodLabel = formatPeriodLabel(period) const ready = ref(false) -const initMessage = ref<{ text: string; tone: 'info' | 'warning' } | null>(null) const detailOpen = ref(false) const detailRecord = ref(null) const sheetOpen = ref(false) @@ -64,6 +66,7 @@ function openExpenseDetail(expense: MonthlyExpense) { onMounted(async () => { const [_, __, history] = await Promise.all([ store.fetchExpenses(period).catch(() => undefined), + templateStore.fetchTemplates().catch(() => undefined), budgetStore.fetchBudget(period).catch(() => undefined), getRecentPeriodsHistory(period, 6).catch(() => []), ]) @@ -79,34 +82,29 @@ async function handleToggle(id: string, status: 'pending' | 'paid') { } async function handleInitMonth() { + if (templateStore.templates.length === 0) { + toast.warning('No tienes plantillas creadas. Ve a Plantillas para crear la base de tus gastos mensuales.') + return + } + try { const result = await store.generateForPeriod(period) if (result.created === 0 && result.skipped === 0) { - initMessage.value = { - text: 'No hay plantillas activas. Crea o activa una plantilla para iniciar el mes.', - tone: 'info', - } + toast.info('No hay plantillas activas. Crea o activa una plantilla para iniciar el mes.') return } if (result.created === 0 && result.skipped > 0) { - initMessage.value = { - text: 'Este mes ya estaba iniciado.', - tone: 'warning', - } + toast.warning('Este mes ya estaba iniciado.') return } - - initMessage.value = null + toast.success(`${result.created} gastos generados para el mes`) getRecentPeriodsHistory(period, 6).then((h) => { historyPoints.value = h }).catch(() => undefined) } catch { - initMessage.value = { - text: 'No se pudo iniciar el mes. Intenta nuevamente.', - tone: 'warning', - } + toast.error('No se pudo iniciar el mes. Intenta nuevamente.') } } @@ -164,12 +162,16 @@ const variablePendingAmount = computed(() => { .filter((e) => e.source === 'manual' || !e.templateId) .reduce((sum, e) => sum + e.amount, 0) }) + +const showInitMonthAction = computed( + () => ready.value && store.expenses.length > 0 && !store.hasMonthInitializedFromTemplates, +) diff --git a/src/views/PendingView.vue b/src/views/PendingView.vue index d307e25..61e34eb 100644 --- a/src/views/PendingView.vue +++ b/src/views/PendingView.vue @@ -1,6 +1,7 @@