diff --git a/expenses_manager/.env.example b/expenses_manager/.env.example new file mode 100644 index 0000000..faaef7b --- /dev/null +++ b/expenses_manager/.env.example @@ -0,0 +1,17 @@ +# Django +DEBUG=True +SECRET_KEY=genera-una-clave-aqui + +# Hosts (separados por comas). En producción, tu dominio real. +ALLOWED_HOSTS=localhost,127.0.0.1 +CSRF_TRUSTED_ORIGINS= + +# Base de datos. +# En local puedes dejar DB_ENGINE vacío o en sqlite3 para usar SQLite sin más. +# Para PostgreSQL, pon DB_ENGINE=postgresql y rellena el resto. +DB_ENGINE=sqlite3 +DB_NAME= +DB_USER= +DB_PASSWORD= +DB_HOST=localhost +DB_PORT=5432 \ No newline at end of file diff --git a/expenses_manager/expenses/forms.py b/expenses_manager/expenses/forms.py index 3d88580..83ee34e 100644 --- a/expenses_manager/expenses/forms.py +++ b/expenses_manager/expenses/forms.py @@ -122,15 +122,15 @@ class GoalForm(forms.ModelForm): class Meta: model = Goal fields = [ - 'name', - 'kind', - 'target_amount', - 'category', + "name", + "kind", + "target_amount", + "category", "include_subcategories", "account", "start_date", "period", - "show_on_home" + "show_on_home", ] widgets = { "start_date": forms.DateInput( diff --git a/expenses_manager/expenses/models.py b/expenses_manager/expenses/models.py index 688e0d9..50b1181 100644 --- a/expenses_manager/expenses/models.py +++ b/expenses_manager/expenses/models.py @@ -5,7 +5,6 @@ from django.conf import settings from django.db.models import Sum from functools import cached_property from django.utils.text import slugify -from django.db.models.fields import related from django.db.models.functions import ExtractMonth diff --git a/expenses_manager/expenses/static/expenses/css/base.css b/expenses_manager/expenses/static/expenses/css/base.css index 80a1763..22b7b85 100644 --- a/expenses_manager/expenses/static/expenses/css/base.css +++ b/expenses_manager/expenses/static/expenses/css/base.css @@ -1,72 +1,276 @@ +:root { + --color-bg: #f6f8f6; + --color-surface: #ffffff; + --color-surface-alt: #f2f5f3; + --color-text: #1c1917; + --color-text-muted: #6b6560; + --color-border: #d8d4d0; + + --color-nav-bg: #10241d; + --color-nav-text: #e7f5f0; + --color-nav-text-muted: #8fb8ac; + + --color-primary: #0f766e; + --color-primary-hover: #0b5c56; + --color-secondary: #57534e; + --color-secondary-hover: #44403c; + --color-on-primary: #ffffff; + + --color-chip-bg: #e3ece8; + --color-chip-hover: #d2e0da; + --color-focus-ring: #5eead4; + + --color-success-bg: #e6f4ea; + --color-success-text: #166534; + --color-success-accent: #1e7e34; + + --color-danger-bg: #fee2e2; + --color-danger-text: #991b1b; + --color-danger-accent: #b91c1c; + --color-danger-accent-hover: #991b1b; + + --color-warning-bg: #fef3c7; + --color-warning-text: #92400e; + --color-warning-accent: #d97706; + + --color-table-head: #edf2ef; + --color-table-alt: #f8faf9; + --color-table-hover: #e9f1ed; + --color-progress-track: #e0e0e0; + + --chart-grid-color: rgba(0, 0, 0, 0.1); + --chart-fill-color: rgba(15, 118, 110, 0.2); +} + +[data-theme="dark"] { + --color-bg: #0d1512; + --color-surface: #16211c; + --color-surface-alt: #1b2a23; + --color-text: #e7f0eb; + --color-text-muted: #9fb0a8; + --color-border: #2a3a33; + + --color-nav-bg: #0a1712; + --color-nav-text: #e7f5f0; + --color-nav-text-muted: #8fb8ac; + + --color-chip-bg: #3a5546; + --color-chip-hover: #46654f; + --color-focus-ring: #5eead4; + + --color-success-bg: #113322; + --color-success-text: #86efac; + + --color-danger-bg: #3f1d1d; + --color-danger-text: #fca5a5; + + --color-warning-bg: #3f2d0d; + --color-warning-text: #fcd34d; + + --color-table-head: #1b2a23; + --color-table-alt: #182420; + --color-table-hover: #20302a; + --color-progress-track: #2a3a33; + + --chart-grid-color: rgba(255, 255, 255, 0.12); + --chart-fill-color: rgba(15, 118, 110, 0.35); +} + +* { + box-sizing: border-box; +} + body { margin: 0; font-family: system-ui, sans-serif; - background: #f7f7f7; + background: var(--color-bg); + color: var(--color-text); } -a.danger { - color: #b91c1c; +a { + color: var(--color-primary); } +a.danger, +a.danger:visited, +.danger { + color: var(--color-danger-accent); +} + +/* ========================= */ +/* Topbar / navigation */ +/* ========================= */ + .topbar { - background: #1f2937; - padding: 12px 20px; + background: var(--color-nav-bg); } .nav { display: flex; align-items: center; - gap: 16px; + gap: 0.5rem; + padding: 0.75rem 1rem; + flex-wrap: wrap; } -.nav a { - color: #e5e7eb; +.nav-item, +.nav .nav-link { + color: var(--color-nav-text); text-decoration: none; font-weight: 500; + padding: 0.4rem 0.1rem; } -.nav .nav-link { - text-decoration: none; - background-color: none; +.logout-form { + display: inline; + margin: 0; + padding: 0; +} + +.logout-form .nav-link { + background: none; border: none; - padding: 8px 12px; font: inherit; - color: #333; cursor: pointer; + color: var(--color-nav-text); + padding: 0.4rem 0.1rem; } -.nav a:hover, +.nav-item:hover, .nav .nav-link:hover { text-decoration: underline; } .nav-item.active { - font-weight: bold; - border-bottom: 2px solid white; -} - -.logout-form { - margin: 0; - border: none; - cursor: pointer; - color: white; + font-weight: 700; + border-bottom: 2px solid var(--color-nav-text); } .spacer { - flex: 1; + flex: 1 1 auto; } .user { - color: #9ca3af; - margin-right: 8px; + color: var(--color-nav-text-muted); + margin-right: 0.5rem; } +.nav-toggle { + display: none; + background: none; + border: none; + flex-direction: column; + justify-content: center; + gap: 4px; + padding: 0.5rem; + cursor: pointer; +} + +.nav-toggle-bar { + display: block; + width: 22px; + height: 2px; + background: var(--color-nav-text); + border-radius: 2px; +} + +.theme-toggle { + background: none; + border: none; + cursor: pointer; + font-size: 1.1rem; + line-height: 1; + color: var(--color-nav-text); + padding: 0.4rem; +} + +.dropdown { + position: relative; +} + +.dropdown-toggle { + background: none; + border: none; + display: inline-block; + font: inherit; + color: var(--color-nav-text); + cursor: pointer; + padding: 0.4rem 0.1rem; +} + +.dropdown-toggle:focus-visible { + outline: 2px solid var(--color-focus-ring); + outline-offset: 2px; +} + +.dropdown-menu { + display: none; + position: absolute; + top: 100%; + left: 0; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 6px; + min-width: 150px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + z-index: 10; +} + +.dropdown-menu a { + display: block; + padding: 10px; + text-decoration: none; + color: var(--color-text); +} + +.dropdown-menu a:hover { + background-color: var(--color-surface-alt); +} + +.dropdown.dropdown--open .dropdown-menu { + display: block; +} + +/* ========================= */ +/* Messages */ +/* ========================= */ + +.messages { + padding: 0 1rem; +} + +.message { + padding: 0.75rem 1rem; + border-radius: 6px; + margin-bottom: 0.75rem; + font-size: 0.9rem; +} + +.message.success { + background: var(--color-success-bg); + color: var(--color-success-text); +} + +.message.error { + background: var(--color-danger-bg); + color: var(--color-danger-text); +} + +.message.warning { + background: var(--color-warning-bg); + color: var(--color-warning-text); +} + +/* ========================= */ +/* Layout */ +/* ========================= */ + .content { - padding: 24px; + padding: 1.5rem; } .dashboard-context { - margin-bottom: 1rem; + margin-bottom: 1rem; } .dashboard-context h2 { @@ -74,18 +278,200 @@ a.danger { } .muted { - color: #6b7280; + color: var(--color-text-muted); font-size: 0.85rem; } +/* ========================= */ +/* Buttons */ +/* ========================= */ + +.btn, +button[type="submit"] { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.35rem; + height: 2.25rem; + box-sizing: border-box; + padding: 0 0.9rem; + border: none; + border-radius: 6px; + font: inherit; + font-size: 0.9rem; + font-weight: 500; + text-decoration: none; + cursor: pointer; + line-height: 1; + background: var(--color-primary); + color: var(--color-on-primary); +} + +button[type="submit"]:hover, +.btn-primary:hover { + background: var(--color-primary-hover); +} + +.btn-primary { + background: var(--color-primary); + color: var(--color-on-primary); +} + +.btn-secondary { + background: var(--color-secondary); + color: var(--color-on-primary); +} + +.btn-secondary:hover { + background: var(--color-secondary-hover); +} + +.btn-danger { + background: var(--color-danger-accent); + color: var(--color-on-primary); +} + +.btn-danger:hover { + background: var(--color-danger-accent-hover); +} + +.form-actions { + display: flex; + align-items: center; + gap: 0.75rem; + margin-top: 1rem; + flex-wrap: wrap; +} + +/* ========================= */ +/* Filters (expense list) */ +/* ========================= */ + +.filters { + margin-bottom: 1.5rem; +} + +.filters-main { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} + +.filters-advanced { + margin-top: 0.5rem; +} + +.advanced-content { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; + margin-top: 0.5rem; +} + +.tags-filters { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.active-tags { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + +.tag { + display: inline-block; + padding: 4px 10px; + border-radius: 16px; + background: var(--color-chip-bg); + text-decoration: none; + margin-right: 4px; + font-size: 0.85em; + color: var(--color-text); +} + +.tag.active { + background: var(--color-primary); + color: var(--color-on-primary); +} + +.tag:hover { + background: var(--color-chip-hover); +} + +/* ========================= */ +/* Dashboard filters/presets */ +/* ========================= */ + +.dashboard-filters { + padding: 1rem; + margin-bottom: 1.5rem; + background: var(--color-surface-alt); + border-radius: 8px; +} + +.dashboard-presets { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} + +.preset { + padding: 4px 12px; + border-radius: 16px; + background: var(--color-chip-bg); + text-decoration: none; + color: var(--color-text); + font-size: 0.9em; +} + +.preset:hover { + background: var(--color-chip-hover); +} + +.preset.active { + background: var(--color-primary); + color: var(--color-on-primary); +} + +.clear-filters-row { + margin-top: 0.5rem; +} + +.checkbox { + display: inline-flex; + align-items: center; + gap: 0.4rem; + color: var(--color-text); + vertical-align: middle; +} + +.clear-filters { + color: var(--color-danger-accent); + text-decoration: none; + font-size: 0.9em; +} + +.clear-filters:hover { + text-decoration: underline; +} + +/* ========================= */ +/* Cards / charts / dashboard grid */ +/* ========================= */ + .dashboard-grid { display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 20px + grid-template-columns: repeat(2, 1fr); + gap: 20px; } .card { - background: #fff; + background: var(--color-surface); border-radius: 8px; padding: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); @@ -108,90 +494,118 @@ a.danger { margin-bottom: 4rem; } +.charts-container > section { + flex: 1 1 320px; + min-width: 280px; +} + .chart-box { - height: 300px; - margin-bottom: 3rem; + position: relative; + height: 320px; + width: 100%; + margin-top: 0.5rem; +} + +.chart-box canvas { + max-width: 100%; } .canvas-wrapper { flex-grow: 1; min-height: 0; - position: relative; width: 100%; + position: relative; } -.card-chart canvas { - display: block; +.canvas-wrapper canvas { width: 100% !important; height: 100% !important; } +/* ========================= */ +/* KPIs */ +/* ========================= */ + .kpi-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; - margin: 2rem 0; + margin: 1.5rem 0; } .kpi-card { - background: #ffffff; + background: var(--color-surface); border-radius: 8px; padding: 1rem; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .kpi-label { + display: block; font-size: 0.85rem; - color: #666; + color: var(--color-text-muted); } .kpi-value { display: block; - font-size: 1.5rem; + font-size: 1.4rem; font-weight: bold; margin-top: 0.25rem; } .comparison { - background: #f9f9f9; + background: var(--color-surface-alt); padding: 1rem; margin-bottom: 2rem; border-radius: 8px; } -/* .comparison strong { - color:#b91c1c; -} */ - -.comparison strong.positive { - color: #166534; +strong.positive { + color: var(--color-success-text); } -.comparison strong.negative { - color:#b91c1c; +strong.negative { + color: var(--color-danger-accent); } +/* ========================= */ +/* Tables */ +/* ========================= */ + table { width: 100%; border-collapse: collapse; - margin-top: 1rem; + margin-top: 0.5rem; } -th, td { - padding: 0.6rem 0.75rem; +.table-wrap { + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + margin-bottom: 1.5rem; +} + +.table-wrap table { + white-space: nowrap; +} + +th, +td { + padding: 8px 12px; text-align: left; + border-bottom: 1px solid var(--color-border); } -thead { - background: #f4f6f8; +thead th { + background: var(--color-table-head); } -tbody tr:nth-child(even) { - background: #fafafa; +tr:nth-child(even) { + background: var(--color-table-alt); } -tbody tr:hover { - background: #f0f4f8; +tr:hover { + background: var(--color-table-hover); } .table-actions a { @@ -203,7 +617,7 @@ tbody tr:hover { .empty-state { text-align: center; padding: 2rem; - color: #666; + color: var(--color-text-muted); } .empty-state a { @@ -211,278 +625,69 @@ tbody tr:hover { margin-top: 0.5rem; } -.btn { - background: #2563eb; - color: white; - padding: 0.4rem 0.7rem; - border-radius: 6px; - text-decoration: none; - font-size: 0.9rem; +.pagination { + margin-top: 1rem; } -.btn.secondary { - background: #6b7280; -} - -.messages { - margin: 1rem 0; -} - -.message { - padding: 0.6rem 0.8rem; - border-radius: 6px; - margin-bottom: 0.5rem; - font-size: 0.9rem; -} - -.message.success { - background: #e6f4ea; - color: #166534; -} - -.message.error { - background: #fee2e2; - color: #991b1b; -} - -.message.warning { - background: #fef3c7; - color: #92400e; -} - -.dashboard-filters { - margin: 1rem 0 1.5rem; - padding: 1rem; - background: #f9fafb; - border-radius: 8px; -} - -.dashboard-filters form { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - align-items: center; -} - -.dashboard-filters select, -.dashboard-filters button { - padding: 0.4rem 0.6rem; -} - -.dashboard-filters .checkbox { - display: flex; - gap: 0.3rem; - align-items: center; -} - -.dashboard-presets { +.step-links { display: flex; gap: 0.5rem; -} - -.preset { - padding: 0.35rem 0.6rem; - border-radius: 6px; - background: #e5e7eb; - text-decoration: none; - color: #111827; - font-size: 0.85rem; -} - -.preset:hover { - background: #d1d5db; -} - -.preset.active { - background: #2563eb; - color: white; -} - -.filters { - margin-bottom: 1.5rem; -} - -.filters-main { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; align-items: center; -} - -.filters-advanced { - margin-top: 0.5rem; -} - -.active-tags { - display: flex; flex-wrap: wrap; - gap: 0.4rem; - margin-top: 0.3rem; } -.tag{ - /* display: inline-block; */ - padding: 0.2rem 0.5rem; - border-radius: 4px; - background: #eee; - text-decoration: none; - margin-right: 4px; - font-size: 0.85rem; - color: #333; -} - -.tag.active { - background: #333; - color: white; -} - -.tag:hover { - background: #dbe3ee; -} - -.dropdown { - position: relative; - cursor: pointer; - color: #e5e7eb; -} - -.dropdown.active { - font-weight: bold; - border-bottom: 2px solid white; -} - -.dropdown-toggle { - padding: 10px; +.step-links a { display: inline-block; -} - -.dropdown-menu { - display: none; - position: absolute; - top: 100%; - left: 0; - background: white; - border: 1px solid #ddd; - border-radius: 6px; - min-width: 150px; - box-shadow: 0 4px 8px rgba(0,0,0,0.1); -} - -.dropdown-menu a { - display: block; - padding: 10px; + padding: 4px 12px; + border-radius: 16px; + background: var(--color-chip-bg); + color: var(--color-text); text-decoration: none; - color: #333; + font-size: 0.9em; } -.dropdown-menu a:hover { - background-color: #f5f5f5; +.step-links a:hover { + background: var(--color-chip-hover); } -.dropdown:hover .dropdown-menu { - display: block; +.step-links span { + color: var(--color-text-muted); + font-size: 0.9em; } -.progress-container { - display: flex; - align-items: center; - gap: 10px; +.badge { + display: inline-block; + padding: 2px 10px; + border-radius: 12px; + font-size: 0.8rem; + font-weight: 600; } -.progress-label { - min-width: 50px; - font-size: 12px; - color: #666; +.badge-active { + background-color: var(--color-success-bg); + color: var(--color-success-text); } -.progress-bar { - width: 100%; - background: #e0e0e0; - border-radius: 6px; - height: 14px; - overflow: hidden; -} - -.progress-fill { - height: 100%; - transition: width 0.6s ease-in-out; -} - -.progress-fill.low { - background: #e74c3c; -} - -.progress-fill.medium { - background: #f1c40f; -} - -.progress-fill.high { - background: #2ecc71; -} - -.goals-widget { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 12px; -} - -.goal-card { - padding: 10px; - border-radius: 8px; - background: #f9f9f9; +.badge-inactive { + background-color: var(--color-danger-bg); + color: var(--color-danger-text); } .row-inactive { opacity: 0.55; } -.badge { - display: inline-block; - padding: 2px 8px; - border-radius: 12px; - font-size: 0.8em; - font-weight: 600; +/* ========================= */ +/* Forms */ +/* ========================= */ + +.form-field { + margin-bottom: 1rem; } -.badge-active { - background-color: #e6f4ea; - color: #1e7e34; -} - -.badge-inactive { - background-color: #fbe9e7; - color: #b71c1c; -} - -.settings-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 16px; - margin-top: 20px; -} - -.settings-card { - display: block; - padding: 16px; - border: 1px solid #ddd; - border-radius: 8px; - text-decoration: none; - color: inherit; - transition: box-shadow 0.15s ease; -} - -.settings-card:hover { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -.settings-card h3 { - margin: 0 0 4px 0; -} - -.settings-card p { - margin: 0; - font-size: 0.9em; - color: #666; +.form-errors { + color: var(--color-danger-accent); + font-size: 0.85rem; } .tag-chip-list { @@ -493,7 +698,7 @@ tbody tr:hover { max-height: 160px; overflow-y: auto; padding: 8px; - border: 1px solid #ddd; + border: 1px solid var(--color-border); border-radius: 8px; } @@ -502,38 +707,210 @@ tbody tr:hover { align-items: center; gap: 4px; padding: 4px 10px; - border: 1px solid #ccc; + border: 1px solid var(--color-border); border-radius: 16px; font-size: 0.85em; cursor: pointer; user-select: none; - background-color: #fafafa; + background-color: var(--color-surface-alt); } .tag-chip input[type="checkbox"] { - accent-color: #1e7e34; + accent-color: var(--color-success-accent); } .tag-chip:has(input:checked) { - background-color: #e6f4ea; - border-color: #1e7e34; - color: #1e7e34; + background-color: var(--color-success-bg); + border-color: var(--color-success-accent); + color: var(--color-success-text); } -.progress-fill.ok { - background-color: #1e88e5; +/* ========================= */ +/* Progress bars */ +/* ========================= */ + +.progress-container { + display: flex; + align-items: center; + gap: 10px; } -.progress-fill.complete { - background-color: #1e7e34; + +.progress-label { + min-width: 90px; + font-size: 0.85rem; + color: var(--color-text-muted); } -.progress-fill.warning { - background-color: #f9a825; + +.progress-bar { + width: 100%; + background: var(--color-progress-track); + border-radius: 6px; + height: 10px; + overflow: hidden; } -.progress-fill.danger { - background-color: #b71c1c; + +.progress-fill { + height: 100%; + transition: width 0.3s ease-in-out; +} + +.progress-fill.low, +.progress-fill.danger { + background-color: var(--color-danger-accent); +} + +.progress-fill.medium, +.progress-fill.warning { + background-color: var(--color-warning-accent); +} + +.progress-fill.high, +.progress-fill.complete { + background-color: var(--color-success-accent); +} + +.progress-fill.ok { + background-color: var(--color-primary); +} + +/* ========================= */ +/* Goals */ +/* ========================= */ + +.goals-widget { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1rem; +} + +.goal-card { + padding: 1rem; + border-radius: 8px; + background: var(--color-surface-alt); +} + +/* ========================= */ +/* Settings */ +/* ========================= */ + +.settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.settings-card { + display: block; + padding: 1rem; + border: 1px solid var(--color-border); + border-radius: 8px; + text-decoration: none; + color: var(--color-text); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.settings-card:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.settings-card h3 { + margin: 0 0 0.25rem 0; +} + +.settings-card p { + margin: 0; + font-size: 0.9em; + color: var(--color-text-muted); +} + +/* ========================= */ +/* Auth pages */ +/* ========================= */ + +.auth-container { + max-width: 400px; + margin: 4rem auto; + padding: 2rem; + background: var(--color-surface); + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .auth-help { - margin-top: 16px; - font-size: 0.9em; -} \ No newline at end of file + margin-top: 1rem; + font-size: 0.9rem; + color: var(--color-text-muted); +} + +/* ========================= */ +/* Responsive */ +/* ========================= */ + +@media (max-width: 768px) { + .topbar .nav { + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + } + + .nav-toggle { + display: flex; + } + + .nav { + display: none; + flex-direction: column; + align-items: flex-start; + width: 100%; + margin-top: 0.5rem; + } + + .nav.nav--open { + display: flex; + } + + .nav .spacer { + display: none; + } + + .dropdown-menu { + position: static; + box-shadow: none; + min-width: 0; + padding-left: 1rem; + } + + .dropdown-menu a { + color: var(--color-nav-text); + } + + .dropdown-menu a:hover { + background-color: transparent; + text-decoration: underline; + } + + .dashboard-grid { + grid-template-columns: 1fr; + } + + .card-chart { + height: 320px; + } +} + +@media (max-width: 480px) { + .kpi-grid, + .goals-widget, + .settings-grid { + grid-template-columns: 1fr; + } + + .chart-box { + height: 240px; + } + + .card-chart { + height: 260px; + } +} diff --git a/expenses_manager/expenses/static/expenses/js/charts.js b/expenses_manager/expenses/static/expenses/js/charts.js new file mode 100644 index 0000000..437e2eb --- /dev/null +++ b/expenses_manager/expenses/static/expenses/js/charts.js @@ -0,0 +1,88 @@ +(function () { + function getActiveTheme() { + return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'; + } + + function getChartColors() { + var styles = getComputedStyle(document.documentElement); + var theme = getActiveTheme(); + + return { + theme: theme, + gridColor: styles.getPropertyValue('--chart-grid-color').trim() || 'rgba(0, 0, 0, 0.1)', + tickColor: styles.getPropertyValue('--color-text-muted').trim() || '#6b6560', + primaryColor: styles.getPropertyValue('--color-primary').trim() || '#0f766e', + primaryFill: styles.getPropertyValue('--chart-fill-color').trim() || 'rgba(15, 118, 110, 0.2)', + palette: ['#ff6384', '#36a2eb', '#cc65fe', '#ffce56', '#4bc0c0', '#ffa1b5'] + }; + } + + // Aplica los colores de grid/ticks a las escalas cartesianas (x/y) de un gráfico. + // Se usa tanto al crear el gráfico (colores embebidos en las opciones iniciales) + // como al refrescar el tema en caliente sobre un gráfico ya existente. + function applyCartesianColors(chart, colors) { + var scales = chart.options.scales; + + if (!scales) { + return; + } + + Object.keys(scales).forEach(function (axis) { + var scale = scales[axis]; + + if (!scale) { + return; + } + + // Object.assign crea un objeto grid/ticks nuevo en vez de mutar en el + // sitio (scale.grid.color = ...). Chart.js reconstruye internamente los + // objetos de opciones de un gráfico 'line' ya creado, y mutar una + // propiedad suelta sobre esa estructura reactiva provoca + // "TypeError: t.startsWith is not a function" al redibujar. Reemplazar + // el objeto entero evita tocar esas referencias internas. + if (typeof colors.gridColor === 'string' && colors.gridColor) { + scale.grid = Object.assign({}, scale.grid, { color: colors.gridColor }); + } + + if (typeof colors.tickColor === 'string' && colors.tickColor) { + scale.ticks = Object.assign({}, scale.ticks, { color: colors.tickColor }); + } + }); + } + + // Los colores de tema se fijan en las opciones al CREAR cada gráfico (evita el + // bug de mutar un gráfico 'line' recién construido). Para que el gráfico + // reaccione a un cambio de tema posterior sin recrearlo, cada plantilla registra + // aquí su instancia junto con un callback que sabe cómo reaplicar sus colores. + var registeredCharts = []; + + function registerThemedChart(chart, applyColors) { + registeredCharts.push({ chart: chart, applyColors: applyColors }); + } + + function refreshThemedCharts() { + var colors = getChartColors(); + + registeredCharts.forEach(function (entry) { + entry.applyColors(entry.chart, colors); + // 'none' desactiva la animación de la actualización: solo repinta con + // los nuevos colores, sin la transición de entrada de datos. + entry.chart.update('none'); + }); + } + + // Observa el atributo data-theme del (lo cambia el toggle de tema) para + // refrescar todos los gráficos registrados en cuanto el usuario cambia de tema, + // sin necesidad de recargar la página. + new MutationObserver(function (mutations) { + var themeChanged = mutations.some(function (m) { return m.attributeName === 'data-theme'; }); + + if (themeChanged) { + refreshThemedCharts(); + } + }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); + + window.getChartColors = getChartColors; + window.applyCartesianColors = applyCartesianColors; + window.registerThemedChart = registerThemedChart; +})(); diff --git a/expenses_manager/expenses/static/expenses/js/nav.js b/expenses_manager/expenses/static/expenses/js/nav.js new file mode 100644 index 0000000..c382c5c --- /dev/null +++ b/expenses_manager/expenses/static/expenses/js/nav.js @@ -0,0 +1,30 @@ +document.addEventListener('DOMContentLoaded', function () { + var toggle = document.getElementById('navToggle'); + var nav = document.getElementById('mainNav'); + + if (!toggle || !nav) { + return; + } + + toggle.addEventListener('click', function () { + var isOpen = nav.classList.toggle('nav--open'); + toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); + }); + + var dropdownToggle = document.getElementById('dropdownToggle'); + var dropdown = document.getElementById('settingsDropdown'); + + if (dropdownToggle && dropdown) { + dropdownToggle.addEventListener('click', function () { + var isOpen = dropdown.classList.toggle('dropdown--open'); + dropdownToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); + }); + + document.addEventListener('click', function (event) { + if (!dropdown.contains(event.target)) { + dropdown.classList.remove('dropdown--open'); + dropdownToggle.setAttribute('aria-expanded', 'false'); + } + }); + } +}); diff --git a/expenses_manager/expenses/static/expenses/js/theme.js b/expenses_manager/expenses/static/expenses/js/theme.js new file mode 100644 index 0000000..2deaa43 --- /dev/null +++ b/expenses_manager/expenses/static/expenses/js/theme.js @@ -0,0 +1,20 @@ +document.addEventListener('DOMContentLoaded', function () { + var toggle = document.getElementById('themeToggle'); + + if (!toggle) { + return; + } + + function applyIcon(theme) { + toggle.textContent = theme === 'dark' ? '☀️' : '🌙'; + } + + applyIcon(document.documentElement.getAttribute('data-theme')); + + toggle.addEventListener('click', function () { + var next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', next); + localStorage.setItem('theme', next); + applyIcon(next); + }); +}); diff --git a/expenses_manager/expenses/templates/categories/confirm_delete.html b/expenses_manager/expenses/templates/categories/confirm_delete.html index 87b4140..98c5bf3 100644 --- a/expenses_manager/expenses/templates/categories/confirm_delete.html +++ b/expenses_manager/expenses/templates/categories/confirm_delete.html @@ -1,14 +1,12 @@ -{% extends "expenses/base.html" %} +{% extends "expenses/_confirm_delete.html" %} -{% block title %} - Categorías -{% endblock %} +{% block title %}Categorías{% endblock %} -{% block content %} -
¿Seguro que quieres eliminar la categoría {{ category.name }}?
+{% block delete_question %}¿Seguro que quieres eliminar la categoría {{ category.name }}?{% endblock %} +{% block delete_warnings %} {% if expense_count %}Esta categoría tiene {{ expense_count }} gasto{{ expense_count|pluralize }} asociado{{ expense_count|pluralize }} @@ -18,7 +16,7 @@ {% if children %}
Atención: también se eliminarán sus subcategorías: - {% for child in children %}{{child.name}}{% if not forloop.last %}, {% endif %}{% endfor %}. + {% for child in children %}{{ child.name }}{% if not forloop.last %}, {% endif %}{% endfor %}.
{% endif %} @@ -28,11 +26,6 @@ {% endif %} {% endif %} +{% endblock %} - -{% endblock %} \ No newline at end of file +{% block cancel_url %}{% url 'category_list' %}{% endblock %} diff --git a/expenses_manager/expenses/templates/categories/form.html b/expenses_manager/expenses/templates/categories/form.html index e45d9c1..61a27b6 100644 --- a/expenses_manager/expenses/templates/categories/form.html +++ b/expenses_manager/expenses/templates/categories/form.html @@ -10,7 +10,9 @@ {% endblock %} \ No newline at end of file diff --git a/expenses_manager/expenses/templates/categories/list.html b/expenses_manager/expenses/templates/categories/list.html index 24826ab..e587118 100644 --- a/expenses_manager/expenses/templates/categories/list.html +++ b/expenses_manager/expenses/templates/categories/list.html @@ -12,12 +12,13 @@{% block delete_question %}¿Seguro que quieres eliminar este elemento?{% endblock %}
+ + {% block delete_warnings %}{% endblock %} + + +{% endblock %} diff --git a/expenses_manager/expenses/templates/expenses/account_confirm_delete.html b/expenses_manager/expenses/templates/expenses/account_confirm_delete.html index 0d91fcb..e2ac093 100644 --- a/expenses_manager/expenses/templates/expenses/account_confirm_delete.html +++ b/expenses_manager/expenses/templates/expenses/account_confirm_delete.html @@ -1,18 +1,11 @@ -{% extends "expenses/base.html" %} +{% extends "expenses/_confirm_delete.html" %} + {% block title %}Eliminar{% endblock %} -{% block content %} -- ¿Seguro que quieres eliminar la cuenta - {{ account.name }} -
+{% block delete_question %}¿Seguro que quieres eliminar la cuenta {{ account.name }}?{% endblock %} - +{% block delete_button %}Sí, eliminar{% endblock %} -{% endblock %} \ No newline at end of file +{% block cancel_url %}{% url 'account_list' %}{% endblock %} diff --git a/expenses_manager/expenses/templates/expenses/account_form.html b/expenses_manager/expenses/templates/expenses/account_form.html index 4fbed41..e017818 100644 --- a/expenses_manager/expenses/templates/expenses/account_form.html +++ b/expenses_manager/expenses/templates/expenses/account_form.html @@ -14,8 +14,10 @@ {% csrf_token %} {{ form.as_p }} - - Cancelar +