2026-01-18 18:57:06 +04:00
|
|
|
import { Routes } from '@angular/router';
|
2026-02-26 22:23:08 +04:00
|
|
|
import { languageGuard } from './guards/language.guard';
|
2026-07-13 15:21:39 +04:00
|
|
|
import { projectEditorDirtyGuard } from './features/project-editor/guards/project-editor-dirty.guard';
|
2026-07-14 12:21:33 +04:00
|
|
|
import { adminAuthGuard } from './core/admin-auth/admin-auth.guard';
|
feat(admin): complete category management
Sprint 20. Adds features/admin/categories/ (model, gateway interface +
local gateway, facade, list/editor pages), mirroring the admin/products
container/facade/service split.
- indented hierarchy view + native HTML5 drag-and-drop reorder
- visibility toggle, item counter, empty state, include-deleted filter
- editor: slug uniqueness validation, translations, SEO fields, breadcrumb
preview, image via existing MediaPickerComponent
- soft delete/restore, blocked when a category has children or items
- draft/publish status + localStorage draft recovery (mirrors Project
Editor autosave) + CanDeactivate unsaved-changes guard
- wired into app.routes.ts (replaces the categories coming-soon placeholder)
- docs/ADMIN.md + docs/BACKEND.md updated with the new gap detail
Not yet done: admin/products' category dropdown still reads from its own
AdminProductsGateway.loadCategories() rather than this gateway (Sprint 21).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 09:34:13 +04:00
|
|
|
import { adminCategoryDirtyGuard } from './features/admin/categories/guards/admin-category-dirty.guard';
|
2026-07-10 13:34:25 +04:00
|
|
|
import { environment } from '../environments/environment';
|
2026-01-18 18:57:06 +04:00
|
|
|
|
2026-01-23 00:00:08 +04:00
|
|
|
// Core routes (same across all brands)
|
|
|
|
|
const coreRoutes: Routes = [
|
2026-01-18 18:57:06 +04:00
|
|
|
{
|
|
|
|
|
path: '',
|
|
|
|
|
loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent)
|
|
|
|
|
},
|
2026-07-05 01:36:21 +04:00
|
|
|
{
|
|
|
|
|
path: 'catalog',
|
|
|
|
|
loadComponent: () => import('./features/website/catalog/containers/catalog-container.component').then(m => m.CatalogContainerComponent)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'catalog/:id',
|
|
|
|
|
loadComponent: () => import('./features/website/catalog/containers/catalog-container.component').then(m => m.CatalogContainerComponent)
|
|
|
|
|
},
|
2026-01-18 18:57:06 +04:00
|
|
|
{
|
|
|
|
|
path: 'category/:id',
|
2026-07-05 01:36:21 +04:00
|
|
|
redirectTo: 'catalog/:id',
|
|
|
|
|
pathMatch: 'full'
|
2026-01-18 18:57:06 +04:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'category/:id/items',
|
2026-07-05 01:36:21 +04:00
|
|
|
redirectTo: 'catalog/:id',
|
|
|
|
|
pathMatch: 'full'
|
2026-01-18 18:57:06 +04:00
|
|
|
},
|
2026-07-05 01:43:56 +04:00
|
|
|
{
|
|
|
|
|
path: 'product/:id',
|
|
|
|
|
loadComponent: () => import('./features/website/product/containers/product-details-container.component').then(m => m.ProductDetailsContainerComponent)
|
|
|
|
|
},
|
2026-01-18 18:57:06 +04:00
|
|
|
{
|
|
|
|
|
path: 'item/:id',
|
2026-07-05 01:43:56 +04:00
|
|
|
redirectTo: 'product/:id',
|
|
|
|
|
pathMatch: 'full'
|
2026-01-18 18:57:06 +04:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'search',
|
2026-07-09 00:55:50 +04:00
|
|
|
loadComponent: () => import('./features/website/catalog/containers/catalog-container.component').then(m => m.CatalogContainerComponent)
|
2026-01-18 18:57:06 +04:00
|
|
|
},
|
2026-07-10 13:43:53 +04:00
|
|
|
{
|
2026-07-13 07:55:43 +04:00
|
|
|
path: 'edit/:section',
|
2026-07-13 15:21:39 +04:00
|
|
|
loadComponent: () => import('./features/project-editor/pages/project-editor-page.component').then(m => m.ProjectEditorPageComponent),
|
|
|
|
|
canDeactivate: [projectEditorDirtyGuard]
|
2026-07-10 13:43:53 +04:00
|
|
|
},
|
2026-07-14 12:21:33 +04:00
|
|
|
{
|
|
|
|
|
path: 'backoffice',
|
|
|
|
|
canActivate: [adminAuthGuard],
|
|
|
|
|
children: [
|
|
|
|
|
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
|
|
|
|
{
|
|
|
|
|
path: 'dashboard',
|
|
|
|
|
loadComponent: () => import('./features/admin/dashboard/pages/admin-dashboard-page.component').then(m => m.AdminDashboardPageComponent)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'products',
|
|
|
|
|
loadComponent: () => import('./features/admin/products/pages/admin-products-list-page.component').then(m => m.AdminProductsListPageComponent)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'products/create',
|
|
|
|
|
loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'products/:id/edit',
|
|
|
|
|
loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'products/:id/duplicate',
|
|
|
|
|
loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'categories',
|
feat(admin): complete category management
Sprint 20. Adds features/admin/categories/ (model, gateway interface +
local gateway, facade, list/editor pages), mirroring the admin/products
container/facade/service split.
- indented hierarchy view + native HTML5 drag-and-drop reorder
- visibility toggle, item counter, empty state, include-deleted filter
- editor: slug uniqueness validation, translations, SEO fields, breadcrumb
preview, image via existing MediaPickerComponent
- soft delete/restore, blocked when a category has children or items
- draft/publish status + localStorage draft recovery (mirrors Project
Editor autosave) + CanDeactivate unsaved-changes guard
- wired into app.routes.ts (replaces the categories coming-soon placeholder)
- docs/ADMIN.md + docs/BACKEND.md updated with the new gap detail
Not yet done: admin/products' category dropdown still reads from its own
AdminProductsGateway.loadCategories() rather than this gateway (Sprint 21).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 09:34:13 +04:00
|
|
|
loadComponent: () => import('./features/admin/categories/pages/admin-categories-list-page.component').then(m => m.AdminCategoriesListPageComponent)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'categories/create',
|
|
|
|
|
loadComponent: () => import('./features/admin/categories/pages/admin-category-editor-page.component').then(m => m.AdminCategoryEditorPageComponent),
|
|
|
|
|
canDeactivate: [adminCategoryDirtyGuard]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'categories/:id/edit',
|
|
|
|
|
loadComponent: () => import('./features/admin/categories/pages/admin-category-editor-page.component').then(m => m.AdminCategoryEditorPageComponent),
|
|
|
|
|
canDeactivate: [adminCategoryDirtyGuard]
|
2026-07-14 12:21:33 +04:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'static-pages',
|
|
|
|
|
loadComponent: () => import('./features/backoffice/shared/backoffice-coming-soon-page.component').then(m => m.BackofficeComingSoonPageComponent),
|
|
|
|
|
data: { titleKey: 'dashboard.actionStaticPages' }
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'transactions',
|
2026-07-15 10:57:29 +04:00
|
|
|
loadComponent: () => import('./features/admin/transactions/pages/admin-transactions-list-page.component').then(m => m.AdminTransactionsListPageComponent)
|
2026-07-14 12:21:33 +04:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'orders',
|
feat(admin): order management
Sprint 23.
New features/admin/orders/ module, same container/facade/service split as
admin/products and admin/categories.
- AdminOrder model + AdminOrdersLocalGateway seeding 24 deterministic
synthetic orders (no real order data source exists anywhere in this
repo - explicitly a placeholder, not a mock of production volume)
- list: search, status filter, pagination, CSV export (client-side Blob
download)
- detail: customer/payment/shipping, itemized total, status timeline,
change-status dropdown, refund request + cancel (window.confirm-gated),
separate customer-facing vs internal notes, print invoice via
window.print() with @media print hiding non-invoice chrome
- wired into /:lang/backoffice/orders(/:id), replacing the coming-soon
placeholder
docs/ADMIN.md + docs/BACKEND.md updated; dashboard's Orders/Revenue cards
(Sprint 19) remain intentionally un-wired to this mock and still render
pending-backend.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 10:50:59 +04:00
|
|
|
loadComponent: () => import('./features/admin/orders/pages/admin-orders-list-page.component').then(m => m.AdminOrdersListPageComponent)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'orders/:id',
|
|
|
|
|
loadComponent: () => import('./features/admin/orders/pages/admin-order-detail-page.component').then(m => m.AdminOrderDetailPageComponent)
|
2026-07-14 12:21:33 +04:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'media',
|
feat(media): add Media Manager UI (grid, upload, delete)
MediaLibraryPageComponent replaces the coming-soon placeholder at
/backoffice/media. Built entirely on Sprint 6 Design System primitives
(app-card, app-button, app-input, app-empty-state, app-dialog, app-pagination,
app-skeleton) and Sprint 4 Task 2's MediaRepository/MockMediaRepository.
- Grid view with per-tile filename/size and delete action
- Hidden native file input triggered by an app-button, uploads via
MediaLibraryFacade -> MediaRepository.upload()
- Delete requires confirmation through app-dialog (destructive action)
- Search + pagination wired to MockMediaRepository's list() params
- Loading state shows app-skeleton tiles; empty state shows app-empty-state
- New mediaLibrary.* translation namespace across en/ru/hy
Verified in browser (via a temporary unguarded route, reverted before
commit - /backoffice/media itself requires Telegram QR admin auth not
available in this session): empty state renders correctly with translated
copy, search input and upload button present, no console errors.
2026-07-15 07:14:16 +04:00
|
|
|
loadComponent: () => import('./features/backoffice/media/media-library-page.component').then(m => m.MediaLibraryPageComponent),
|
2026-07-14 12:21:33 +04:00
|
|
|
data: { titleKey: 'dashboard.actionMediaLibrary' }
|
|
|
|
|
},
|
feat(admin): users and permissions
Sprint 25.
New features/admin/users/ module, net-new /:lang/backoffice/users route +
Dashboard Quick Action.
- users: name, Telegram username, scope (marketplace vs office admin),
role (inline change), status (active/invited/suspended), last login
- 4 built-in roles (owner/admin/editor/viewer) with flat permission lists
- invitations: email + role + scope form, pending list + revoke (no email
actually sends - local record only)
- passwordless login confirmed already real (AdminAuthService Telegram QR,
docs/BACKEND.md item 1) - linked, not reimplemented
- per-user mock session list (device/IP/last-active, revoke) - flagged as
mock since the real AdminAuthService only ever tracks the current
browser's session
- per-user audit log dialog (role/status changes), same pattern as
Sprint 24's per-transaction audit, intentionally separate from the
system-wide log planned for Sprint 26
docs/ADMIN.md + docs/BACKEND.md (new item 14) updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:05:21 +04:00
|
|
|
{
|
|
|
|
|
path: 'users',
|
|
|
|
|
loadComponent: () => import('./features/admin/users/pages/admin-users-page.component').then(m => m.AdminUsersPageComponent)
|
|
|
|
|
},
|
2026-07-15 11:12:47 +04:00
|
|
|
{
|
|
|
|
|
path: 'monitoring',
|
|
|
|
|
loadComponent: () => import('./features/admin/monitoring/pages/admin-monitoring-page.component').then(m => m.AdminMonitoringPageComponent)
|
|
|
|
|
},
|
2026-07-14 12:21:33 +04:00
|
|
|
{ path: '**', redirectTo: 'dashboard' }
|
|
|
|
|
]
|
|
|
|
|
},
|
2026-07-13 07:55:43 +04:00
|
|
|
{
|
|
|
|
|
path: 'edit',
|
|
|
|
|
redirectTo: 'edit/general',
|
|
|
|
|
pathMatch: 'full'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'builder',
|
|
|
|
|
redirectTo: 'edit/general',
|
|
|
|
|
pathMatch: 'full'
|
|
|
|
|
},
|
2026-07-10 13:43:53 +04:00
|
|
|
{
|
|
|
|
|
path: 'project-editor',
|
2026-07-13 07:55:43 +04:00
|
|
|
redirectTo: 'edit/general',
|
2026-07-10 13:43:53 +04:00
|
|
|
pathMatch: 'full'
|
|
|
|
|
},
|
2026-07-09 01:13:54 +04:00
|
|
|
{
|
|
|
|
|
path: 'wishlist',
|
|
|
|
|
loadComponent: () => import('./features/website/user-experience/wishlist/containers/wishlist-page.component').then(m => m.WishlistPageComponent)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'compare',
|
|
|
|
|
loadComponent: () => import('./features/website/user-experience/compare/containers/compare-page.component').then(m => m.ComparePageComponent)
|
|
|
|
|
},
|
2026-01-18 18:57:06 +04:00
|
|
|
{
|
|
|
|
|
path: 'cart',
|
|
|
|
|
loadComponent: () => import('./pages/cart/cart.component').then(m => m.CartComponent)
|
2026-07-05 03:37:35 +04:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'page/:key',
|
|
|
|
|
loadComponent: () => import('./pages/static-page/static-page.component').then(m => m.StaticPageComponent)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: ':staticPath',
|
|
|
|
|
loadComponent: () => import('./pages/static-page/static-page.component').then(m => m.StaticPageComponent)
|
2026-01-23 00:00:08 +04:00
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
2026-07-05 00:51:29 +04:00
|
|
|
// TODO(CMS): Resolve informational/legal pages from backend content configuration here.
|
|
|
|
|
// Disabled hardcoded pages: about, contacts, faq, delivery, guarantee,
|
|
|
|
|
// company-details, payment-terms, return-policy, public-offer, privacy-policy.
|
|
|
|
|
const cmsContentRoutes: Routes = [];
|
2026-07-03 01:55:23 +04:00
|
|
|
|
2026-07-05 01:43:56 +04:00
|
|
|
// All routes sit under a :lang prefix (e.g. /ru/cart, /en/product/5)
|
2026-01-23 00:00:08 +04:00
|
|
|
export const routes: Routes = [
|
2026-07-10 13:34:25 +04:00
|
|
|
...(environment.production ? [] : [{
|
|
|
|
|
path: '__diagnostics',
|
|
|
|
|
loadComponent: () => import('./features/diagnostics/components/diagnostics-page.component').then(m => m.DiagnosticsPageComponent)
|
|
|
|
|
}]),
|
2026-01-18 18:57:06 +04:00
|
|
|
{
|
2026-02-26 22:23:08 +04:00
|
|
|
path: ':lang',
|
|
|
|
|
canActivate: [languageGuard],
|
|
|
|
|
children: [
|
|
|
|
|
...coreRoutes,
|
2026-07-05 00:51:29 +04:00
|
|
|
...cmsContentRoutes,
|
2026-02-26 22:23:08 +04:00
|
|
|
{ path: '**', redirectTo: '' }
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
// URLs without a language prefix → redirect to default language
|
|
|
|
|
{ path: '**', redirectTo: 'ru' }
|
2026-01-23 00:00:08 +04:00
|
|
|
];
|