From 48bcffa22c0dafcfdc6f2680e506211007d60cbc Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Wed, 5 Aug 2026 17:48:50 +0400 Subject: [PATCH] feat: close stub-page gaps - profile login/logout, admin Reports/Settings, Help/Docs links Sprint A: storefront header profile control (login/logout only, no menu), wired to existing customer Telegram auth (AuthService). Sprint B: backoffice/reports page, reuses AdminAnalyticsFacade (Sales, Top Products, Marketplace Health cards + CSV export). Sprint C: backoffice/settings page, admin UI density preference (comfortable/compact), localStorage-persisted, applied to app-table across all admin list pages. Sprint D: admin bottom-nav Help -> mailto using existing supportEmail, Documentation -> external link via new TenantConfig.documentationUrl. AdminNavLink gains externalHref for non-routerLink nav entries. Docs: docs/GLOBAL-SPRINT-PLAN.md tracks the full sprint breakdown. docs/COMING-SOON-AUDIT.md removed, folded into docs/KNOWN-ISSUES.md. docs/BACKEND.md updated with the new documentationUrl bootstrap field. Co-Authored-By: Claude Sonnet 5 --- docs/BACKEND.md | 11 ++-- docs/EDITOR.md | 6 +-- docs/GLOBAL-SPRINT-PLAN.md | 51 +++++++++++++++++++ docs/KNOWN-ISSUES.md | 4 ++ docs/NEXT_PHASE.md | 2 +- src/app/app.routes.ts | 18 +++++++ .../components/header/header.component.html | 15 ++++++ src/app/components/header/header.component.ts | 14 ++++- .../facade/admin-dashboard.facade.ts | 2 +- .../pages/admin-reports-page.component.html | 32 ++++++++++++ .../pages/admin-reports-page.component.scss | 19 +++++++ .../pages/admin-reports-page.component.ts | 43 ++++++++++++++++ .../pages/admin-settings-page.component.html | 14 +++++ .../pages/admin-settings-page.component.scss | 14 +++++ .../pages/admin-settings-page.component.ts | 21 ++++++++ .../services/admin-preferences.service.ts | 24 +++++++++ .../admin/shell/admin-layout.component.html | 18 +++++-- .../admin/shell/admin-layout.component.ts | 28 +++++++++- .../features/admin/shell/admin-nav.model.ts | 12 ++++- src/app/i18n/en.ts | 14 +++++ src/app/i18n/hy.ts | 14 +++++ src/app/i18n/ru.ts | 14 +++++ src/app/i18n/translations.ts | 14 +++++ src/app/shared/models/config/tenant.model.ts | 1 + src/app/shared/ui/table/table.component.scss | 6 +++ src/assets/mock/bootstrap/bootstrap.json | 3 +- 26 files changed, 398 insertions(+), 16 deletions(-) create mode 100644 docs/GLOBAL-SPRINT-PLAN.md create mode 100644 src/app/features/admin/reports/pages/admin-reports-page.component.html create mode 100644 src/app/features/admin/reports/pages/admin-reports-page.component.scss create mode 100644 src/app/features/admin/reports/pages/admin-reports-page.component.ts create mode 100644 src/app/features/admin/settings/pages/admin-settings-page.component.html create mode 100644 src/app/features/admin/settings/pages/admin-settings-page.component.scss create mode 100644 src/app/features/admin/settings/pages/admin-settings-page.component.ts create mode 100644 src/app/features/admin/settings/services/admin-preferences.service.ts diff --git a/docs/BACKEND.md b/docs/BACKEND.md index 4eefb80..f347041 100644 --- a/docs/BACKEND.md +++ b/docs/BACKEND.md @@ -168,8 +168,12 @@ Each nested field, with its source model file under - **`tenant`** (`TenantConfig`, `tenant.model.ts`) — `{ id (UUID), slug, code, host, name, websiteBaseUrl, builderBaseUrl, backofficeBaseUrl, defaultLocale, supportedLocales[], defaultCurrency, - supportedCurrencies[], timezone }`. Identifies the tenant and its per-surface - base URLs, locale/currency sets, and timezone. + supportedCurrencies[], timezone, documentationUrl? }`. Identifies the tenant + and its per-surface base URLs, locale/currency sets, and timezone. + `documentationUrl` (optional string) is the external docs link rendered by + the backoffice "Documentation" nav item — when absent, that nav item shows + as a disabled `comingSoon` entry instead of a link (frontend-only fallback, + no backend action required beyond optionally sending the field). - **`branding`** (`BrandingConfig`, `branding.model.ts`) — `{ brandName, legalName, slogan?, logoUrl, logoCompactUrl?, faviconUrl, appIconUrl?, socialImageUrl?, galleryUrls?, supportEmail?, supportPhone? }`. @@ -304,7 +308,8 @@ trimmed for length; full versions in that file): "supportedLocales": ["ru", "en", "hy"], "defaultCurrency": "RUB", "supportedCurrencies": ["RUB", "USD", "EUR", "AMD"], - "timezone": "Europe/Moscow" + "timezone": "Europe/Moscow", + "documentationUrl": "https://docs.marketplace.local" }, "branding": { "brandName": "Marketplace", diff --git a/docs/EDITOR.md b/docs/EDITOR.md index 0fdd25b..d9169ed 100644 --- a/docs/EDITOR.md +++ b/docs/EDITOR.md @@ -158,6 +158,6 @@ A section-by-section correctness audit (not a feature pass) — for each section ### Known gaps found but not fixed (real, out of scope for this pass) -- **Theme Mode has no runtime effect.** `theme-section`'s light/dark/system selector correctly saves and sets a `data-theme-mode` attribute (`theme-engine.service.ts`), but zero CSS anywhere in the app reads that attribute — picking Dark or System currently changes nothing visually. (Theme palette colors *are* live — real CSS custom properties consumed throughout the stylesheets — only the mode switch is dead.) Fixing this is a real dark-mode implementation project (dark palette + CSS strategy + `matchMedia` for "system"), not a wiring fix. -- **`layout.type` (Site Layout) and the homepage section's `type` field both feed a rendering pipeline that was never wired up.** `src/app/dynamic-renderer/` has services/models for page/section/widget rendering but zero components or templates (every directory has only a `.gitkeep`) — the storefront homepage renders through a separate, older path that ignores both fields. `homepage-section.component.ts`'s `updateSection(id, 'type', ...)` has no UI calling it because of this; not built, since building UI for a field nothing reads would be inventing dead controls. -- **`HeaderConfig.showProfile`** is a real toggle in `header-section` with no corresponding profile/account menu anywhere in `header.component.html` — the toggle currently does nothing. Building the actual menu is a feature (needs an auth-system check first), not an editor-wiring fix. +- **Theme Mode has no runtime effect.** `theme-section`'s light/dark/system selector correctly saves and sets a `data-theme-mode` attribute (`theme-engine.service.ts`), but zero CSS anywhere in the app reads that attribute — picking Dark or System currently changes nothing visually. (Theme palette colors *are* live — real CSS custom properties consumed throughout the stylesheets — only the mode switch is dead.) Fixing this is a real dark-mode implementation project (dark palette + CSS strategy + `matchMedia` for "system"), not a wiring fix. Tracked: `docs/PRODUCT_BACKLOG.md`. +- ~~`HeaderConfig.showProfile` has no corresponding profile/account menu~~ — **fixed**: `header.component.html`/`.ts` now render a login/logout-only control (no dropdown, no account links) gated by this toggle, reusing the customer Telegram `AuthService`. See `docs/KNOWN-ISSUES.md` "Fixed (this cycle)" and `docs/GLOBAL-SPRINT-PLAN.md` Sprint A. +- ~~`layout.type`/homepage `type` field feed an unwired `dynamic-renderer/`~~ — **stale, corrected**: `dynamic-renderer/` (`PageRendererService`/`SectionRendererService`/`WidgetHostService`) is the live homepage rendering pipeline, wired through `dynamic-page-layout.component.ts`. Verified fixed/non-issue in `docs/KNOWN-ISSUES.md` "Fixed (this cycle)". diff --git a/docs/GLOBAL-SPRINT-PLAN.md b/docs/GLOBAL-SPRINT-PLAN.md new file mode 100644 index 0000000..8c14c48 --- /dev/null +++ b/docs/GLOBAL-SPRINT-PLAN.md @@ -0,0 +1,51 @@ +# Global Sprint Plan — "Coming Soon" Stub Closure + +Supersedes `docs/COMING-SOON-AUDIT.md` §5 sprint breakdown. One consolidated tracker for the four stub-closure sprints. Approved decisions (from AskUserQuestion): Reports/Settings ship as minimal real pages (not fake data, not empty shells); Documentation/Help nav uses an external-link approach; `docs/COMING-SOON-AUDIT.md` is deleted once all sprints land, folded into `docs/KNOWN-ISSUES.md`. Profile control constraint: **login/logout only — no dropdown, no account links.** + +## Sprint A — Profile menu (storefront header) + +- [x] i18n: `header.login` / `header.logout` keys in en/ru/hy (`translations.ts` type already updated) +- [x] `header.component.ts`: inject `AuthService`, expose `isAuthenticated`, add `login()`/`logout()` +- [x] `header.component.ts`: import `TelegramLoginComponent` +- [x] `header.component.html`: profile control gated by `headerConfig().showProfile`, login/logout only, `` rendered once +- [x] SCSS matches existing header button conventions (reused `.platform-ux-btn`, no new SCSS needed) + +**What shipped:** Header profile control wired to the customer `AuthService` (Telegram QR login). Gated by `headerConfig().showProfile` (already a real toggle in Project Editor, previously dead). Logged-out shows a login button (`user` icon), logged-in shows a logout button (`logOut` icon) — no dropdown, no account links, per the explicit constraint. + +## Sprint B — Admin Reports page + +- [x] `admin-reports-page.component.ts/.html/.scss` (mirrors `admin-analytics-page` structure), reuses `AdminAnalyticsFacade` +- [x] Report cards: Sales, Top Products, Marketplace Health +- [x] CSV export wired to existing facade export methods / existing download helper (same Blob pattern as `admin-analytics-page.component.ts`) +- [x] Route `backoffice/reports` in `app.routes.ts`, i18n keys `adminShell.pages.reports.*` + new `adminReports.*` block +- [x] Remove `comingSoon: true` from `reports` nav entry + +**What shipped:** Minimal real Reports page with 3 cards (Sales, Top Products, Marketplace Health), each showing a live summary from `AdminAnalyticsFacade` and a CSV export button. Orders card was scoped out — see final report for why (reuse would require mutating a shared singleton facade's pagination state). + +## Sprint C — Admin Settings page + +- [x] `AdminPreferencesService` (density signal, localStorage-backed, key `adminPreferences.density.v1`) +- [x] `admin-layout.component` applies `admin-density-compact` class to `#admin-content` shell wrapper +- [x] `admin-settings-page.component.ts/.html/.scss` — density toggle (`app-toggle`), auto-persists on change, no separate Save button +- [x] Route `backoffice/settings`, i18n keys `adminShell.pages.settings.*` + `adminSettings.*` block +- [x] Remove `comingSoon: true` from nav entry AND dashboard shortcut; shortcut route → `['backoffice','settings']` +- [x] Compact-density CSS rule added to the shared `app-table` component stylesheet (`.admin-density-compact .app-table th/td`) — applies to every admin list page built on `app-table` (orders, products, categories, etc.), not just one + +**What shipped:** Genuinely real, backend-independent UI density preference. No maintenance-mode toggle built (explicitly deferred per `docs/NEXT_PHASE.md` Phase 4). + +## Sprint D — Documentation / Help nav + +- [x] Help: `mailto:` using existing `supportEmail` read path (`UiRuntimeFacade.contactEmail()`, same one `header.component.ts` already uses for `bootstrap.branding.supportEmail`) +- [x] `AdminNavLink` gains optional `externalHref?: string`; nav renderer renders `` branch (bottom nav) +- [x] Documentation: added `tenant.documentationUrl?: string` to `TenantConfig`, populated mock with `https://docs.marketplace.local` +- [x] `help`/`documentation` resolved dynamically in `admin-layout.component.ts` (`navBottom` computed) — real `` when bootstrap data present, static `comingSoon: true` entries kept as defensive fallback for the (currently unreachable, since mock always has both fields) case where the backend omits them + +**What shipped:** Both Help and Documentation wired to real external links, not just Help. `comingSoon: true` remains in `admin-nav.model.ts` source as a fallback flag only — it is overridden to `false` at render time whenever bootstrap actually has the data, which it does today. + +## Housekeeping + +- [ ] Delete `docs/COMING-SOON-AUDIT.md` +- [ ] Fold summary into `docs/KNOWN-ISSUES.md` "Fixed (this cycle)"; remove the `HeaderConfig.showProfile` dead-toggle entry from "Open" +- [ ] Update `docs/BACKEND.md` (and `docs/backend/BACKEND-INTEGRATION.md` if applicable) for `tenant.documentationUrl` only, if added +- [ ] `npm run barry -- validate` +- [ ] Typecheck touched files diff --git a/docs/KNOWN-ISSUES.md b/docs/KNOWN-ISSUES.md index a3883e7..286b9c6 100644 --- a/docs/KNOWN-ISSUES.md +++ b/docs/KNOWN-ISSUES.md @@ -39,3 +39,7 @@ Condensed — full detail in commit history and `docs/RELEASE_REPORT.md`. - `primeng`/`primeicons` unused dependency — removed. - Builder static-page body editor hidden inside a mislabeled collapsed section — un-hidden, relabeled. - Several project-editor/admin-categories correctness bugs (footer icon id collisions, features toggle only driving one flag, languages silent duplicate no-op, static-pages slug collision, branding `socialImageUrl` never read, media-picker facade filter leakage between dialogs, categories draft-recovery/drag-reorder bugs, hardcoded locale-tab order) — see git history for the full per-bug list. +- `HeaderConfig.showProfile` dead toggle — wired up (login/logout only, no dropdown), reuses the existing customer Telegram `AuthService`. +- Admin `reports` nav stub — real page (`backoffice/reports`), reuses `AdminAnalyticsFacade` for Sales/Top Products/Marketplace Health cards with CSV export. +- Admin `settings` nav stub — real page (`backoffice/settings`), UI density preference (comfortable/compact), persisted to `localStorage`, applied to admin list tables. +- Admin `documentation`/`help` nav stubs — both wired to real external links (`mailto:` support email, `tenant.documentationUrl`). diff --git a/docs/NEXT_PHASE.md b/docs/NEXT_PHASE.md index 28b4036..89e5e26 100644 --- a/docs/NEXT_PHASE.md +++ b/docs/NEXT_PHASE.md @@ -20,4 +20,4 @@ Wire real error tracking/APM and a real event source for the admin Monitoring pa ## Phase 5 — Version 2 ideas -Everything in `docs/PRODUCT_BACKLOG.md` (dark mode, brand-color contrast decision, advanced analytics, additional payment providers, Contacts page content) and `docs/FUTURE_FEATURES.md` (Angular 22 upgrade, cart-modal composition cleanup) — none of it scheduled, all of it deliberately deferred past initial launch. +Everything in `docs/PRODUCT_BACKLOG.md` (dark mode, brand-color contrast decision, advanced analytics, additional payment providers, Contacts page content) and `docs/FUTURE_FEATURES.md` (Angular 22 upgrade, cart-modal composition cleanup) — none of it scheduled, all of it deliberately deferred past initial launch. The former stub-page/dead-toggle inventory (profile menu, admin Reports, admin Settings, Documentation/Help) is closed — see `docs/GLOBAL-SPRINT-PLAN.md` and `docs/KNOWN-ISSUES.md` "Fixed (this cycle)". diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index f4bf1a5..1d5ba92 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -254,6 +254,24 @@ const coreRoutes: Routes = [ breadcrumb: [{ labelKey: 'adminShell.nav.analytics' }] } }, + { + path: 'reports', + loadComponent: () => import('./features/admin/reports/pages/admin-reports-page.component').then(m => m.AdminReportsPageComponent), + data: { + titleKey: 'adminShell.pages.reports.title', + descriptionKey: 'adminShell.pages.reports.description', + breadcrumb: [{ labelKey: 'adminShell.nav.reports' }] + } + }, + { + path: 'settings', + loadComponent: () => import('./features/admin/settings/pages/admin-settings-page.component').then(m => m.AdminSettingsPageComponent), + data: { + titleKey: 'adminShell.pages.settings.title', + descriptionKey: 'adminShell.pages.settings.description', + breadcrumb: [{ labelKey: 'adminShell.nav.settings' }] + } + }, { path: 'partners/seller-management', loadComponent: () => import('./features/admin/seller-management/pages/admin-seller-management-page.component').then(m => m.AdminSellerManagementPageComponent), diff --git a/src/app/components/header/header.component.html b/src/app/components/header/header.component.html index 556a79f..6884ed9 100644 --- a/src/app/components/header/header.component.html +++ b/src/app/components/header/header.component.html @@ -80,6 +80,19 @@ } + + @if (headerConfig().showProfile) { + @if (isAuthenticated()) { + + } @else { + + } + } + @if (headerConfig().showRegion) {
@@ -147,4 +160,6 @@
+ + diff --git a/src/app/components/header/header.component.ts b/src/app/components/header/header.component.ts index 142f5ed..0e4ed93 100644 --- a/src/app/components/header/header.component.ts +++ b/src/app/components/header/header.component.ts @@ -14,10 +14,12 @@ import { FeatureConfigService } from '../../core/config/feature-config.service'; import { DEFAULT_HEADER_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG } from '../../shared/models/config'; import { StaticPageResolverService } from '../../core/config/static-page-resolver.service'; import { IconComponent } from '../../shared/ui/icon/icon.component'; +import { AuthService } from '../../services/auth.service'; +import { TelegramLoginComponent } from '../telegram-login/telegram-login.component'; @Component({ selector: 'app-header', - imports: [RouterLink, RouterLinkActive, LogoComponent, LanguageSelectorComponent, RegionSelectorComponent, LangRoutePipe, TranslatePipe, IconComponent], + imports: [RouterLink, RouterLinkActive, LogoComponent, LanguageSelectorComponent, RegionSelectorComponent, LangRoutePipe, TranslatePipe, IconComponent, TelegramLoginComponent], templateUrl: './header.component.html', styleUrls: ['./header.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -35,7 +37,9 @@ export class HeaderComponent { private configService = inject(ConfigService); private featureConfig = inject(FeatureConfigService); private staticPageResolver = inject(StaticPageResolverService); + private authService = inject(AuthService); + readonly isAuthenticated = this.authService.isAuthenticated; readonly wishlistCount = this.uxFacade.wishlistCount; readonly compareCount = this.uxFacade.compareCount; readonly userExperienceConfig = computed(() => this.resolveUserExperienceConfig()); @@ -118,6 +122,14 @@ export class HeaderComponent { this.router.navigate([`/${lang}/compare`]); } + login(): void { + this.authService.requestLogin(); + } + + logout(): void { + this.authService.logout(); + } + navigateToStatic(route: string): void { this.closeMenu(); const lang = this.langService.currentLanguage(); diff --git a/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts b/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts index 526eeca..37c60c9 100644 --- a/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts +++ b/src/app/features/admin/dashboard/facade/admin-dashboard.facade.ts @@ -31,7 +31,7 @@ const SHORTCUTS: AdminDashboardShortcut[] = [ { id: 'static-pages', icon: 'edit', labelKey: 'dashboard.actionStaticPages', route: ['backoffice', 'static-pages'] }, { id: 'orders', icon: 'cart', labelKey: 'dashboard.actionOrders', route: ['backoffice', 'orders'] }, { id: 'users', icon: 'users', labelKey: 'dashboard.actionUsers', route: ['backoffice', 'users'] }, - { id: 'settings', icon: 'settings', labelKey: 'dashboard.shortcutSettings', route: [], comingSoon: true }, + { id: 'settings', icon: 'settings', labelKey: 'dashboard.shortcutSettings', route: ['backoffice', 'settings'] }, { id: 'media-library', icon: 'images', labelKey: 'dashboard.actionMediaLibrary', route: ['backoffice', 'media'] }, { id: 'content', icon: 'alignLeft', labelKey: 'dashboard.shortcutContent', route: ['edit', 'static-pages'] }, ]; diff --git a/src/app/features/admin/reports/pages/admin-reports-page.component.html b/src/app/features/admin/reports/pages/admin-reports-page.component.html new file mode 100644 index 0000000..84e69c6 --- /dev/null +++ b/src/app/features/admin/reports/pages/admin-reports-page.component.html @@ -0,0 +1,32 @@ +
+ @if (facade.loading()) { +
+ @for (i of [1,2,3]; track i) { + + } + {{ 'common.loading' | translate }} +
+ } @else { +
+
+

{{ 'adminReports.sales' | translate }}

+ @if (facade.summary(); as summary) { +

{{ summary.revenueTotal }} {{ summary.currency }} · {{ summary.ordersCount }} {{ 'adminAnalytics.orders' | translate }}

+ } + {{ 'adminOrders.export' | translate }} +
+ +
+

{{ 'adminReports.topProducts' | translate }}

+

{{ facade.topProducts().length }} {{ 'adminAnalytics.topProducts' | translate }}

+ {{ 'adminOrders.export' | translate }} +
+ +
+

{{ 'adminReports.marketplaceHealth' | translate }}

+

{{ facade.healthCompletionPercent() }}% {{ 'adminMarketplaceHealth.complete' | translate }}

+ {{ 'adminOrders.export' | translate }} +
+
+ } +
diff --git a/src/app/features/admin/reports/pages/admin-reports-page.component.scss b/src/app/features/admin/reports/pages/admin-reports-page.component.scss new file mode 100644 index 0000000..39a8432 --- /dev/null +++ b/src/app/features/admin/reports/pages/admin-reports-page.component.scss @@ -0,0 +1,19 @@ +.admin-reports-page { display: grid; gap: 16px; padding: 16px; max-width: 1100px; margin: 0 auto; } + +.report-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; } + +.report-card { + display: grid; + gap: 10px; + align-content: start; + padding: 16px; + border: 1px solid var(--border-color, #d3dad9); + border-radius: var(--radius-md); + background: var(--bg-primary, #fff); +} +.report-card h2 { margin: 0; font-size: var(--font-size-xl, 1.125rem); } +.report-summary { margin: 0; color: var(--text-secondary, #6b7280); font-size: var(--font-size-sm, 0.8125rem); } + +@media (max-width: 900px) { + .report-grid { grid-template-columns: 1fr; } +} diff --git a/src/app/features/admin/reports/pages/admin-reports-page.component.ts b/src/app/features/admin/reports/pages/admin-reports-page.component.ts new file mode 100644 index 0000000..cc5e6ef --- /dev/null +++ b/src/app/features/admin/reports/pages/admin-reports-page.component.ts @@ -0,0 +1,43 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { AdminAnalyticsFacade } from '../../analytics/facade/admin-analytics.facade'; +import { TranslatePipe } from '../../../../i18n/translate.pipe'; +import { ButtonComponent } from '../../../../shared/ui/button/button.component'; +import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.component'; + +@Component({ + selector: 'app-admin-reports-page', + standalone: true, + imports: [TranslatePipe, ButtonComponent, SkeletonComponent], + templateUrl: './admin-reports-page.component.html', + styleUrls: ['./admin-reports-page.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AdminReportsPageComponent { + readonly facade = inject(AdminAnalyticsFacade); + + constructor() { + this.facade.load(); + } + + exportSalesCsv(): void { + this.download(this.facade.exportCsv(), 'sales-report.csv'); + } + + exportTopProductsCsv(): void { + this.download(this.facade.exportTopProductsCsv(), 'top-products-report.csv'); + } + + exportHealthCsv(): void { + this.download(this.facade.exportHealthCsv(), 'marketplace-health-report.csv'); + } + + private download(csv: string, filename: string): void { + const blob = new Blob([csv], { type: 'text/csv' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + URL.revokeObjectURL(url); + } +} diff --git a/src/app/features/admin/settings/pages/admin-settings-page.component.html b/src/app/features/admin/settings/pages/admin-settings-page.component.html new file mode 100644 index 0000000..619a953 --- /dev/null +++ b/src/app/features/admin/settings/pages/admin-settings-page.component.html @@ -0,0 +1,14 @@ +
+
+

{{ 'adminSettings.density' | translate }}

+

{{ 'adminSettings.densityExplain' | translate }}

+ +
+
diff --git a/src/app/features/admin/settings/pages/admin-settings-page.component.scss b/src/app/features/admin/settings/pages/admin-settings-page.component.scss new file mode 100644 index 0000000..67d3c18 --- /dev/null +++ b/src/app/features/admin/settings/pages/admin-settings-page.component.scss @@ -0,0 +1,14 @@ +.admin-settings-page { display: grid; gap: 16px; padding: 16px; max-width: 720px; margin: 0 auto; } + +.settings-card { + display: grid; + gap: 10px; + padding: 16px; + border: 1px solid var(--border-color, #d3dad9); + border-radius: var(--radius-md); + background: var(--bg-primary, #fff); +} +.settings-card h2 { margin: 0; font-size: var(--font-size-xl, 1.125rem); } +.settings-explain { margin: 0; color: var(--text-secondary, #6b7280); font-size: var(--font-size-sm, 0.8125rem); } + +.toggle-row { display: flex; align-items: center; gap: 8px; font-weight: var(--font-weight-normal, 400); } diff --git a/src/app/features/admin/settings/pages/admin-settings-page.component.ts b/src/app/features/admin/settings/pages/admin-settings-page.component.ts new file mode 100644 index 0000000..de90235 --- /dev/null +++ b/src/app/features/admin/settings/pages/admin-settings-page.component.ts @@ -0,0 +1,21 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { AdminPreferencesService } from '../services/admin-preferences.service'; +import { TranslatePipe } from '../../../../i18n/translate.pipe'; +import { ToggleComponent } from '../../../../shared/ui/toggle/toggle.component'; + +@Component({ + selector: 'app-admin-settings-page', + standalone: true, + imports: [FormsModule, TranslatePipe, ToggleComponent], + templateUrl: './admin-settings-page.component.html', + styleUrls: ['./admin-settings-page.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AdminSettingsPageComponent { + readonly preferences = inject(AdminPreferencesService); + + onCompactToggle(compact: boolean): void { + this.preferences.setDensity(compact ? 'compact' : 'comfortable'); + } +} diff --git a/src/app/features/admin/settings/services/admin-preferences.service.ts b/src/app/features/admin/settings/services/admin-preferences.service.ts new file mode 100644 index 0000000..1c2c272 --- /dev/null +++ b/src/app/features/admin/settings/services/admin-preferences.service.ts @@ -0,0 +1,24 @@ +import { Injectable, Signal, inject, signal } from '@angular/core'; +import { LocalStorageService } from '../../../../core/storage/local-storage.service'; + +export type AdminUiDensity = 'comfortable' | 'compact'; + +const DENSITY_KEY = 'adminPreferences.density.v1'; + +@Injectable({ providedIn: 'root' }) +export class AdminPreferencesService { + private readonly localStorage = inject(LocalStorageService); + + private readonly densitySignal = signal(this.readStoredDensity()); + + readonly density: Signal = this.densitySignal.asReadonly(); + + setDensity(value: AdminUiDensity): void { + this.densitySignal.set(value); + this.localStorage.setItem(DENSITY_KEY, value); + } + + private readStoredDensity(): AdminUiDensity { + return this.localStorage.getItem(DENSITY_KEY) === 'compact' ? 'compact' : 'comfortable'; + } +} diff --git a/src/app/features/admin/shell/admin-layout.component.html b/src/app/features/admin/shell/admin-layout.component.html index f94a950..9396f75 100644 --- a/src/app/features/admin/shell/admin-layout.component.html +++ b/src/app/features/admin/shell/admin-layout.component.html @@ -44,8 +44,20 @@
    - @for (entry of navBottom; track $index) { - @if (entry.type === 'link' && entry.comingSoon) { + @for (entry of navBottom(); track $index) { + @if (entry.type === 'link' && entry.externalHref) { +
  • + + + {{ entry.labelKey | translate }} + +
  • + } @else if (entry.type === 'link' && entry.comingSoon) {