feat: Implement product categories, stores, suppliers, and users management features

- Added ProductCategoriesService for handling product category API interactions.
- Created components for listing and viewing product categories.
- Implemented UI for managing product categories with a data list component.
- Developed StoresService for managing store-related API calls.
- Created components for listing and viewing stores with appropriate UI.
- Added SuppliersService for handling supplier API interactions.
- Implemented components for managing suppliers with a data list component.
- Developed UsersService for managing user-related API calls.
- Created components for listing and viewing users with appropriate UI.
- Enhanced not found page with improved layout and navigation options.
This commit is contained in:
2025-12-04 23:34:00 +03:30
parent 07fec02ea1
commit 3bc1202c77
154 changed files with 3149 additions and 1820 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

+21
View File
@@ -0,0 +1,21 @@
{
"name": "صندوق فروشگاهی",
"short_name": "صندوق فروشگاهی",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+19 -7
View File
@@ -1,9 +1,15 @@
import { CUSTOMERS_ROUTES } from '@/modules/customers/constants';
import { INVENTORIES_ROUTES } from '@/modules/inventories/constants';
import { PRODUCT_BRANDS_ROUTES } from '@/modules/productBrands/constants';
import { PRODUCT_CATEGORIES_ROUTES } from '@/modules/productCategories/constants';
import { STORES_ROUTES } from '@/modules/stores/constants';
import { SUPPLIERS_ROUTES } from '@/modules/suppliers/constants';
import { USERS_ROUTES } from '@/modules/users/constants';
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { AppLayout } from './app/layout/component/app.layout'; import { AppLayout } from './app/layout/component/app.layout.component';
import { Dashboard } from './app/pages/dashboard/dashboard'; import { Dashboard } from './app/pages/dashboard/dashboard';
import { Documentation } from './app/pages/documentation/documentation'; import { Documentation } from './app/pages/documentation/documentation';
import { Landing } from './app/pages/landing/landing'; import { Notfound } from './app/pages/notfound/notfound.component';
import { Notfound } from './app/pages/notfound/notfound';
export const appRoutes: Routes = [ export const appRoutes: Routes = [
{ {
@@ -11,13 +17,19 @@ export const appRoutes: Routes = [
component: AppLayout, component: AppLayout,
children: [ children: [
{ path: '', component: Dashboard }, { path: '', component: Dashboard },
...USERS_ROUTES,
...STORES_ROUTES,
...SUPPLIERS_ROUTES,
...PRODUCT_BRANDS_ROUTES,
...PRODUCT_CATEGORIES_ROUTES,
...CUSTOMERS_ROUTES,
...INVENTORIES_ROUTES,
{ path: 'uikit', loadChildren: () => import('./app/pages/uikit/uikit.routes') }, { path: 'uikit', loadChildren: () => import('./app/pages/uikit/uikit.routes') },
{ path: 'documentation', component: Documentation }, { path: 'documentation', component: Documentation },
{ path: 'pages', loadChildren: () => import('./app/pages/pages.routes') } { path: 'pages', loadChildren: () => import('./app/pages/pages.routes') },
] ],
}, },
{ path: 'landing', component: Landing },
{ path: 'notfound', component: Notfound }, { path: 'notfound', component: Notfound },
{ path: 'auth', loadChildren: () => import('./app/pages/auth/auth.routes') }, { path: 'auth', loadChildren: () => import('./app/pages/auth/auth.routes') },
{ path: '**', redirectTo: '/notfound' } { path: '**', redirectTo: '/notfound' },
]; ];
+1
View File
@@ -1,2 +1,3 @@
export * from './defaultData.const'; export * from './defaultData.const';
export * from './menuItems.const';
export * from './roles.const'; export * from './roles.const';
+71
View File
@@ -0,0 +1,71 @@
import { MenuItem } from 'primeng/api';
export const MENU_ITEMS = [
{
items: [
{
label: 'داشبورد',
icon: 'pi pi-fw pi-home',
routerLink: ['/dashboard'],
},
],
},
{
label: 'موجودی‌ها',
icon: 'pi pi-fw pi-box',
items: [
{
label: 'لیست موجودی‌ها',
icon: 'pi pi-fw pi-list',
routerLink: ['/inventories'],
},
],
},
{
label: 'محصولات',
icon: 'pi pi-fw pi-tags',
items: [
{
label: 'لیست محصولات',
icon: 'pi pi-fw pi-list',
routerLink: ['/products'],
},
{
label: 'برندهای محصول',
icon: 'pi pi-fw pi-tags',
routerLink: ['/product-brands'],
},
{
label: 'دسته‌بندی‌های محصول',
icon: 'pi pi-fw pi-list',
routerLink: ['/product-categories'],
},
],
},
{
label: 'تامین‌کنندگان',
icon: 'pi pi-fw pi-users',
items: [
{
label: 'لیست تامین‌کنندگان',
icon: 'pi pi-fw pi-list',
routerLink: ['/suppliers'],
},
],
},
{
label: 'مدیریت سیستم',
icon: 'pi pi-fw pi-cog',
items: [
{
label: 'فروشگاه‌ها',
icon: 'pi pi-fw pi-store',
routerLink: ['/stores'],
},
{
label: 'کاربران',
icon: 'pi pi-fw pi-user',
routerLink: ['/users'],
},
],
},
] as MenuItem[];
@@ -0,0 +1,49 @@
<div class="flex flex-col gap-4">
<div>
<span class="text-sm text-muted-color font-semibold">Primary</span>
<div class="pt-2 flex gap-2 flex-wrap justify-start">
@for (primaryColor of primaryColors(); track primaryColor.name) {
<button
type="button"
[title]="primaryColor.name"
(click)="updateColors($event, 'primary', primaryColor)"
[ngClass]="{
'outline outline-primary': primaryColor.name === selectedPrimaryColor()
}"
class="cursor-pointer w-5 h-5 rounded-full flex shrink-0 items-center justify-center outline-offset-1 shadow"
[style]="{
'background-color': primaryColor?.name === 'noir' ? 'var(--text-color)' : primaryColor?.palette?.['500']
}"
>
</button>
}
</div>
</div>
<div>
<span class="text-sm text-muted-color font-semibold">Surface</span>
<div class="pt-2 flex gap-2 flex-wrap justify-start">
@for (surface of surfaces; track surface.name) {
<button
type="button"
[title]="surface.name"
(click)="updateColors($event, 'surface', surface)"
class="cursor-pointer w-5 h-5 rounded-full flex shrink-0 items-center justify-center p-0 outline-offset-1"
[ngClass]="{
'outline outline-primary': selectedSurfaceColor() ? selectedSurfaceColor() === surface.name : layoutService.layoutConfig().darkTheme ? surface.name === 'zinc' : surface.name === 'slate'
}"
[style]="{
'background-color': surface?.palette?.['500']
}"
></button>
}
</div>
</div>
<div class="flex flex-col gap-2">
<span class="text-sm text-muted-color font-semibold">Presets</span>
<p-selectbutton [options]="presets" [ngModel]="selectedPreset()" (ngModelChange)="onPresetChange($event)" [allowEmpty]="false" size="small"></p-selectbutton>
</div>
<div *ngIf="showMenuModeButton()" class="flex flex-col gap-2">
<span class="text-sm text-muted-color font-semibold">Menu Mode</span>
<p-selectbutton [ngModel]="menuMode()" (ngModelChange)="onMenuModeChange($event)" [options]="menuModeOptions" [allowEmpty]="false" size="small"></p-selectbutton>
</div>
</div>
@@ -0,0 +1,424 @@
import { CommonModule, isPlatformBrowser } from '@angular/common';
import { Component, computed, inject, PLATFORM_ID, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { $t, updatePreset, updateSurfacePalette } from '@primeuix/themes';
import Aura from '@primeuix/themes/aura';
import Lara from '@primeuix/themes/lara';
import Nora from '@primeuix/themes/nora';
import { PrimeNG } from 'primeng/config';
import { SelectButtonModule } from 'primeng/selectbutton';
import { LayoutService } from '../service/layout.service';
const presets = {
Aura,
Lara,
Nora,
} as const;
declare type KeyOfType<T> = keyof T extends infer U ? U : never;
declare type SurfacesType = {
name?: string;
palette?: {
0?: string;
50?: string;
100?: string;
200?: string;
300?: string;
400?: string;
500?: string;
600?: string;
700?: string;
800?: string;
900?: string;
950?: string;
};
};
@Component({
selector: 'app-configurator',
standalone: true,
imports: [CommonModule, FormsModule, SelectButtonModule],
templateUrl: './app.configurator.component.html',
host: {
class:
'hidden absolute top-13 right-0 w-72 p-4 bg-surface-0 dark:bg-surface-900 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]',
},
})
export class AppConfigurator {
router = inject(Router);
config: PrimeNG = inject(PrimeNG);
layoutService: LayoutService = inject(LayoutService);
platformId = inject(PLATFORM_ID);
primeng = inject(PrimeNG);
presets = Object.keys(presets);
showMenuModeButton = signal(!this.router.url.includes('auth'));
menuModeOptions = [
{ label: 'Static', value: 'static' },
{ label: 'Overlay', value: 'overlay' },
];
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
this.onPresetChange(this.layoutService.layoutConfig().preset);
}
}
surfaces: SurfacesType[] = [
{
name: 'slate',
palette: {
0: '#ffffff',
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617',
},
},
{
name: 'gray',
palette: {
0: '#ffffff',
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
950: '#030712',
},
},
{
name: 'zinc',
palette: {
0: '#ffffff',
50: '#fafafa',
100: '#f4f4f5',
200: '#e4e4e7',
300: '#d4d4d8',
400: '#a1a1aa',
500: '#71717a',
600: '#52525b',
700: '#3f3f46',
800: '#27272a',
900: '#18181b',
950: '#09090b',
},
},
{
name: 'neutral',
palette: {
0: '#ffffff',
50: '#fafafa',
100: '#f5f5f5',
200: '#e5e5e5',
300: '#d4d4d4',
400: '#a3a3a3',
500: '#737373',
600: '#525252',
700: '#404040',
800: '#262626',
900: '#171717',
950: '#0a0a0a',
},
},
{
name: 'stone',
palette: {
0: '#ffffff',
50: '#fafaf9',
100: '#f5f5f4',
200: '#e7e5e4',
300: '#d6d3d1',
400: '#a8a29e',
500: '#78716c',
600: '#57534e',
700: '#44403c',
800: '#292524',
900: '#1c1917',
950: '#0c0a09',
},
},
{
name: 'soho',
palette: {
0: '#ffffff',
50: '#ececec',
100: '#dedfdf',
200: '#c4c4c6',
300: '#adaeb0',
400: '#97979b',
500: '#7f8084',
600: '#6a6b70',
700: '#55565b',
800: '#3f4046',
900: '#2c2c34',
950: '#16161d',
},
},
{
name: 'viva',
palette: {
0: '#ffffff',
50: '#f3f3f3',
100: '#e7e7e8',
200: '#cfd0d0',
300: '#b7b8b9',
400: '#9fa1a1',
500: '#87898a',
600: '#6e7173',
700: '#565a5b',
800: '#3e4244',
900: '#262b2c',
950: '#0e1315',
},
},
{
name: 'ocean',
palette: {
0: '#ffffff',
50: '#fbfcfc',
100: '#F7F9F8',
200: '#EFF3F2',
300: '#DADEDD',
400: '#B1B7B6',
500: '#828787',
600: '#5F7274',
700: '#415B61',
800: '#29444E',
900: '#183240',
950: '#0c1920',
},
},
];
selectedPrimaryColor = computed(() => {
return this.layoutService.layoutConfig().primary;
});
selectedSurfaceColor = computed(() => this.layoutService.layoutConfig().surface);
selectedPreset = computed(() => this.layoutService.layoutConfig().preset);
menuMode = computed(() => this.layoutService.layoutConfig().menuMode);
primaryColors = computed<SurfacesType[]>(() => {
const presetPalette =
presets[this.layoutService.layoutConfig().preset as KeyOfType<typeof presets>].primitive;
const colors = [
'emerald',
'green',
'lime',
'orange',
'amber',
'yellow',
'teal',
'cyan',
'sky',
'blue',
'indigo',
'violet',
'purple',
'fuchsia',
'pink',
'rose',
];
const palettes: SurfacesType[] = [{ name: 'noir', palette: {} }];
colors.forEach((color) => {
palettes.push({
name: color,
palette: presetPalette?.[
color as KeyOfType<typeof presetPalette>
] as SurfacesType['palette'],
});
});
return palettes;
});
getPresetExt() {
const color: SurfacesType =
this.primaryColors().find((c) => c.name === this.selectedPrimaryColor()) || {};
const preset = this.layoutService.layoutConfig().preset;
if (color.name === 'noir') {
return {
semantic: {
primary: {
50: '{surface.50}',
100: '{surface.100}',
200: '{surface.200}',
300: '{surface.300}',
400: '{surface.400}',
500: '{surface.500}',
600: '{surface.600}',
700: '{surface.700}',
800: '{surface.800}',
900: '{surface.900}',
950: '{surface.950}',
},
colorScheme: {
light: {
primary: {
color: '{primary.950}',
contrastColor: '#ffffff',
hoverColor: '{primary.800}',
activeColor: '{primary.700}',
},
highlight: {
background: '{primary.950}',
focusBackground: '{primary.700}',
color: '#ffffff',
focusColor: '#ffffff',
},
},
dark: {
primary: {
color: '{primary.50}',
contrastColor: '{primary.950}',
hoverColor: '{primary.200}',
activeColor: '{primary.300}',
},
highlight: {
background: '{primary.50}',
focusBackground: '{primary.300}',
color: '{primary.950}',
focusColor: '{primary.950}',
},
},
},
},
};
} else {
if (preset === 'Nora') {
return {
semantic: {
primary: color.palette,
colorScheme: {
light: {
primary: {
color: '{primary.600}',
contrastColor: '#ffffff',
hoverColor: '{primary.700}',
activeColor: '{primary.800}',
},
highlight: {
background: '{primary.600}',
focusBackground: '{primary.700}',
color: '#ffffff',
focusColor: '#ffffff',
},
},
dark: {
primary: {
color: '{primary.500}',
contrastColor: '{surface.900}',
hoverColor: '{primary.400}',
activeColor: '{primary.300}',
},
highlight: {
background: '{primary.500}',
focusBackground: '{primary.400}',
color: '{surface.900}',
focusColor: '{surface.900}',
},
},
},
},
};
} else {
return {
semantic: {
primary: color.palette,
colorScheme: {
light: {
primary: {
color: '{primary.500}',
contrastColor: '#ffffff',
hoverColor: '{primary.600}',
activeColor: '{primary.700}',
},
highlight: {
background: '{primary.50}',
focusBackground: '{primary.100}',
color: '{primary.700}',
focusColor: '{primary.800}',
},
},
dark: {
primary: {
color: '{primary.400}',
contrastColor: '{surface.900}',
hoverColor: '{primary.300}',
activeColor: '{primary.200}',
},
highlight: {
background: 'color-mix(in srgb, {primary.400}, transparent 84%)',
focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)',
color: 'rgba(255,255,255,.87)',
focusColor: 'rgba(255,255,255,.87)',
},
},
},
},
};
}
}
}
updateColors(event: any, type: string, color: any) {
if (type === 'primary') {
this.layoutService.layoutConfig.update((state) => ({ ...state, primary: color.name }));
} else if (type === 'surface') {
this.layoutService.layoutConfig.update((state) => ({ ...state, surface: color.name }));
}
this.applyTheme(type, color);
event.stopPropagation();
}
applyTheme(type: string, color: any) {
if (type === 'primary') {
updatePreset(this.getPresetExt());
} else if (type === 'surface') {
updateSurfacePalette(color.palette);
}
}
onPresetChange(event: any) {
this.layoutService.layoutConfig.update((state) => ({ ...state, preset: event }));
const preset = presets[event as KeyOfType<typeof presets>];
const surfacePalette = this.surfaces.find(
(s) => s.name === this.selectedSurfaceColor(),
)?.palette;
$t()
.preset(preset)
.preset(this.getPresetExt())
.surfacePalette(surfacePalette)
.use({ useDefaultOptions: true });
}
onMenuModeChange(event: string) {
this.layoutService.layoutConfig.update((prev) => ({ ...prev, menuMode: event }));
}
}
@@ -1,446 +0,0 @@
import { CommonModule, isPlatformBrowser } from '@angular/common';
import { Component, computed, inject, PLATFORM_ID, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { $t, updatePreset, updateSurfacePalette } from '@primeuix/themes';
import Aura from '@primeuix/themes/aura';
import Lara from '@primeuix/themes/lara';
import Nora from '@primeuix/themes/nora';
import { PrimeNG } from 'primeng/config';
import { SelectButtonModule } from 'primeng/selectbutton';
import { LayoutService } from '../service/layout.service';
const presets = {
Aura,
Lara,
Nora
} as const;
declare type KeyOfType<T> = keyof T extends infer U ? U : never;
declare type SurfacesType = {
name?: string;
palette?: {
0?: string;
50?: string;
100?: string;
200?: string;
300?: string;
400?: string;
500?: string;
600?: string;
700?: string;
800?: string;
900?: string;
950?: string;
};
};
@Component({
selector: 'app-configurator',
standalone: true,
imports: [CommonModule, FormsModule, SelectButtonModule],
template: `
<div class="flex flex-col gap-4">
<div>
<span class="text-sm text-muted-color font-semibold">Primary</span>
<div class="pt-2 flex gap-2 flex-wrap justify-start">
@for (primaryColor of primaryColors(); track primaryColor.name) {
<button
type="button"
[title]="primaryColor.name"
(click)="updateColors($event, 'primary', primaryColor)"
[ngClass]="{
'outline outline-primary': primaryColor.name === selectedPrimaryColor()
}"
class="cursor-pointer w-5 h-5 rounded-full flex shrink-0 items-center justify-center outline-offset-1 shadow"
[style]="{
'background-color': primaryColor?.name === 'noir' ? 'var(--text-color)' : primaryColor?.palette?.['500']
}"
>
</button>
}
</div>
</div>
<div>
<span class="text-sm text-muted-color font-semibold">Surface</span>
<div class="pt-2 flex gap-2 flex-wrap justify-start">
@for (surface of surfaces; track surface.name) {
<button
type="button"
[title]="surface.name"
(click)="updateColors($event, 'surface', surface)"
class="cursor-pointer w-5 h-5 rounded-full flex shrink-0 items-center justify-center p-0 outline-offset-1"
[ngClass]="{
'outline outline-primary': selectedSurfaceColor() ? selectedSurfaceColor() === surface.name : layoutService.layoutConfig().darkTheme ? surface.name === 'zinc' : surface.name === 'slate'
}"
[style]="{
'background-color': surface?.palette?.['500']
}"
></button>
}
</div>
</div>
<div class="flex flex-col gap-2">
<span class="text-sm text-muted-color font-semibold">Presets</span>
<p-selectbutton [options]="presets" [ngModel]="selectedPreset()" (ngModelChange)="onPresetChange($event)" [allowEmpty]="false" size="small" />
</div>
<div *ngIf="showMenuModeButton()" class="flex flex-col gap-2">
<span class="text-sm text-muted-color font-semibold">Menu Mode</span>
<p-selectbutton [ngModel]="menuMode()" (ngModelChange)="onMenuModeChange($event)" [options]="menuModeOptions" [allowEmpty]="false" size="small" />
</div>
</div>
`,
host: {
class: 'hidden absolute top-13 right-0 w-72 p-4 bg-surface-0 dark:bg-surface-900 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]'
}
})
export class AppConfigurator {
router = inject(Router);
config: PrimeNG = inject(PrimeNG);
layoutService: LayoutService = inject(LayoutService);
platformId = inject(PLATFORM_ID);
primeng = inject(PrimeNG);
presets = Object.keys(presets);
showMenuModeButton = signal(!this.router.url.includes('auth'));
menuModeOptions = [
{ label: 'Static', value: 'static' },
{ label: 'Overlay', value: 'overlay' }
];
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
this.onPresetChange(this.layoutService.layoutConfig().preset);
}
}
surfaces: SurfacesType[] = [
{
name: 'slate',
palette: {
0: '#ffffff',
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617'
}
},
{
name: 'gray',
palette: {
0: '#ffffff',
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
950: '#030712'
}
},
{
name: 'zinc',
palette: {
0: '#ffffff',
50: '#fafafa',
100: '#f4f4f5',
200: '#e4e4e7',
300: '#d4d4d8',
400: '#a1a1aa',
500: '#71717a',
600: '#52525b',
700: '#3f3f46',
800: '#27272a',
900: '#18181b',
950: '#09090b'
}
},
{
name: 'neutral',
palette: {
0: '#ffffff',
50: '#fafafa',
100: '#f5f5f5',
200: '#e5e5e5',
300: '#d4d4d4',
400: '#a3a3a3',
500: '#737373',
600: '#525252',
700: '#404040',
800: '#262626',
900: '#171717',
950: '#0a0a0a'
}
},
{
name: 'stone',
palette: {
0: '#ffffff',
50: '#fafaf9',
100: '#f5f5f4',
200: '#e7e5e4',
300: '#d6d3d1',
400: '#a8a29e',
500: '#78716c',
600: '#57534e',
700: '#44403c',
800: '#292524',
900: '#1c1917',
950: '#0c0a09'
}
},
{
name: 'soho',
palette: {
0: '#ffffff',
50: '#ececec',
100: '#dedfdf',
200: '#c4c4c6',
300: '#adaeb0',
400: '#97979b',
500: '#7f8084',
600: '#6a6b70',
700: '#55565b',
800: '#3f4046',
900: '#2c2c34',
950: '#16161d'
}
},
{
name: 'viva',
palette: {
0: '#ffffff',
50: '#f3f3f3',
100: '#e7e7e8',
200: '#cfd0d0',
300: '#b7b8b9',
400: '#9fa1a1',
500: '#87898a',
600: '#6e7173',
700: '#565a5b',
800: '#3e4244',
900: '#262b2c',
950: '#0e1315'
}
},
{
name: 'ocean',
palette: {
0: '#ffffff',
50: '#fbfcfc',
100: '#F7F9F8',
200: '#EFF3F2',
300: '#DADEDD',
400: '#B1B7B6',
500: '#828787',
600: '#5F7274',
700: '#415B61',
800: '#29444E',
900: '#183240',
950: '#0c1920'
}
}
];
selectedPrimaryColor = computed(() => {
return this.layoutService.layoutConfig().primary;
});
selectedSurfaceColor = computed(() => this.layoutService.layoutConfig().surface);
selectedPreset = computed(() => this.layoutService.layoutConfig().preset);
menuMode = computed(() => this.layoutService.layoutConfig().menuMode);
primaryColors = computed<SurfacesType[]>(() => {
const presetPalette = presets[this.layoutService.layoutConfig().preset as KeyOfType<typeof presets>].primitive;
const colors = ['emerald', 'green', 'lime', 'orange', 'amber', 'yellow', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple', 'fuchsia', 'pink', 'rose'];
const palettes: SurfacesType[] = [{ name: 'noir', palette: {} }];
colors.forEach((color) => {
palettes.push({
name: color,
palette: presetPalette?.[color as KeyOfType<typeof presetPalette>] as SurfacesType['palette']
});
});
return palettes;
});
getPresetExt() {
const color: SurfacesType = this.primaryColors().find((c) => c.name === this.selectedPrimaryColor()) || {};
const preset = this.layoutService.layoutConfig().preset;
if (color.name === 'noir') {
return {
semantic: {
primary: {
50: '{surface.50}',
100: '{surface.100}',
200: '{surface.200}',
300: '{surface.300}',
400: '{surface.400}',
500: '{surface.500}',
600: '{surface.600}',
700: '{surface.700}',
800: '{surface.800}',
900: '{surface.900}',
950: '{surface.950}'
},
colorScheme: {
light: {
primary: {
color: '{primary.950}',
contrastColor: '#ffffff',
hoverColor: '{primary.800}',
activeColor: '{primary.700}'
},
highlight: {
background: '{primary.950}',
focusBackground: '{primary.700}',
color: '#ffffff',
focusColor: '#ffffff'
}
},
dark: {
primary: {
color: '{primary.50}',
contrastColor: '{primary.950}',
hoverColor: '{primary.200}',
activeColor: '{primary.300}'
},
highlight: {
background: '{primary.50}',
focusBackground: '{primary.300}',
color: '{primary.950}',
focusColor: '{primary.950}'
}
}
}
}
};
} else {
if (preset === 'Nora') {
return {
semantic: {
primary: color.palette,
colorScheme: {
light: {
primary: {
color: '{primary.600}',
contrastColor: '#ffffff',
hoverColor: '{primary.700}',
activeColor: '{primary.800}'
},
highlight: {
background: '{primary.600}',
focusBackground: '{primary.700}',
color: '#ffffff',
focusColor: '#ffffff'
}
},
dark: {
primary: {
color: '{primary.500}',
contrastColor: '{surface.900}',
hoverColor: '{primary.400}',
activeColor: '{primary.300}'
},
highlight: {
background: '{primary.500}',
focusBackground: '{primary.400}',
color: '{surface.900}',
focusColor: '{surface.900}'
}
}
}
}
};
} else {
return {
semantic: {
primary: color.palette,
colorScheme: {
light: {
primary: {
color: '{primary.500}',
contrastColor: '#ffffff',
hoverColor: '{primary.600}',
activeColor: '{primary.700}'
},
highlight: {
background: '{primary.50}',
focusBackground: '{primary.100}',
color: '{primary.700}',
focusColor: '{primary.800}'
}
},
dark: {
primary: {
color: '{primary.400}',
contrastColor: '{surface.900}',
hoverColor: '{primary.300}',
activeColor: '{primary.200}'
},
highlight: {
background: 'color-mix(in srgb, {primary.400}, transparent 84%)',
focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)',
color: 'rgba(255,255,255,.87)',
focusColor: 'rgba(255,255,255,.87)'
}
}
}
}
};
}
}
}
updateColors(event: any, type: string, color: any) {
if (type === 'primary') {
this.layoutService.layoutConfig.update((state) => ({ ...state, primary: color.name }));
} else if (type === 'surface') {
this.layoutService.layoutConfig.update((state) => ({ ...state, surface: color.name }));
}
this.applyTheme(type, color);
event.stopPropagation();
}
applyTheme(type: string, color: any) {
if (type === 'primary') {
updatePreset(this.getPresetExt());
} else if (type === 'surface') {
updateSurfacePalette(color.palette);
}
}
onPresetChange(event: any) {
this.layoutService.layoutConfig.update((state) => ({ ...state, preset: event }));
const preset = presets[event as KeyOfType<typeof presets>];
const surfacePalette = this.surfaces.find((s) => s.name === this.selectedSurfaceColor())?.palette;
$t().preset(preset).preset(this.getPresetExt()).surfacePalette(surfacePalette).use({ useDefaultOptions: true });
}
onMenuModeChange(event: string) {
this.layoutService.layoutConfig.update((prev) => ({ ...prev, menuMode: event }));
}
}
@@ -0,0 +1,7 @@
<div class="flex gap-4 top-8 right-8" [ngClass]="{'fixed':float()}">
<p-button type="button" (onClick)="toggleDarkMode()" [rounded]="true" [icon]="isDarkTheme() ? 'pi pi-moon' : 'pi pi-sun'" severity="secondary"></p-button>
<div class="relative">
<p-button icon="pi pi-palette" pStyleClass="@next" enterFromClass="hidden" enterActiveClass="animate-scalein" leaveToClass="hidden" leaveActiveClass="animate-fadeout" [hideOnOutsideClick]="true" type="button" rounded></p-button>
<app-configurator></app-configurator>
</div>
</div>
@@ -0,0 +1,23 @@
import { CommonModule } from '@angular/common';
import { Component, computed, inject, input } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { StyleClassModule } from 'primeng/styleclass';
import { LayoutService } from '../service/layout.service';
import { AppConfigurator } from './app.configurator.component';
@Component({
selector: 'app-floating-configurator',
imports: [CommonModule, ButtonModule, StyleClassModule, AppConfigurator],
templateUrl: './app.floatingconfigurator.component.html',
})
export class AppFloatingConfigurator {
LayoutService = inject(LayoutService);
float = input<boolean>(true);
isDarkTheme = computed(() => this.LayoutService.layoutConfig().darkTheme);
toggleDarkMode() {
this.LayoutService.layoutConfig.update((state) => ({ ...state, darkTheme: !state.darkTheme }));
}
}
@@ -1,32 +0,0 @@
import {Component, computed, inject, input} from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { StyleClassModule } from 'primeng/styleclass';
import { AppConfigurator } from './app.configurator';
import { LayoutService } from '../service/layout.service';
import {CommonModule} from "@angular/common";
@Component({
selector: 'app-floating-configurator',
imports: [CommonModule, ButtonModule, StyleClassModule, AppConfigurator],
template: `
<div class="flex gap-4 top-8 right-8" [ngClass]="{'fixed':float()}">
<p-button type="button" (onClick)="toggleDarkMode()" [rounded]="true" [icon]="isDarkTheme() ? 'pi pi-moon' : 'pi pi-sun'" severity="secondary" />
<div class="relative">
<p-button icon="pi pi-palette" pStyleClass="@next" enterFromClass="hidden" enterActiveClass="animate-scalein" leaveToClass="hidden" leaveActiveClass="animate-fadeout" [hideOnOutsideClick]="true" type="button" rounded />
<app-configurator />
</div>
</div>
`
})
export class AppFloatingConfigurator {
LayoutService = inject(LayoutService);
float = input<boolean>(true);
isDarkTheme = computed(() => this.LayoutService.layoutConfig().darkTheme);
toggleDarkMode() {
this.LayoutService.layoutConfig.update((state) => ({ ...state, darkTheme: !state.darkTheme }));
}
}
@@ -0,0 +1 @@
@@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
standalone: true,
selector: 'app-footer',
templateUrl: './app.footer.component.html',
})
export class AppFooter {}
-11
View File
@@ -1,11 +0,0 @@
import { Component } from '@angular/core';
@Component({
standalone: true,
selector: 'app-footer',
template: `<div class="layout-footer">
SAKAI by
<a href="https://primeng.org" target="_blank" rel="noopener noreferrer" class="text-primary font-bold hover:underline">PrimeNG</a>
</div>`
})
export class AppFooter {}
@@ -0,0 +1,31 @@
<div class="layout-wrapper" [ngClass]="containerClass">
<app-topbar></app-topbar>
@if (fullLoading) {
<div class="flex justify-center align-items-center h-svh items-center">
<p-progressSpinner></p-progressSpinner>
</div>
} @else {
@if (showMenu()) {
<app-sidebar></app-sidebar>
}
<div [class]="`layout-main-container ${!showMenu ? 'hideMenu' : ''}`">
<div class="layout-main flex flex-col gap-4">
@if (showBreadcrumb) {
<app-breadcrumb class="rounded-md overflow-hidden shrink-0" />
}
<div [class]="`rounded-md flex flex-col grow ${isFixedContentSize ? 'overflow-auto' : 'overflow-hidden'}`">
<!-- style="container-type: size" -->
<div class="h-full">
@if (content) {
<ng-container [ngTemplateOutlet]="content"></ng-container>
} @else {
<router-outlet></router-outlet>
}
</div>
</div>
</div>
<app-footer></app-footer>
</div>
}
<div class="layout-mask animate-fadein"></div>
</div>
@@ -0,0 +1,153 @@
import { BreadcrumbService } from '@/core/services';
import { BreadcrumbComponent } from '@/shared/components';
import { CommonModule } from '@angular/common';
import {
Component,
computed,
ContentChild,
inject,
Input,
Renderer2,
TemplateRef,
ViewChild,
} from '@angular/core';
import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { ProgressSpinner } from 'primeng/progressspinner';
import { filter, Subscription } from 'rxjs';
import { LayoutService } from '../service/layout.service';
import { AppFooter } from './app.footer.component';
import { AppSidebar } from './app.sidebar.component';
import { AppTopbar } from './app.topbar.component';
@Component({
selector: 'app-layout',
standalone: true,
imports: [
CommonModule,
AppTopbar,
AppSidebar,
RouterModule,
AppFooter,
BreadcrumbComponent,
ProgressSpinner,
],
templateUrl: './app.layout.component.html',
})
export class AppLayout {
overlayMenuOpenSubscription: Subscription;
menuOutsideClickListener: any;
@Input() fullLoading: boolean = false;
@ViewChild(AppSidebar) appSidebar!: AppSidebar;
@ViewChild(AppTopbar) appTopBar!: AppTopbar;
@ContentChild('content', { static: true }) content?: TemplateRef<any> | null;
private breadcrumbService = inject(BreadcrumbService);
constructor(
public layoutService: LayoutService,
public renderer: Renderer2,
public router: Router,
) {
this.overlayMenuOpenSubscription = this.layoutService.overlayOpen$.subscribe(() => {
if (!this.menuOutsideClickListener) {
this.menuOutsideClickListener = this.renderer.listen('document', 'click', (event) => {
if (this.isOutsideClicked(event)) {
this.hideMenu();
}
});
}
if (this.layoutService.layoutState().staticMenuMobileActive) {
this.blockBodyScroll();
}
});
this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe(() => {
this.hideMenu();
});
}
isOutsideClicked(event: MouseEvent) {
const sidebarEl = document.querySelector('.layout-sidebar');
const topbarEl = document.querySelector('.layout-menu-button');
const eventTarget = event.target as Node;
return !(
sidebarEl?.isSameNode(eventTarget) ||
sidebarEl?.contains(eventTarget) ||
topbarEl?.isSameNode(eventTarget) ||
topbarEl?.contains(eventTarget)
);
}
hideMenu() {
this.layoutService.layoutState.update((prev) => ({
...prev,
overlayMenuActive: false,
staticMenuMobileActive: false,
menuHoverActive: false,
}));
if (this.menuOutsideClickListener) {
this.menuOutsideClickListener();
this.menuOutsideClickListener = null;
}
this.unblockBodyScroll();
}
blockBodyScroll(): void {
if (document.body.classList) {
document.body.classList.add('blocked-scroll');
} else {
document.body.className += ' blocked-scroll';
}
}
unblockBodyScroll(): void {
if (document.body.classList) {
document.body.classList.remove('blocked-scroll');
} else {
document.body.className = document.body.className.replace(
new RegExp('(^|\\b)' + 'blocked-scroll'.split(' ').join('|') + '(\\b|$)', 'gi'),
' ',
);
}
}
get containerClass() {
return {
'layout-overlay': this.layoutService.layoutConfig().menuMode === 'overlay',
'layout-static': this.layoutService.layoutConfig().menuMode === 'static',
'layout-static-inactive':
this.layoutService.layoutState().staticMenuDesktopInactive &&
this.layoutService.layoutConfig().menuMode === 'static',
'layout-overlay-active': this.layoutService.layoutState().overlayMenuActive,
'layout-mobile-active': this.layoutService.layoutState().staticMenuMobileActive,
};
}
get showBreadcrumb(): boolean {
return this.breadcrumbService.items.length > 0;
}
get isFixedContentSize(): boolean {
return this.layoutService.isFixedContentSize() || false;
}
showMenu = computed(() => {
return this.layoutService.menuItems().length > 0;
});
ngOnDestroy() {
if (this.overlayMenuOpenSubscription) {
this.overlayMenuOpenSubscription.unsubscribe();
}
if (this.menuOutsideClickListener) {
this.menuOutsideClickListener();
}
}
}
-111
View File
@@ -1,111 +0,0 @@
import { Component, Renderer2, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { filter, Subscription } from 'rxjs';
import { AppTopbar } from './app.topbar';
import { AppSidebar } from './app.sidebar';
import { AppFooter } from './app.footer';
import { LayoutService } from '../service/layout.service';
@Component({
selector: 'app-layout',
standalone: true,
imports: [CommonModule, AppTopbar, AppSidebar, RouterModule, AppFooter],
template: `<div class="layout-wrapper" [ngClass]="containerClass">
<app-topbar></app-topbar>
<app-sidebar></app-sidebar>
<div class="layout-main-container">
<div class="layout-main">
<router-outlet></router-outlet>
</div>
<app-footer></app-footer>
</div>
<div class="layout-mask animate-fadein"></div>
</div> `
})
export class AppLayout {
overlayMenuOpenSubscription: Subscription;
menuOutsideClickListener: any;
@ViewChild(AppSidebar) appSidebar!: AppSidebar;
@ViewChild(AppTopbar) appTopBar!: AppTopbar;
constructor(
public layoutService: LayoutService,
public renderer: Renderer2,
public router: Router
) {
this.overlayMenuOpenSubscription = this.layoutService.overlayOpen$.subscribe(() => {
if (!this.menuOutsideClickListener) {
this.menuOutsideClickListener = this.renderer.listen('document', 'click', (event) => {
if (this.isOutsideClicked(event)) {
this.hideMenu();
}
});
}
if (this.layoutService.layoutState().staticMenuMobileActive) {
this.blockBodyScroll();
}
});
this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe(() => {
this.hideMenu();
});
}
isOutsideClicked(event: MouseEvent) {
const sidebarEl = document.querySelector('.layout-sidebar');
const topbarEl = document.querySelector('.layout-menu-button');
const eventTarget = event.target as Node;
return !(sidebarEl?.isSameNode(eventTarget) || sidebarEl?.contains(eventTarget) || topbarEl?.isSameNode(eventTarget) || topbarEl?.contains(eventTarget));
}
hideMenu() {
this.layoutService.layoutState.update((prev) => ({ ...prev, overlayMenuActive: false, staticMenuMobileActive: false, menuHoverActive: false }));
if (this.menuOutsideClickListener) {
this.menuOutsideClickListener();
this.menuOutsideClickListener = null;
}
this.unblockBodyScroll();
}
blockBodyScroll(): void {
if (document.body.classList) {
document.body.classList.add('blocked-scroll');
} else {
document.body.className += ' blocked-scroll';
}
}
unblockBodyScroll(): void {
if (document.body.classList) {
document.body.classList.remove('blocked-scroll');
} else {
document.body.className = document.body.className.replace(new RegExp('(^|\\b)' + 'blocked-scroll'.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
}
}
get containerClass() {
return {
'layout-overlay': this.layoutService.layoutConfig().menuMode === 'overlay',
'layout-static': this.layoutService.layoutConfig().menuMode === 'static',
'layout-static-inactive': this.layoutService.layoutState().staticMenuDesktopInactive && this.layoutService.layoutConfig().menuMode === 'static',
'layout-overlay-active': this.layoutService.layoutState().overlayMenuActive,
'layout-mobile-active': this.layoutService.layoutState().staticMenuMobileActive
};
}
ngOnDestroy() {
if (this.overlayMenuOpenSubscription) {
this.overlayMenuOpenSubscription.unsubscribe();
}
if (this.menuOutsideClickListener) {
this.menuOutsideClickListener();
}
}
}
@@ -0,0 +1,10 @@
<ul class="layout-menu">
<ng-container *ngFor="let item of model; let i = index">
@if (!item.separator) {
<li app-menuitem [item]="item" [index]="i" [root]="true"></li>
}
@if (item.separator) {
<li class="menu-separator"></li>
}
</ng-container>
</ul>
@@ -0,0 +1,19 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AppMenuitem } from './app.menuitem.component';
import { LayoutService } from '../service/layout.service';
@Component({
selector: 'app-menu',
standalone: true,
imports: [CommonModule, AppMenuitem, RouterModule],
templateUrl: './app.menu.component.html',
})
export class AppMenu {
private layoutService = inject(LayoutService);
get model() {
return this.layoutService.menuItems();
}
}
-157
View File
@@ -1,157 +0,0 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MenuItem } from 'primeng/api';
import { AppMenuitem } from './app.menuitem';
@Component({
selector: 'app-menu',
standalone: true,
imports: [CommonModule, AppMenuitem, RouterModule],
template: `<ul class="layout-menu">
<ng-container *ngFor="let item of model; let i = index">
<li app-menuitem *ngIf="!item.separator" [item]="item" [index]="i" [root]="true"></li>
<li *ngIf="item.separator" class="menu-separator"></li>
</ng-container>
</ul> `
})
export class AppMenu {
model: MenuItem[] = [];
ngOnInit() {
this.model = [
{
label: 'Home',
items: [{ label: 'Dashboard', icon: 'pi pi-fw pi-home', routerLink: ['/'] }]
},
{
label: 'UI Components',
items: [
{ label: 'Form Layout', icon: 'pi pi-fw pi-id-card', routerLink: ['/uikit/formlayout'] },
{ label: 'Input', icon: 'pi pi-fw pi-check-square', routerLink: ['/uikit/input'] },
{ label: 'Button', icon: 'pi pi-fw pi-mobile', class: 'rotated-icon', routerLink: ['/uikit/button'] },
{ label: 'Table', icon: 'pi pi-fw pi-table', routerLink: ['/uikit/table'] },
{ label: 'List', icon: 'pi pi-fw pi-list', routerLink: ['/uikit/list'] },
{ label: 'Tree', icon: 'pi pi-fw pi-share-alt', routerLink: ['/uikit/tree'] },
{ label: 'Panel', icon: 'pi pi-fw pi-tablet', routerLink: ['/uikit/panel'] },
{ label: 'Overlay', icon: 'pi pi-fw pi-clone', routerLink: ['/uikit/overlay'] },
{ label: 'Media', icon: 'pi pi-fw pi-image', routerLink: ['/uikit/media'] },
{ label: 'Menu', icon: 'pi pi-fw pi-bars', routerLink: ['/uikit/menu'] },
{ label: 'Message', icon: 'pi pi-fw pi-comment', routerLink: ['/uikit/message'] },
{ label: 'File', icon: 'pi pi-fw pi-file', routerLink: ['/uikit/file'] },
{ label: 'Chart', icon: 'pi pi-fw pi-chart-bar', routerLink: ['/uikit/charts'] },
{ label: 'Timeline', icon: 'pi pi-fw pi-calendar', routerLink: ['/uikit/timeline'] },
{ label: 'Misc', icon: 'pi pi-fw pi-circle', routerLink: ['/uikit/misc'] }
]
},
{
label: 'Pages',
icon: 'pi pi-fw pi-briefcase',
routerLink: ['/pages'],
items: [
{
label: 'Landing',
icon: 'pi pi-fw pi-globe',
routerLink: ['/landing']
},
{
label: 'Auth',
icon: 'pi pi-fw pi-user',
items: [
{
label: 'Login',
icon: 'pi pi-fw pi-sign-in',
routerLink: ['/auth/login']
},
{
label: 'Error',
icon: 'pi pi-fw pi-times-circle',
routerLink: ['/auth/error']
},
{
label: 'Access Denied',
icon: 'pi pi-fw pi-lock',
routerLink: ['/auth/access']
}
]
},
{
label: 'Crud',
icon: 'pi pi-fw pi-pencil',
routerLink: ['/pages/crud']
},
{
label: 'Not Found',
icon: 'pi pi-fw pi-exclamation-circle',
routerLink: ['/pages/notfound']
},
{
label: 'Empty',
icon: 'pi pi-fw pi-circle-off',
routerLink: ['/pages/empty']
}
]
},
{
label: 'Hierarchy',
items: [
{
label: 'Submenu 1',
icon: 'pi pi-fw pi-bookmark',
items: [
{
label: 'Submenu 1.1',
icon: 'pi pi-fw pi-bookmark',
items: [
{ label: 'Submenu 1.1.1', icon: 'pi pi-fw pi-bookmark' },
{ label: 'Submenu 1.1.2', icon: 'pi pi-fw pi-bookmark' },
{ label: 'Submenu 1.1.3', icon: 'pi pi-fw pi-bookmark' }
]
},
{
label: 'Submenu 1.2',
icon: 'pi pi-fw pi-bookmark',
items: [{ label: 'Submenu 1.2.1', icon: 'pi pi-fw pi-bookmark' }]
}
]
},
{
label: 'Submenu 2',
icon: 'pi pi-fw pi-bookmark',
items: [
{
label: 'Submenu 2.1',
icon: 'pi pi-fw pi-bookmark',
items: [
{ label: 'Submenu 2.1.1', icon: 'pi pi-fw pi-bookmark' },
{ label: 'Submenu 2.1.2', icon: 'pi pi-fw pi-bookmark' }
]
},
{
label: 'Submenu 2.2',
icon: 'pi pi-fw pi-bookmark',
items: [{ label: 'Submenu 2.2.1', icon: 'pi pi-fw pi-bookmark' }]
}
]
}
]
},
{
label: 'Get Started',
items: [
{
label: 'Documentation',
icon: 'pi pi-fw pi-book',
routerLink: ['/documentation']
},
{
label: 'View Source',
icon: 'pi pi-fw pi-github',
url: 'https://github.com/primefaces/sakai-ng',
target: '_blank'
}
]
}
];
}
}
@@ -0,0 +1,58 @@
<ng-container>
<div *ngIf="root && item.visible !== false" class="layout-menuitem-root-text">{{ item.label }}</div>
<ng-template #defaultIcon>
<i [ngClass]="item.icon" class="layout-menuitem-icon"></i>
</ng-template>
<a
*ngIf="(!item.routerLink || item.items) && item.visible !== false"
[attr.href]="item.url"
(click)="itemClick($event)"
[ngClass]="item.styleClass"
[attr.target]="item.target"
tabindex="0"
pRipple
>
<ng-container *ngIf="item['customIcon']; else defaultIcon">
<ng-container *ngComponentOutlet="item['customIcon']" size="14"></ng-container>
</ng-container>
<span class="layout-menuitem-text">{{ item.label }}</span>
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" *ngIf="item.items"></i>
</a>
<a
*ngIf="item.routerLink && !item.items && item.visible !== false"
(click)="itemClick($event)"
[ngClass]="item.styleClass"
[routerLink]="item.routerLink"
routerLinkActive="active-route"
[routerLinkActiveOptions]="
item.routerLinkActiveOptions || {
paths: 'subset',
queryParams: 'ignored',
matrixParams: 'ignored',
fragment: 'ignored',
}
"
[fragment]="item.fragment"
[queryParamsHandling]="item.queryParamsHandling"
[preserveFragment]="item.preserveFragment"
[skipLocationChange]="item.skipLocationChange"
[replaceUrl]="item.replaceUrl"
[state]="item.state"
[queryParams]="item.queryParams"
[attr.target]="item.target"
tabindex="0"
pRipple
>
<ng-container *ngIf="item['customIcon']; else defaultIcon">
<ng-container *ngComponentOutlet="item['customIcon']" size="14"></ng-container>
</ng-container>
<span class="layout-menuitem-text">{{ item.label }}</span>
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" *ngIf="item.items"></i>
</a>
<ul *ngIf="item.items && item.visible !== false" [@children]="submenuAnimation">
<ng-template ngFor let-child let-i="index" [ngForOf]="item.items">
<li app-menuitem [item]="child" [index]="i" [parentKey]="key" [class]="child['badgeClass']"></li>
</ng-template>
</ul>
</ng-container>
@@ -0,0 +1,166 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { CommonModule } from '@angular/common';
import { Component, HostBinding, Input } from '@angular/core';
import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { MenuItem } from 'primeng/api';
import { RippleModule } from 'primeng/ripple';
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
import { LayoutService } from '../service/layout.service';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: '[app-menuitem]',
imports: [CommonModule, RouterModule, RippleModule],
templateUrl: './app.menuitem.component.html',
animations: [
trigger('children', [
state(
'collapsed',
style({
height: '0',
}),
),
state(
'expanded',
style({
height: '*',
}),
),
transition('collapsed <=> expanded', animate('400ms cubic-bezier(0.86, 0, 0.07, 1)')),
]),
],
providers: [LayoutService],
})
export class AppMenuitem {
@Input() item!: MenuItem;
@Input() index!: number;
@Input() @HostBinding('class.layout-root-menuitem') root!: boolean;
@Input() parentKey!: string;
active = false;
menuSourceSubscription: Subscription;
menuResetSubscription: Subscription;
key: string = '';
constructor(
public router: Router,
private layoutService: LayoutService,
) {
this.menuSourceSubscription = this.layoutService.menuSource$.subscribe((value) => {
Promise.resolve(null).then(() => {
if (value.routeEvent) {
this.active =
value.key === this.key || value.key.startsWith(this.key + '-') ? true : false;
} else {
if (value.key !== this.key && !value.key.startsWith(this.key + '-')) {
this.active = false;
}
}
});
});
this.menuResetSubscription = this.layoutService.resetSource$.subscribe(() => {
this.active = false;
});
this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe(() => {
// always update active state on navigation so parent items can
// detect if any descendant route is active
this.updateActiveStateFromRoute();
});
}
ngOnInit() {
this.key = this.parentKey ? this.parentKey + '-' + this.index : String(this.index);
if (this.item.routerLink) {
this.updateActiveStateFromRoute();
}
}
updateActiveStateFromRoute() {
const opts = {
paths: 'subset' as any,
queryParams: 'ignored' as any,
matrixParams: 'subset' as any,
fragment: 'ignored' as any,
};
let activeRoute = false;
if (this.item.routerLink) {
activeRoute = this.router.isActive(this.item.routerLink[0], opts);
}
// if this item itself is not active, check its descendants
if (!activeRoute && this.item.items && this.item.items.length) {
const anyChildActive = (items: MenuItem[]): boolean => {
for (const it of items) {
if (it.routerLink && this.router.isActive(it.routerLink[0], opts)) {
return true;
}
if (it.items && it.items.length && anyChildActive(it.items)) {
return true;
}
}
return false;
};
activeRoute = anyChildActive(this.item.items);
}
if (activeRoute) {
// notify layout that this menu (or its descendant) is active
this.layoutService.onMenuStateChange({ key: this.key, routeEvent: true });
}
}
itemClick(event: Event) {
// avoid processing disabled items
if (this.item.disabled) {
event.preventDefault();
return;
}
// execute command
if (this.item.command) {
this.item.command({ originalEvent: event, item: this.item });
}
// toggle active state
if (this.item.items) {
this.active = !this.active;
}
this.layoutService.onMenuStateChange({ key: this.key });
}
get submenuAnimation() {
return this.root ? 'expanded' : this.active ? 'expanded' : 'collapsed';
}
@HostBinding('class.active-menuitem')
get activeClass() {
// mark item as active when itself or any of its submenu items are active
// include root items as well so parents highlight when a child route is active
return this.active;
}
ngOnDestroy() {
if (this.menuSourceSubscription) {
this.menuSourceSubscription.unsubscribe();
}
if (this.menuResetSubscription) {
this.menuResetSubscription.unsubscribe();
}
}
}
-170
View File
@@ -1,170 +0,0 @@
import { Component, HostBinding, Input } from '@angular/core';
import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import { RippleModule } from 'primeng/ripple';
import { MenuItem } from 'primeng/api';
import { LayoutService } from '../service/layout.service';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: '[app-menuitem]',
imports: [CommonModule, RouterModule, RippleModule],
template: `
<ng-container>
<div *ngIf="root && item.visible !== false" class="layout-menuitem-root-text">{{ item.label }}</div>
<a *ngIf="(!item.routerLink || item.items) && item.visible !== false" [attr.href]="item.url" (click)="itemClick($event)" [ngClass]="item.styleClass" [attr.target]="item.target" tabindex="0" pRipple>
<i [ngClass]="item.icon" class="layout-menuitem-icon"></i>
<span class="layout-menuitem-text">{{ item.label }}</span>
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" *ngIf="item.items"></i>
</a>
<a
*ngIf="item.routerLink && !item.items && item.visible !== false"
(click)="itemClick($event)"
[ngClass]="item.styleClass"
[routerLink]="item.routerLink"
routerLinkActive="active-route"
[routerLinkActiveOptions]="item.routerLinkActiveOptions || { paths: 'exact', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored' }"
[fragment]="item.fragment"
[queryParamsHandling]="item.queryParamsHandling"
[preserveFragment]="item.preserveFragment"
[skipLocationChange]="item.skipLocationChange"
[replaceUrl]="item.replaceUrl"
[state]="item.state"
[queryParams]="item.queryParams"
[attr.target]="item.target"
tabindex="0"
pRipple
>
<i [ngClass]="item.icon" class="layout-menuitem-icon"></i>
<span class="layout-menuitem-text">{{ item.label }}</span>
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" *ngIf="item.items"></i>
</a>
<ul *ngIf="item.items && item.visible !== false" [@children]="submenuAnimation">
<ng-template ngFor let-child let-i="index" [ngForOf]="item.items">
<li app-menuitem [item]="child" [index]="i" [parentKey]="key" [class]="child['badgeClass']"></li>
</ng-template>
</ul>
</ng-container>
`,
animations: [
trigger('children', [
state(
'collapsed',
style({
height: '0'
})
),
state(
'expanded',
style({
height: '*'
})
),
transition('collapsed <=> expanded', animate('400ms cubic-bezier(0.86, 0, 0.07, 1)'))
])
],
providers: [LayoutService]
})
export class AppMenuitem {
@Input() item!: MenuItem;
@Input() index!: number;
@Input() @HostBinding('class.layout-root-menuitem') root!: boolean;
@Input() parentKey!: string;
active = false;
menuSourceSubscription: Subscription;
menuResetSubscription: Subscription;
key: string = '';
constructor(
public router: Router,
private layoutService: LayoutService
) {
this.menuSourceSubscription = this.layoutService.menuSource$.subscribe((value) => {
Promise.resolve(null).then(() => {
if (value.routeEvent) {
this.active = value.key === this.key || value.key.startsWith(this.key + '-') ? true : false;
} else {
if (value.key !== this.key && !value.key.startsWith(this.key + '-')) {
this.active = false;
}
}
});
});
this.menuResetSubscription = this.layoutService.resetSource$.subscribe(() => {
this.active = false;
});
this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe((params) => {
if (this.item.routerLink) {
this.updateActiveStateFromRoute();
}
});
}
ngOnInit() {
this.key = this.parentKey ? this.parentKey + '-' + this.index : String(this.index);
if (this.item.routerLink) {
this.updateActiveStateFromRoute();
}
}
updateActiveStateFromRoute() {
let activeRoute = this.router.isActive(this.item.routerLink[0], { paths: 'exact', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored' });
if (activeRoute) {
this.layoutService.onMenuStateChange({ key: this.key, routeEvent: true });
}
}
itemClick(event: Event) {
// avoid processing disabled items
if (this.item.disabled) {
event.preventDefault();
return;
}
// execute command
if (this.item.command) {
this.item.command({ originalEvent: event, item: this.item });
}
// toggle active state
if (this.item.items) {
this.active = !this.active;
}
this.layoutService.onMenuStateChange({ key: this.key });
}
get submenuAnimation() {
return this.root ? 'expanded' : this.active ? 'expanded' : 'collapsed';
}
@HostBinding('class.active-menuitem')
get activeClass() {
return this.active && !this.root;
}
ngOnDestroy() {
if (this.menuSourceSubscription) {
this.menuSourceSubscription.unsubscribe();
}
if (this.menuResetSubscription) {
this.menuResetSubscription.unsubscribe();
}
}
}
@@ -0,0 +1,3 @@
<div class="layout-sidebar">
<app-menu></app-menu>
</div>
@@ -0,0 +1,12 @@
import { Component, ElementRef } from '@angular/core';
import { AppMenu } from './app.menu.component';
@Component({
selector: 'app-sidebar',
standalone: true,
imports: [AppMenu],
templateUrl: './app.sidebar.component.html',
})
export class AppSidebar {
constructor(public el: ElementRef) {}
}
-14
View File
@@ -1,14 +0,0 @@
import { Component, ElementRef } from '@angular/core';
import { AppMenu } from './app.menu';
@Component({
selector: 'app-sidebar',
standalone: true,
imports: [AppMenu],
template: ` <div class="layout-sidebar">
<app-menu></app-menu>
</div>`
})
export class AppSidebar {
constructor(public el: ElementRef) {}
}
@@ -0,0 +1,40 @@
<div class="layout-topbar">
<div class="layout-topbar-logo-container">
<button class="layout-menu-button layout-topbar-action" (click)="layoutService.onMenuToggle()">
<i class="pi pi-bars"></i>
</button>
<div class="layout-topbar-logo">
<img [src]="logo" width="32" />
<span class="text-xl">صندوق فروشگاهی</span>
</div>
</div>
<div class="layout-topbar-actions">
<div class="layout-config-menu">
<button type="button" class="layout-topbar-action" (click)="toggleDarkMode()">
<i
[ngClass]="{ 'pi ': true, 'pi-moon': layoutService.isDarkTheme(), 'pi-sun': !layoutService.isDarkTheme() }"
></i>
</button>
<!-- <div class="relative">
<button
class="layout-topbar-action layout-topbar-action-highlight"
pStyleClass="@next"
enterFromClass="hidden"
enterActiveClass="animate-scalein"
leaveToClass="hidden"
leaveActiveClass="animate-fadeout"
[hideOnOutsideClick]="true"
>
<i class="pi pi-palette"></i>
</button>
<app-configurator></app-configurator>
</div> -->
</div>
<div class="">
<p-menu #menu [model]="profileMenuItems" [popup]="true" />
<p-button (click)="menu.toggle($event)" icon="pi pi-user" text severity="contrast" size="large" />
</div>
</div>
</div>
@@ -0,0 +1,53 @@
import { AuthService } from '@/core/services/auth.service';
import { CommonModule } from '@angular/common';
import { Component, computed, inject } from '@angular/core';
import { RouterModule } from '@angular/router';
import { MenuItem } from 'primeng/api';
import { Button } from 'primeng/button';
import { MenuModule } from 'primeng/menu';
import { StyleClassModule } from 'primeng/styleclass';
import images from 'src/assets/images';
import { LayoutService } from '../service/layout.service';
@Component({
selector: 'app-topbar',
standalone: true,
imports: [RouterModule, CommonModule, StyleClassModule, MenuModule, Button],
templateUrl: './app.topbar.component.html',
})
export class AppTopbar {
items!: MenuItem[];
constructor(public layoutService: LayoutService) {}
private authService: AuthService = inject(AuthService);
readonly logo = images.logo;
toggleDarkMode() {
this.layoutService.layoutConfig.update((state) => ({ ...state, darkTheme: !state.darkTheme }));
}
username = computed(() => this.authService.currentUser()?.fullName || 'کاربر ناشناس');
logout = () => {
this.authService.logout();
};
profileMenuItems: MenuItem[] = [
{
label: this.username(),
icon: 'pi pi-user',
disabled: true,
},
{
label: 'راهنمای سامانه',
icon: 'pi pi-question-circle',
disabled: true,
},
{
label: 'خروج',
icon: 'pi pi-sign-out',
command: this.logout,
},
];
}
-120
View File
@@ -1,120 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { MenuItem } from 'primeng/api';
import { StyleClassModule } from 'primeng/styleclass';
import { LayoutService } from '../service/layout.service';
import { AppConfigurator } from './app.configurator';
@Component({
selector: 'app-topbar',
standalone: true,
imports: [RouterModule, CommonModule, StyleClassModule, AppConfigurator],
template: ` <div class="layout-topbar">
<div class="layout-topbar-logo-container">
<button
class="layout-menu-button layout-topbar-action"
(click)="layoutService.onMenuToggle()"
>
<i class="pi pi-bars"></i>
</button>
<a class="layout-topbar-logo" routerLink="/">
<svg viewBox="0 0 54 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M17.1637 19.2467C17.1566 19.4033 17.1529 19.561 17.1529 19.7194C17.1529 25.3503 21.7203 29.915 27.3546 29.915C32.9887 29.915 37.5561 25.3503 37.5561 19.7194C37.5561 19.5572 37.5524 19.3959 37.5449 19.2355C38.5617 19.0801 39.5759 18.9013 40.5867 18.6994L40.6926 18.6782C40.7191 19.0218 40.7326 19.369 40.7326 19.7194C40.7326 27.1036 34.743 33.0896 27.3546 33.0896C19.966 33.0896 13.9765 27.1036 13.9765 19.7194C13.9765 19.374 13.9896 19.0316 14.0154 18.6927L14.0486 18.6994C15.0837 18.9062 16.1223 19.0886 17.1637 19.2467ZM33.3284 11.4538C31.6493 10.2396 29.5855 9.52381 27.3546 9.52381C25.1195 9.52381 23.0524 10.2421 21.3717 11.4603C20.0078 11.3232 18.6475 11.1387 17.2933 10.907C19.7453 8.11308 23.3438 6.34921 27.3546 6.34921C31.36 6.34921 34.9543 8.10844 37.4061 10.896C36.0521 11.1292 34.692 11.3152 33.3284 11.4538ZM43.826 18.0518C43.881 18.6003 43.9091 19.1566 43.9091 19.7194C43.9091 28.8568 36.4973 36.2642 27.3546 36.2642C18.2117 36.2642 10.8 28.8568 10.8 19.7194C10.8 19.1615 10.8276 18.61 10.8816 18.0663L7.75383 17.4411C7.66775 18.1886 7.62354 18.9488 7.62354 19.7194C7.62354 30.6102 16.4574 39.4388 27.3546 39.4388C38.2517 39.4388 47.0855 30.6102 47.0855 19.7194C47.0855 18.9439 47.0407 18.1789 46.9536 17.4267L43.826 18.0518ZM44.2613 9.54743L40.9084 10.2176C37.9134 5.95821 32.9593 3.1746 27.3546 3.1746C21.7442 3.1746 16.7856 5.96385 13.7915 10.2305L10.4399 9.56057C13.892 3.83178 20.1756 0 27.3546 0C34.5281 0 40.8075 3.82591 44.2613 9.54743Z"
fill="var(--primary-color)"
/>
<mask
id="mask0_1413_1551"
style="mask-type: alpha"
maskUnits="userSpaceOnUse"
x="0"
y="8"
width="54"
height="11"
>
<path
d="M27 18.3652C10.5114 19.1944 0 8.88892 0 8.88892C0 8.88892 16.5176 14.5866 27 14.5866C37.4824 14.5866 54 8.88892 54 8.88892C54 8.88892 43.4886 17.5361 27 18.3652Z"
fill="var(--primary-color)"
/>
</mask>
<g mask="url(#mask0_1413_1551)">
<path
d="M-4.673e-05 8.88887L3.73084 -1.91434L-8.00806 17.0473L-4.673e-05 8.88887ZM27 18.3652L26.4253 6.95109L27 18.3652ZM54 8.88887L61.2673 17.7127L50.2691 -1.91434L54 8.88887ZM-4.673e-05 8.88887C-8.00806 17.0473 -8.00469 17.0505 -8.00132 17.0538C-8.00018 17.055 -7.99675 17.0583 -7.9944 17.0607C-7.98963 17.0653 -7.98474 17.0701 -7.97966 17.075C-7.96949 17.0849 -7.95863 17.0955 -7.94707 17.1066C-7.92401 17.129 -7.89809 17.1539 -7.86944 17.1812C-7.8122 17.236 -7.74377 17.3005 -7.66436 17.3743C-7.50567 17.5218 -7.30269 17.7063 -7.05645 17.9221C-6.56467 18.3532 -5.89662 18.9125 -5.06089 19.5534C-3.39603 20.83 -1.02575 22.4605 1.98012 24.0457C7.97874 27.2091 16.7723 30.3226 27.5746 29.7793L26.4253 6.95109C20.7391 7.23699 16.0326 5.61231 12.6534 3.83024C10.9703 2.94267 9.68222 2.04866 8.86091 1.41888C8.45356 1.10653 8.17155 0.867278 8.0241 0.738027C7.95072 0.673671 7.91178 0.637576 7.90841 0.634492C7.90682 0.63298 7.91419 0.639805 7.93071 0.65557C7.93897 0.663455 7.94952 0.673589 7.96235 0.686039C7.96883 0.692262 7.97582 0.699075 7.98338 0.706471C7.98719 0.710167 7.99113 0.714014 7.99526 0.718014C7.99729 0.720008 8.00047 0.723119 8.00148 0.724116C8.00466 0.727265 8.00796 0.730446 -4.673e-05 8.88887ZM27.5746 29.7793C37.6904 29.2706 45.9416 26.3684 51.6602 23.6054C54.5296 22.2191 56.8064 20.8465 58.4186 19.7784C59.2265 19.2431 59.873 18.7805 60.3494 18.4257C60.5878 18.2482 60.7841 18.0971 60.9374 17.977C61.014 17.9169 61.0799 17.8645 61.1349 17.8203C61.1624 17.7981 61.1872 17.7781 61.2093 17.7602C61.2203 17.7512 61.2307 17.7427 61.2403 17.7348C61.2452 17.7308 61.2499 17.727 61.2544 17.7233C61.2566 17.7215 61.2598 17.7188 61.261 17.7179C61.2642 17.7153 61.2673 17.7127 54 8.88887C46.7326 0.0650536 46.7357 0.0625219 46.7387 0.0600241C46.7397 0.0592345 46.7427 0.0567658 46.7446 0.0551857C46.7485 0.0520238 46.7521 0.0489887 46.7557 0.0460799C46.7628 0.0402623 46.7694 0.0349487 46.7753 0.0301318C46.7871 0.0204986 46.7966 0.0128495 46.8037 0.00712562C46.818 -0.00431848 46.8228 -0.00808311 46.8184 -0.00463784C46.8096 0.00228345 46.764 0.0378652 46.6828 0.0983779C46.5199 0.219675 46.2165 0.439161 45.7812 0.727519C44.9072 1.30663 43.5257 2.14765 41.7061 3.02677C38.0469 4.79468 32.7981 6.63058 26.4253 6.95109L27.5746 29.7793ZM54 8.88887C50.2691 -1.91433 50.27 -1.91467 50.271 -1.91498C50.2712 -1.91506 50.272 -1.91535 50.2724 -1.9155C50.2733 -1.91581 50.274 -1.91602 50.2743 -1.91616C50.2752 -1.91643 50.275 -1.91636 50.2738 -1.91595C50.2714 -1.91515 50.2652 -1.91302 50.2552 -1.9096C50.2351 -1.90276 50.1999 -1.89078 50.1503 -1.874C50.0509 -1.84043 49.8938 -1.78773 49.6844 -1.71863C49.2652 -1.58031 48.6387 -1.377 47.8481 -1.13035C46.2609 -0.635237 44.0427 0.0249875 41.5325 0.6823C36.215 2.07471 30.6736 3.15796 27 3.15796V26.0151C33.8087 26.0151 41.7672 24.2495 47.3292 22.7931C50.2586 22.026 52.825 21.2618 54.6625 20.6886C55.5842 20.4011 56.33 20.1593 56.8551 19.986C57.1178 19.8993 57.3258 19.8296 57.4735 19.7797C57.5474 19.7548 57.6062 19.7348 57.6493 19.72C57.6709 19.7127 57.6885 19.7066 57.7021 19.7019C57.7089 19.6996 57.7147 19.6976 57.7195 19.696C57.7219 19.6952 57.7241 19.6944 57.726 19.6938C57.7269 19.6934 57.7281 19.693 57.7286 19.6929C57.7298 19.6924 57.7309 19.692 54 8.88887ZM27 3.15796C23.3263 3.15796 17.7849 2.07471 12.4674 0.6823C9.95717 0.0249875 7.73904 -0.635237 6.15184 -1.13035C5.36118 -1.377 4.73467 -1.58031 4.3155 -1.71863C4.10609 -1.78773 3.94899 -1.84043 3.84961 -1.874C3.79994 -1.89078 3.76474 -1.90276 3.74471 -1.9096C3.73469 -1.91302 3.72848 -1.91515 3.72613 -1.91595C3.72496 -1.91636 3.72476 -1.91643 3.72554 -1.91616C3.72593 -1.91602 3.72657 -1.91581 3.72745 -1.9155C3.72789 -1.91535 3.72874 -1.91506 3.72896 -1.91498C3.72987 -1.91467 3.73084 -1.91433 -4.673e-05 8.88887C-3.73093 19.692 -3.72983 19.6924 -3.72868 19.6929C-3.72821 19.693 -3.72698 19.6934 -3.72603 19.6938C-3.72415 19.6944 -3.72201 19.6952 -3.71961 19.696C-3.71482 19.6976 -3.70901 19.6996 -3.7022 19.7019C-3.68858 19.7066 -3.67095 19.7127 -3.6494 19.72C-3.60629 19.7348 -3.54745 19.7548 -3.47359 19.7797C-3.32589 19.8296 -3.11788 19.8993 -2.85516 19.986C-2.33008 20.1593 -1.58425 20.4011 -0.662589 20.6886C1.17485 21.2618 3.74125 22.026 6.67073 22.7931C12.2327 24.2495 20.1913 26.0151 27 26.0151V3.15796Z"
fill="var(--primary-color)"
/>
</g>
</svg>
<span>SAKAI</span>
</a>
</div>
<div class="layout-topbar-actions">
<div class="layout-config-menu">
<button type="button" class="layout-topbar-action" (click)="toggleDarkMode()">
<i
[ngClass]="{
'pi ': true,
'pi-moon': layoutService.isDarkTheme(),
'pi-sun': !layoutService.isDarkTheme(),
}"
></i>
</button>
<div class="relative">
<button
class="layout-topbar-action layout-topbar-action-highlight"
pStyleClass="@next"
enterFromClass="hidden"
enterActiveClass="animate-scalein"
leaveToClass="hidden"
leaveActiveClass="animate-fadeout"
[hideOnOutsideClick]="true"
>
<i class="pi pi-palette"></i>
</button>
<app-configurator />
</div>
</div>
<button
class="layout-topbar-menu-button layout-topbar-action"
pStyleClass="@next"
enterFromClass="hidden"
enterActiveClass="animate-scalein"
leaveToClass="hidden"
leaveActiveClass="animate-fadeout"
[hideOnOutsideClick]="true"
>
<i class="pi pi-ellipsis-v"></i>
</button>
<div class="layout-topbar-menu hidden lg:block">
<div class="layout-topbar-menu-content">
<button type="button" class="layout-topbar-action">
<i class="pi pi-calendar"></i>
<span>Calendar</span>
</button>
<button type="button" class="layout-topbar-action">
<i class="pi pi-inbox"></i>
<span>Messages</span>
</button>
<button type="button" class="layout-topbar-action">
<i class="pi pi-user"></i>
<span>Profile</span>
</button>
</div>
</div>
</div>
</div>`,
})
export class AppTopbar {
items!: MenuItem[];
constructor(public layoutService: LayoutService) {}
toggleDarkMode() {
this.layoutService.layoutConfig.update((state) => ({ ...state, darkTheme: !state.darkTheme }));
}
}
+23 -2
View File
@@ -1,4 +1,6 @@
import { Injectable, computed, effect, signal } from '@angular/core'; import { MENU_ITEMS } from '@/core/constants';
import { computed, effect, Injectable, signal } from '@angular/core';
import { MenuItem } from 'primeng/api';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
export interface layoutConfig { export interface layoutConfig {
@@ -15,6 +17,7 @@ interface LayoutState {
configSidebarVisible?: boolean; configSidebarVisible?: boolean;
staticMenuMobileActive?: boolean; staticMenuMobileActive?: boolean;
menuHoverActive?: boolean; menuHoverActive?: boolean;
isFixedContentSize?: boolean;
} }
interface MenuChangeEvent { interface MenuChangeEvent {
@@ -28,7 +31,7 @@ interface MenuChangeEvent {
export class LayoutService { export class LayoutService {
_config: layoutConfig = { _config: layoutConfig = {
preset: 'Aura', preset: 'Aura',
primary: 'violet', primary: 'blue',
surface: null, surface: null,
darkTheme: localStorage.getItem('isDarkTheme') === 'true', darkTheme: localStorage.getItem('isDarkTheme') === 'true',
menuMode: 'static', menuMode: 'static',
@@ -40,6 +43,7 @@ export class LayoutService {
configSidebarVisible: false, configSidebarVisible: false,
staticMenuMobileActive: false, staticMenuMobileActive: false,
menuHoverActive: false, menuHoverActive: false,
isFixedContentSize: true,
}; };
layoutConfig = signal<layoutConfig>(this._config); layoutConfig = signal<layoutConfig>(this._config);
@@ -54,6 +58,8 @@ export class LayoutService {
private resetSource = new Subject(); private resetSource = new Subject();
public menuItems = signal<MenuItem[]>(MENU_ITEMS);
menuSource$ = this.menuSource.asObservable(); menuSource$ = this.menuSource.asObservable();
resetSource$ = this.resetSource.asObservable(); resetSource$ = this.resetSource.asObservable();
@@ -76,11 +82,14 @@ export class LayoutService {
isOverlay = computed(() => this.layoutConfig().menuMode === 'overlay'); isOverlay = computed(() => this.layoutConfig().menuMode === 'overlay');
isFixedContentSize = computed(() => this.layoutState().isFixedContentSize);
transitionComplete = signal<boolean>(false); transitionComplete = signal<boolean>(false);
private initialized = false; private initialized = false;
constructor() { constructor() {
this.handleDarkModeTransition(this._config);
effect(() => { effect(() => {
const config = this.layoutConfig(); const config = this.layoutConfig();
if (config) { if (config) {
@@ -121,8 +130,16 @@ export class LayoutService {
.catch(() => {}); .catch(() => {});
} }
changeIsFixedContentSize(status: boolean) {
this.layoutState.update((prev) => ({
...prev,
isFixedContentSize: status,
}));
}
toggleDarkMode(config?: layoutConfig): void { toggleDarkMode(config?: layoutConfig): void {
const _config = config || this.layoutConfig(); const _config = config || this.layoutConfig();
localStorage.setItem('isDarkTheme', JSON.stringify(_config.darkTheme));
if (_config.darkTheme) { if (_config.darkTheme) {
document.documentElement.classList.add('app-dark'); document.documentElement.classList.add('app-dark');
} else { } else {
@@ -186,4 +203,8 @@ export class LayoutService {
reset() { reset() {
this.resetSource.next(true); this.resetSource.next(true);
} }
setMenuItems(items: MenuItem[]) {
this.menuItems.set(items);
}
} }
@@ -7,8 +7,8 @@
class="flex flex-col items-center justify-center px-12 py-10 h-auto bg-surface-card border border-surface-border rounded-lg shadow w-full max-w-md" class="flex flex-col items-center justify-center px-12 py-10 h-auto bg-surface-card border border-surface-border rounded-lg shadow w-full max-w-md"
> >
<div class="flex flex-col gap-4 text-center mb-10 items-center justify-center"> <div class="flex flex-col gap-4 text-center mb-10 items-center justify-center">
<img [src]="logo" alt="امتحانات شبه نهایی سنجش" class="w-20 h-auto" /> <img [src]="logo" alt="صندوق فروشگاهی" class="w-20 h-auto" />
<span class="text-lg font-bold"> به پنل امتحانات شبه نهایی سنجش خوش آمدید. </span> <span class="text-lg font-bold"> به پنل صندوق فروشگاهی خوش آمدید. </span>
</div> </div>
@if (activeStep() === "login") { @if (activeStep() === "login") {
@@ -0,0 +1,14 @@
<p-dialog header="فرم مشتری" [(visible)]="visible" [modal]="true" [style]="{ width: '600px' }" [closable]="true">
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="نام" [control]="form.controls.firstName" name="firstName" />
<app-input label="نام خانوادگی" [control]="form.controls.lastName" name="lastName" />
<app-input label="ایمیل" [control]="form.controls.email" name="email" type="email" />
<app-input label="شماره موبایل" [control]="form.controls.mobileNumber" name="mobileNumber" type="mobile" />
<app-input label="آدرس" [control]="form.controls.address" name="address" />
<app-input label="شهر" [control]="form.controls.city" name="city" />
<app-input label="استان" [control]="form.controls.state" name="state" />
<app-input label="کشور" [control]="form.controls.country" name="country" />
<app-input label="فعال" [control]="form.controls.isActive" name="isActive" type="switch" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
</form>
</p-dialog>
@@ -0,0 +1,74 @@
import { InputComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Dialog } from 'primeng/dialog';
import { ICustomerRequest, ICustomerResponse } from '../models';
import { CustomersService } from '../services/main.service';
@Component({
selector: 'customer-form',
templateUrl: './form.component.html',
imports: [ReactiveFormsModule, Dialog, InputComponent, FormFooterActionsComponent],
})
export class CustomerFormComponent {
@Input() initialValues?: ICustomerResponse;
@Input()
set visible(v: boolean) {
this.visibleSignal.set(!!v);
}
get visible() {
return this.visibleSignal();
}
private visibleSignal = signal(false);
@Output() visibleChange = new EventEmitter<boolean>();
@Output() onSubmit = new EventEmitter<ICustomerResponse>();
private fb = inject(FormBuilder);
constructor(private service: CustomersService) {
effect(() => {
const v = this.visibleSignal();
if (!v) this.form.reset();
});
}
form = this.fb.group({
firstName: [this.initialValues?.firstName || null, [Validators.required]],
lastName: [this.initialValues?.lastName || null, [Validators.required]],
email: [this.initialValues?.email || null, [Validators.required, Validators.email]],
mobileNumber: [this.initialValues?.mobileNumber || null, [Validators.required]],
address: [this.initialValues?.address || null, [Validators.required]],
city: [this.initialValues?.city || null, [Validators.required]],
state: [this.initialValues?.state || null, [Validators.required]],
country: [this.initialValues?.country || null, [Validators.required]],
isActive: [this.initialValues?.isActive || false, [Validators.required]],
});
submitLoading = signal(false);
submit() {
this.form.markAllAsTouched();
if (this.form.valid) {
this.form.disable();
this.submitLoading.set(true);
this.service.create(this.form.value as ICustomerRequest).subscribe({
next: (res) => {
this.submitLoading.set(false);
this.form.enable();
this.form.reset();
this.onSubmit.emit(res);
},
error: () => {
this.submitLoading.set(false);
this.form.enable();
},
});
}
}
close() {
this.visibleChange.emit(false);
}
}
@@ -0,0 +1,6 @@
const baseUrl = '/api/v1/customers';
export const CUSTOMERS_API_ROUTES = {
list: () => `${baseUrl}`,
single: (customerId: string) => `${baseUrl}/${customerId}`,
};
@@ -0,0 +1,2 @@
export * from './apiRoutes';
export * from './routes';
@@ -0,0 +1,25 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export type TCustomersRouteNames = 'customers' | 'customer';
export const customersNamedRoutes: NamedRoutes<TCustomersRouteNames> = {
customers: {
path: 'customers',
loadComponent: () => import('../../views/list.component').then((m) => m.CustomersComponent),
meta: {
title: 'مشتریان',
pagePath: () => '/customers',
},
},
customer: {
path: 'customers/:customerId',
loadComponent: () => import('../../views/single.component').then((m) => m.CustomerComponent),
meta: {
title: 'مشتری',
pagePath: () => '/customers/:customerId',
},
},
};
export const CUSTOMERS_ROUTES: Routes = Object.values(customersNamedRoutes);
@@ -0,0 +1 @@
export * from './io';
+29
View File
@@ -0,0 +1,29 @@
export interface ICustomerRawResponse {
id: number;
firstName: string;
lastName: string;
email: string;
mobileNumber: string;
address: string;
city: string;
state: string;
country: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
deletedAt: Date;
}
export interface ICustomerResponse extends ICustomerRawResponse {}
export interface ICustomerRequest {
firstName: string;
lastName: string;
email: string;
mobileNumber: string;
address: string;
city: string;
state: string;
country: string;
isActive: boolean;
}
@@ -0,0 +1,24 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { CUSTOMERS_API_ROUTES } from '../constants';
import { ICustomerRawResponse, ICustomerRequest, ICustomerResponse } from '../models';
@Injectable({ providedIn: 'root' })
export class CustomersService {
constructor(private http: HttpClient) {}
private apiRoutes = CUSTOMERS_API_ROUTES;
getAll(): Observable<ICustomerResponse[]> {
return this.http.get<ICustomerRawResponse[]>(this.apiRoutes.list());
}
getSingle(customerId: string): Observable<ICustomerResponse> {
return this.http.get<ICustomerRawResponse>(this.apiRoutes.single(customerId));
}
create(data: ICustomerRequest): Observable<ICustomerResponse> {
return this.http.post<ICustomerRawResponse>(this.apiRoutes.list(), data);
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './list.component';
export * from './single.component';
@@ -0,0 +1,12 @@
<app-page-data-list
[pageTitle]="'مدیریت مشتریان'"
[addNewCtaLabel]="'افزودن مشتری جدید'"
[columns]="columns"
[showAdd]="true"
emptyPlaceholderTitle="مشتری‌ای یافت نشد"
emptyPlaceholderDescription="برای افزودن مشتری جدید، روی دکمهٔ بالا کلیک کنید."
[items]="items()"
[loading]="loading()"
[showDetails]="true"
>
</app-page-data-list>
@@ -0,0 +1,45 @@
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal } from '@angular/core';
import { ICustomerResponse } from '../models';
import { CustomersService } from '../services/main.service';
@Component({
selector: 'app-customers',
templateUrl: './list.component.html',
imports: [PageDataListComponent],
})
export class CustomersComponent {
constructor(private customerService: CustomersService) {
this.getData();
}
columns = [
{ field: 'id', header: 'شناسه' },
{ field: 'firstName', header: 'نام' },
{ field: 'lastName', header: 'نام خانوادگی' },
{ field: 'email', header: 'ایمیل' },
{ field: 'mobileNumber', header: 'شماره موبایل' },
{ field: 'address', header: 'آدرس' },
{ field: 'city', header: 'شهر' },
{ field: 'state', header: 'استان' },
{ field: 'country', header: 'کشور' },
{ field: 'isActive', header: 'فعال' },
{ field: 'createdAt', header: 'تاریخ ایجاد' },
{ field: 'updatedAt', header: 'تاریخ به‌روزرسانی' },
{ field: 'actions' },
] as IColumn[];
loading = signal(false);
items = signal<ICustomerResponse[]>([]);
getData() {
this.loading.set(true);
this.customerService.getAll().subscribe((res) => {
this.loading.set(false);
this.items.set(res);
});
}
}
@@ -0,0 +1 @@
<div class=""></div>
@@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-customer',
templateUrl: './single.component.html',
})
export class CustomerComponent {
constructor() {}
}
@@ -0,0 +1,8 @@
<p-dialog header="فرم انبار" [(visible)]="visible" [modal]="true" [style]="{ width: '500px' }" [closable]="true">
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="نام" [control]="form.controls.name" name="name" />
<app-input label="موقعیت" [control]="form.controls.location" name="location" />
<app-input label="فعال" [control]="form.controls.isActive" name="isActive" type="switch" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
</form>
</p-dialog>
@@ -0,0 +1,68 @@
import { InputComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Dialog } from 'primeng/dialog';
import { IInventoryRequest, IInventoryResponse } from '../models';
import { InventoriesService } from '../services/main.service';
@Component({
selector: 'inventory-form',
templateUrl: './form.component.html',
imports: [ReactiveFormsModule, Dialog, InputComponent, FormFooterActionsComponent],
})
export class InventoryFormComponent {
@Input() initialValues?: IInventoryResponse;
@Input()
set visible(v: boolean) {
this.visibleSignal.set(!!v);
}
get visible() {
return this.visibleSignal();
}
private visibleSignal = signal(false);
@Output() visibleChange = new EventEmitter<boolean>();
@Output() onSubmit = new EventEmitter<IInventoryResponse>();
private fb = inject(FormBuilder);
constructor(private service: InventoriesService) {
effect(() => {
const v = this.visibleSignal();
if (!v) this.form.reset();
});
}
form = this.fb.group({
name: [this.initialValues?.name || null, [Validators.required]],
location: [this.initialValues?.location || null, [Validators.required]],
isActive: [this.initialValues?.isActive || false, [Validators.required]],
});
submitLoading = signal(false);
submit() {
this.form.markAllAsTouched();
if (this.form.valid) {
this.form.disable();
this.submitLoading.set(true);
this.service.create(this.form.value as IInventoryRequest).subscribe({
next: (res) => {
this.submitLoading.set(false);
this.form.enable();
this.form.reset();
this.onSubmit.emit(res);
},
error: () => {
this.submitLoading.set(false);
this.form.enable();
},
});
}
}
close() {
this.visibleChange.emit(false);
}
}
@@ -0,0 +1,6 @@
const baseUrl = '/api/v1/inventories';
export const INVENTORIES_API_ROUTES = {
list: () => `${baseUrl}`,
single: (inventoryId: string) => `${baseUrl}/${inventoryId}`,
};
@@ -0,0 +1,2 @@
export * from './apiRoutes';
export * from './routes';
@@ -0,0 +1,25 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export type TInventoriesRouteNames = 'inventories' | 'inventory';
export const inventoriesNamedRoutes: NamedRoutes<TInventoriesRouteNames> = {
inventories: {
path: 'inventories',
loadComponent: () => import('../../views/list.component').then((m) => m.InventoriesComponent),
meta: {
title: 'انبارها',
pagePath: () => '/inventories',
},
},
inventory: {
path: 'inventories/:inventoryId',
loadComponent: () => import('../../views/single.component').then((m) => m.InventoryComponent),
meta: {
title: 'انبار',
pagePath: () => '/inventories/:inventoryId',
},
},
};
export const INVENTORIES_ROUTES: Routes = Object.values(inventoriesNamedRoutes);
@@ -0,0 +1 @@
export * from './io';
+14
View File
@@ -0,0 +1,14 @@
export interface IInventoryRawResponse {
id: number;
name: string;
location: string;
isActive: boolean;
}
export interface IInventoryResponse extends IInventoryRawResponse {}
export interface IInventoryRequest {
name: string;
location: string;
isActive: boolean;
}
@@ -0,0 +1,24 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { INVENTORIES_API_ROUTES } from '../constants';
import { IInventoryRawResponse, IInventoryRequest, IInventoryResponse } from '../models';
@Injectable({ providedIn: 'root' })
export class InventoriesService {
constructor(private http: HttpClient) {}
private apiRoutes = INVENTORIES_API_ROUTES;
getAll(): Observable<IInventoryResponse[]> {
return this.http.get<IInventoryRawResponse[]>(this.apiRoutes.list());
}
getSingle(inventoryId: string): Observable<IInventoryResponse> {
return this.http.get<IInventoryRawResponse>(this.apiRoutes.single(inventoryId));
}
create(data: IInventoryRequest): Observable<IInventoryResponse> {
return this.http.post<IInventoryRawResponse>(this.apiRoutes.list(), data);
}
}
@@ -0,0 +1,2 @@
export * from './list.component';
export * from './single.component';
@@ -0,0 +1,12 @@
<app-page-data-list
[pageTitle]="'مدیریت انبارها'"
[addNewCtaLabel]="'افزودن انبار جدید'"
[columns]="columns"
[showAdd]="true"
emptyPlaceholderTitle="انباری یافت نشد"
emptyPlaceholderDescription="برای افزودن انبار جدید، روی دکمهٔ بالا کلیک کنید."
[items]="items()"
[loading]="loading()"
[showDetails]="true"
>
</app-page-data-list>
@@ -0,0 +1,37 @@
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal } from '@angular/core';
import { IInventoryResponse } from '../models';
import { InventoriesService } from '../services/main.service';
@Component({
selector: 'app-inventories',
templateUrl: './list.component.html',
imports: [PageDataListComponent],
})
export class InventoriesComponent {
constructor(private inventoryService: InventoriesService) {
this.getData();
}
columns = [
{ field: 'id', header: 'شناسه' },
{ field: 'name', header: 'نام' },
{ field: 'location', header: 'موقعیت' },
{ field: 'isActive', header: 'فعال' },
{ field: 'actions' },
] as IColumn[];
loading = signal(false);
items = signal<IInventoryResponse[]>([]);
getData() {
this.loading.set(true);
this.inventoryService.getAll().subscribe((res) => {
this.loading.set(false);
this.items.set(res);
});
}
}
@@ -0,0 +1 @@
<div class=""></div>
@@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-inventory',
templateUrl: './single.component.html',
})
export class InventoryComponent {
constructor() {}
}
@@ -0,0 +1,8 @@
<p-dialog header="فرم برند محصول" [(visible)]="visible" [modal]="true" [style]="{ width: '500px' }" [closable]="true">
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="نام" [control]="form.controls.name" name="name" />
<app-input label="توضیحات" [control]="form.controls.description" name="description" />
<app-input label="URL تصویر" [control]="form.controls.imageUrl" name="imageUrl" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
</form>
</p-dialog>
@@ -0,0 +1,68 @@
import { InputComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Dialog } from 'primeng/dialog';
import { IProductBrandRequest, IProductBrandResponse } from '../models';
import { ProductBrandsService } from '../services/main.service';
@Component({
selector: 'product-brand-form',
templateUrl: './form.component.html',
imports: [ReactiveFormsModule, Dialog, InputComponent, FormFooterActionsComponent],
})
export class ProductBrandFormComponent {
@Input() initialValues?: IProductBrandResponse;
@Input()
set visible(v: boolean) {
this.visibleSignal.set(!!v);
}
get visible() {
return this.visibleSignal();
}
private visibleSignal = signal(false);
@Output() visibleChange = new EventEmitter<boolean>();
@Output() onSubmit = new EventEmitter<IProductBrandResponse>();
private fb = inject(FormBuilder);
constructor(private service: ProductBrandsService) {
effect(() => {
const v = this.visibleSignal();
if (!v) this.form.reset();
});
}
form = this.fb.group({
name: [this.initialValues?.name || null, [Validators.required]],
description: [this.initialValues?.description || null, [Validators.required]],
imageUrl: [this.initialValues?.imageUrl || null, [Validators.required]],
});
submitLoading = signal(false);
submit() {
this.form.markAllAsTouched();
if (this.form.valid) {
this.form.disable();
this.submitLoading.set(true);
this.service.create(this.form.value as IProductBrandRequest).subscribe({
next: (res) => {
this.submitLoading.set(false);
this.form.enable();
this.form.reset();
this.onSubmit.emit(res);
},
error: () => {
this.submitLoading.set(false);
this.form.enable();
},
});
}
}
close() {
this.visibleChange.emit(false);
}
}
@@ -0,0 +1,6 @@
const baseUrl = '/api/v1/product-brands';
export const PRODUCT_BRANDS_API_ROUTES = {
list: () => `${baseUrl}`,
single: (brandId: string) => `${baseUrl}/${brandId}`,
};
@@ -0,0 +1,2 @@
export * from './apiRoutes';
export * from './routes';
@@ -0,0 +1,26 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export type TProductBrandsRouteNames = 'productBrands' | 'productBrand';
export const productBrandsNamedRoutes: NamedRoutes<TProductBrandsRouteNames> = {
productBrands: {
path: 'product-brands',
loadComponent: () => import('../../views/list.component').then((m) => m.ProductBrandsComponent),
meta: {
title: 'برندهای محصول',
pagePath: () => '/product-brands',
},
},
productBrand: {
path: 'product-brands/:brandId',
loadComponent: () =>
import('../../views/single.component').then((m) => m.ProductBrandComponent),
meta: {
title: 'برند محصول',
pagePath: () => '/product-brands/:brandId',
},
},
};
export const PRODUCT_BRANDS_ROUTES: Routes = Object.values(productBrandsNamedRoutes);
@@ -0,0 +1 @@
export * from './io';
+17
View File
@@ -0,0 +1,17 @@
export interface IProductBrandRawResponse {
id: number;
name: string;
description: string;
imageUrl: string;
createdAt: Date;
updatedAt: Date;
deletedAt: Date;
}
export interface IProductBrandResponse extends IProductBrandRawResponse {}
export interface IProductBrandRequest {
name: string;
description: string;
imageUrl: string;
}
@@ -0,0 +1,24 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { PRODUCT_BRANDS_API_ROUTES } from '../constants';
import { IProductBrandRawResponse, IProductBrandRequest, IProductBrandResponse } from '../models';
@Injectable({ providedIn: 'root' })
export class ProductBrandsService {
constructor(private http: HttpClient) {}
private apiRoutes = PRODUCT_BRANDS_API_ROUTES;
getAll(): Observable<IProductBrandResponse[]> {
return this.http.get<IProductBrandRawResponse[]>(this.apiRoutes.list());
}
getSingle(brandId: string): Observable<IProductBrandResponse> {
return this.http.get<IProductBrandRawResponse>(this.apiRoutes.single(brandId));
}
create(data: IProductBrandRequest): Observable<IProductBrandResponse> {
return this.http.post<IProductBrandRawResponse>(this.apiRoutes.list(), data);
}
}
@@ -0,0 +1,2 @@
export * from './list.component';
export * from './single.component';
@@ -0,0 +1,12 @@
<app-page-data-list
[pageTitle]="'مدیریت برندهای محصول'"
[addNewCtaLabel]="'افزودن برند جدید'"
[columns]="columns"
[showAdd]="true"
emptyPlaceholderTitle="برندی یافت نشد"
emptyPlaceholderDescription="برای افزودن برند جدید، روی دکمهٔ بالا کلیک کنید."
[items]="items()"
[loading]="loading()"
[showDetails]="true"
>
</app-page-data-list>
@@ -0,0 +1,39 @@
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal } from '@angular/core';
import { IProductBrandResponse } from '../models';
import { ProductBrandsService } from '../services/main.service';
@Component({
selector: 'app-product-brands',
templateUrl: './list.component.html',
imports: [PageDataListComponent],
})
export class ProductBrandsComponent {
constructor(private productBrandService: ProductBrandsService) {
this.getData();
}
columns = [
{ field: 'id', header: 'شناسه' },
{ field: 'name', header: 'نام' },
{ field: 'description', header: 'توضیحات' },
{ field: 'imageUrl', header: 'تصویر' },
{ field: 'createdAt', header: 'تاریخ ایجاد' },
{ field: 'updatedAt', header: 'تاریخ به‌روزرسانی' },
{ field: 'actions' },
] as IColumn[];
loading = signal(false);
items = signal<IProductBrandResponse[]>([]);
getData() {
this.loading.set(true);
this.productBrandService.getAll().subscribe((res) => {
this.loading.set(false);
this.items.set(res);
});
}
}
@@ -0,0 +1 @@
<div class=""></div>
@@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-product-brand',
templateUrl: './single.component.html',
})
export class ProductBrandComponent {
constructor() {}
}
@@ -0,0 +1,14 @@
<p-dialog
header="فرم دسته‌بندی محصول"
[(visible)]="visible"
[modal]="true"
[style]="{ width: '500px' }"
[closable]="true"
>
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="نام" [control]="form.controls.name" name="name" />
<app-input label="توضیحات" [control]="form.controls.description" name="description" />
<app-input label="URL تصویر" [control]="form.controls.imageUrl" name="imageUrl" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
</form>
</p-dialog>
@@ -0,0 +1,68 @@
import { InputComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Dialog } from 'primeng/dialog';
import { IProductCategoryRequest, IProductCategoryResponse } from '../models';
import { ProductCategoriesService } from '../services/main.service';
@Component({
selector: 'product-category-form',
templateUrl: './form.component.html',
imports: [ReactiveFormsModule, Dialog, InputComponent, FormFooterActionsComponent],
})
export class ProductCategoryFormComponent {
@Input() initialValues?: IProductCategoryResponse;
@Input()
set visible(v: boolean) {
this.visibleSignal.set(!!v);
}
get visible() {
return this.visibleSignal();
}
private visibleSignal = signal(false);
@Output() visibleChange = new EventEmitter<boolean>();
@Output() onSubmit = new EventEmitter<IProductCategoryResponse>();
private fb = inject(FormBuilder);
constructor(private service: ProductCategoriesService) {
effect(() => {
const v = this.visibleSignal();
if (!v) this.form.reset();
});
}
form = this.fb.group({
name: [this.initialValues?.name || null, [Validators.required]],
description: [this.initialValues?.description || null, [Validators.required]],
imageUrl: [this.initialValues?.imageUrl || null, [Validators.required]],
});
submitLoading = signal(false);
submit() {
this.form.markAllAsTouched();
if (this.form.valid) {
this.form.disable();
this.submitLoading.set(true);
this.service.create(this.form.value as IProductCategoryRequest).subscribe({
next: (res) => {
this.submitLoading.set(false);
this.form.enable();
this.form.reset();
this.onSubmit.emit(res);
},
error: () => {
this.submitLoading.set(false);
this.form.enable();
},
});
}
}
close() {
this.visibleChange.emit(false);
}
}
@@ -0,0 +1,6 @@
const baseUrl = '/api/v1/product-categories';
export const PRODUCT_CATEGORIES_API_ROUTES = {
list: () => `${baseUrl}`,
single: (categoryId: string) => `${baseUrl}/${categoryId}`,
};
@@ -0,0 +1,2 @@
export * from './apiRoutes';
export * from './routes';
@@ -0,0 +1,27 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export type TProductCategoriesRouteNames = 'productCategories' | 'productCategory';
export const productCategoriesNamedRoutes: NamedRoutes<TProductCategoriesRouteNames> = {
productCategories: {
path: 'product-categories',
loadComponent: () =>
import('../../views/list.component').then((m) => m.ProductCategoriesComponent),
meta: {
title: 'دسته‌بندی محصولات',
pagePath: () => '/product-categories',
},
},
productCategory: {
path: 'product-categories/:categoryId',
loadComponent: () =>
import('../../views/single.component').then((m) => m.ProductCategoryComponent),
meta: {
title: 'دسته‌بندی محصول',
pagePath: () => '/product-categories/:categoryId',
},
},
};
export const PRODUCT_CATEGORIES_ROUTES: Routes = Object.values(productCategoriesNamedRoutes);
@@ -0,0 +1 @@
export * from './io';
+15
View File
@@ -0,0 +1,15 @@
export interface IProductCategoryRawResponse {
id: number;
name: string;
description: string;
imageUrl: string;
createdAt: Date;
}
export interface IProductCategoryResponse extends IProductCategoryRawResponse {}
export interface IProductCategoryRequest {
name: string;
description: string;
imageUrl: string;
}
@@ -0,0 +1,28 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { PRODUCT_CATEGORIES_API_ROUTES } from '../constants';
import {
IProductCategoryRawResponse,
IProductCategoryRequest,
IProductCategoryResponse,
} from '../models';
@Injectable({ providedIn: 'root' })
export class ProductCategoriesService {
constructor(private http: HttpClient) {}
private apiRoutes = PRODUCT_CATEGORIES_API_ROUTES;
getAll(): Observable<IProductCategoryResponse[]> {
return this.http.get<IProductCategoryRawResponse[]>(this.apiRoutes.list());
}
getSingle(categoryId: string): Observable<IProductCategoryResponse> {
return this.http.get<IProductCategoryRawResponse>(this.apiRoutes.single(categoryId));
}
create(data: IProductCategoryRequest): Observable<IProductCategoryResponse> {
return this.http.post<IProductCategoryRawResponse>(this.apiRoutes.list(), data);
}
}
@@ -0,0 +1,2 @@
export * from './list.component';
export * from './single.component';
@@ -0,0 +1,12 @@
<app-page-data-list
[pageTitle]="'مدیریت دسته‌بندی محصولات'"
[addNewCtaLabel]="'افزودن دسته‌بندی جدید'"
[columns]="columns"
[showAdd]="true"
emptyPlaceholderTitle="دسته‌بندی یافت نشد"
emptyPlaceholderDescription="برای افزودن دسته‌بندی جدید، روی دکمهٔ بالا کلیک کنید."
[items]="items()"
[loading]="loading()"
[showDetails]="true"
>
</app-page-data-list>
@@ -0,0 +1,38 @@
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal } from '@angular/core';
import { IProductCategoryResponse } from '../models';
import { ProductCategoriesService } from '../services/main.service';
@Component({
selector: 'app-product-categories',
templateUrl: './list.component.html',
imports: [PageDataListComponent],
})
export class ProductCategoriesComponent {
constructor(private productCategoryService: ProductCategoriesService) {
this.getData();
}
columns = [
{ field: 'id', header: 'شناسه' },
{ field: 'name', header: 'نام' },
{ field: 'description', header: 'توضیحات' },
{ field: 'imageUrl', header: 'تصویر' },
{ field: 'createdAt', header: 'تاریخ ایجاد' },
{ field: 'actions' },
] as IColumn[];
loading = signal(false);
items = signal<IProductCategoryResponse[]>([]);
getData() {
this.loading.set(true);
this.productCategoryService.getAll().subscribe((res) => {
this.loading.set(false);
this.items.set(res);
});
}
}
@@ -0,0 +1 @@
<div class=""></div>
@@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-product-category',
templateUrl: './single.component.html',
})
export class ProductCategoryComponent {
constructor() {}
}
@@ -0,0 +1,8 @@
<p-dialog header="فرم فروشگاه" [(visible)]="visible" [modal]="true" [style]="{ width: '500px' }" [closable]="true">
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="نام" [control]="form.controls.name" name="name" />
<app-input label="موقعیت" [control]="form.controls.location" name="location" />
<app-input label="فعال" [control]="form.controls.isActive" name="isActive" type="switch" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
</form>
</p-dialog>
@@ -0,0 +1,68 @@
import { InputComponent } from '@/shared/components';
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Dialog } from 'primeng/dialog';
import { IStoreRequest, IStoreResponse } from '../models';
import { StoresService } from '../services/main.service';
@Component({
selector: 'store-form',
templateUrl: './form.component.html',
imports: [ReactiveFormsModule, Dialog, InputComponent, FormFooterActionsComponent],
})
export class StoreFormComponent {
@Input() initialValues?: IStoreResponse;
@Input()
set visible(v: boolean) {
this.visibleSignal.set(!!v);
}
get visible() {
return this.visibleSignal();
}
private visibleSignal = signal(false);
@Output() visibleChange = new EventEmitter<boolean>();
@Output() onSubmit = new EventEmitter<IStoreResponse>();
private fb = inject(FormBuilder);
constructor(private service: StoresService) {
effect(() => {
const v = this.visibleSignal();
if (!v) this.form.reset();
});
}
form = this.fb.group({
name: [this.initialValues?.name || null, [Validators.required]],
location: [this.initialValues?.location || null, [Validators.required]],
isActive: [this.initialValues?.isActive || false, [Validators.required]],
});
submitLoading = signal(false);
submit() {
this.form.markAllAsTouched();
if (this.form.valid) {
this.form.disable();
this.submitLoading.set(true);
this.service.create(this.form.value as IStoreRequest).subscribe({
next: (res) => {
this.submitLoading.set(false);
this.form.enable();
this.form.reset();
this.onSubmit.emit(res);
},
error: () => {
this.submitLoading.set(false);
this.form.enable();
},
});
}
}
close() {
this.visibleChange.emit(false);
}
}
@@ -0,0 +1,6 @@
const baseUrl = '/api/v1/stores';
export const STORES_API_ROUTES = {
list: () => `${baseUrl}`,
single: (storeId: string) => `${baseUrl}/${storeId}`,
};
@@ -0,0 +1,2 @@
export * from './apiRoutes';
export * from './routes';
@@ -0,0 +1,25 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export type TStoresRouteNames = 'stores' | 'store';
export const storesNamedRoutes: NamedRoutes<TStoresRouteNames> = {
stores: {
path: 'stores',
loadComponent: () => import('../../views/list.component').then((m) => m.StoresComponent),
meta: {
title: 'فروشگاه‌ها',
pagePath: () => '/stores',
},
},
store: {
path: 'stores/:storeId',
loadComponent: () => import('../../views/single.component').then((m) => m.StoreComponent),
meta: {
title: 'فروشگاه',
pagePath: () => '/stores/:storeId',
},
},
};
export const STORES_ROUTES: Routes = Object.values(storesNamedRoutes);
+1
View File
@@ -0,0 +1 @@
export * from './io';
+16
View File
@@ -0,0 +1,16 @@
export interface IStoreRawResponse {
id: number;
name: string;
location: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
export interface IStoreResponse extends IStoreRawResponse {}
export interface IStoreRequest {
name: string;
location: string;
isActive: boolean;
}
@@ -0,0 +1,24 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { STORES_API_ROUTES } from '../constants';
import { IStoreRawResponse, IStoreRequest, IStoreResponse } from '../models';
@Injectable({ providedIn: 'root' })
export class StoresService {
constructor(private http: HttpClient) {}
private apiRoutes = STORES_API_ROUTES;
getAll(): Observable<IStoreResponse[]> {
return this.http.get<IStoreRawResponse[]>(this.apiRoutes.list());
}
getSingle(storeId: string): Observable<IStoreResponse> {
return this.http.get<IStoreRawResponse>(this.apiRoutes.single(storeId));
}
create(data: IStoreRequest): Observable<IStoreResponse> {
return this.http.post<IStoreRawResponse>(this.apiRoutes.list(), data);
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './list.component';
export * from './single.component';
@@ -0,0 +1,12 @@
<app-page-data-list
[pageTitle]="'مدیریت فروشگاه‌ها'"
[addNewCtaLabel]="'افزودن فروشگاه جدید'"
[columns]="columns"
[showAdd]="true"
emptyPlaceholderTitle="فروشگاهی یافت نشد"
emptyPlaceholderDescription="برای افزودن فروشگاه جدید، روی دکمهٔ بالا کلیک کنید."
[items]="items()"
[loading]="loading()"
[showDetails]="true"
>
</app-page-data-list>
@@ -0,0 +1,39 @@
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal } from '@angular/core';
import { IStoreResponse } from '../models';
import { StoresService } from '../services/main.service';
@Component({
selector: 'app-stores',
templateUrl: './list.component.html',
imports: [PageDataListComponent],
})
export class StoresComponent {
constructor(private storeService: StoresService) {
this.getData();
}
columns = [
{ field: 'id', header: 'شناسه' },
{ field: 'name', header: 'نام' },
{ field: 'location', header: 'موقعیت' },
{ field: 'isActive', header: 'فعال' },
{ field: 'createdAt', header: 'تاریخ ایجاد' },
{ field: 'updatedAt', header: 'تاریخ به‌روزرسانی' },
{ field: 'actions' },
] as IColumn[];
loading = signal(false);
items = signal<IStoreResponse[]>([]);
getData() {
this.loading.set(true);
this.storeService.getAll().subscribe((res) => {
this.loading.set(false);
this.items.set(res);
});
}
}

Some files were not shown because too many files have changed in this diff Show More