WIP
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
<!-- Start of FTR-7 -->
|
||||
<footer class="!k-bg-primary k-color-white k-bg-light k-py-6 k-px-2 k-px-sm-4.5 k-px-md-6 k-px-lg-4 k-px-xl-7.5">
|
||||
<p class="!k-mb-0">Copyright © {{ currentYear }} RBJ Software, Inc. All rights reserved.</p>
|
||||
</footer>
|
||||
<!-- End of FTR-7 -->
|
||||
@@ -0,0 +1,3 @@
|
||||
footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss']
|
||||
})
|
||||
export class FooterComponent {
|
||||
public currentYear = new Date().getFullYear();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<!-- Start of TPNAV-1 -->
|
||||
<header>
|
||||
<kendo-appbar positionMode='sticky' themeColor="inherit" class='k-bg-surface-alt' [style.z-index]="10000">
|
||||
<kendo-appbar-section class="k-flex-basis-0 k-flex-grow k-gap-2">
|
||||
<button kendoButton [svgIcon]="menuIcon" fillMode="clear" title="Menu" (click)="onMenuClick()"></button>
|
||||
<a href="#" class="k-d-none k-d-sm-flex logo-link">
|
||||
<img src="assets/rbj-logo.svg" class="k-h-8" alt="RBJ RBJ Identity logo" />
|
||||
<span class="logo-text">RBJ Identity Portal</span>
|
||||
</a>
|
||||
<a href="#" class="k-d-flex k-d-sm-none">
|
||||
<img src="assets/rbj-logo.svg" class="k-h-8" alt="RBJ RBJ Identity compact logo" />
|
||||
</a>
|
||||
</kendo-appbar-section>
|
||||
<kendo-appbar-section class="k-flex-basis-0 k-flex-grow k-justify-content-center">
|
||||
<div class="k-d-flex k-d-md-none">
|
||||
<button kendoButton [svgIcon]="searchIcon" fillMode="clear" title="Search"></button>
|
||||
</div>
|
||||
<div class="k-d-none k-d-md-flex search-box-wrapper">
|
||||
<kendo-textbox class="search-box" placeholder="Input value" fillMode="flat">
|
||||
<ng-template kendoTextBoxPrefixTemplate>
|
||||
<kendo-svgicon [icon]="searchIcon"></kendo-svgicon>
|
||||
<kendo-textbox-separator></kendo-textbox-separator>
|
||||
</ng-template>
|
||||
</kendo-textbox>
|
||||
</div>
|
||||
</kendo-appbar-section>
|
||||
<kendo-appbar-section class="k-flex-basis-0 k-flex-grow k-justify-content-end k-gap-1.5">
|
||||
<kendo-badge-container>
|
||||
<button kendoButton [svgIcon]="bellIcon" fillMode="clear" title="Notifications"></button>
|
||||
<kendo-badge rounded="medium" position="inside" [align]="badgeAlign" themeColor="error"></kendo-badge>
|
||||
</kendo-badge-container>
|
||||
<span class="k-appbar-separator k-color-border k-d-none k-d-sm-inline"></span>
|
||||
<kendo-dropdownbutton [data]="userMenuItems" fillMode="clear" [svgIcon]="userIcon" [arrowIcon]="true"
|
||||
(itemClick)="onUserMenuClick($event)">
|
||||
<span class="k-d-none k-d-sm-inline">
|
||||
{{ isAuthenticated ? (getDisplayName() || currentUser?.email || 'User') : 'Sign In' }}
|
||||
</span>
|
||||
</kendo-dropdownbutton>
|
||||
</kendo-appbar-section>
|
||||
</kendo-appbar>
|
||||
</header>
|
||||
<!-- End of TPNAV-1 -->
|
||||
@@ -0,0 +1,48 @@
|
||||
/* Logo styling */
|
||||
.logo-link {
|
||||
//display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
background: linear-gradient(135deg, #0066cc 0%, #0052a3 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
white-space: nowrap;
|
||||
|
||||
// Fallback for browsers that don't support background-clip
|
||||
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
|
||||
color: #0066cc;
|
||||
background: none;
|
||||
-webkit-text-fill-color: initial;
|
||||
}
|
||||
}
|
||||
|
||||
/* Search box responsive styling */
|
||||
.search-box-wrapper {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
@media (max-width: 767px) {
|
||||
/* Optimize spacing on mobile */
|
||||
kendo-appbar {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
import { AppBarModule } from '@progress/kendo-angular-navigation';
|
||||
import { ButtonsModule } from '@progress/kendo-angular-buttons';
|
||||
import { IndicatorsModule } from '@progress/kendo-angular-indicators';
|
||||
import { InputsModule } from '@progress/kendo-angular-inputs';
|
||||
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
|
||||
import { IconsModule } from '@progress/kendo-angular-icons';
|
||||
import { SVGIcon, bellIcon, menuIcon, searchIcon, userIcon, logoutIcon } from '@progress/kendo-svg-icons';
|
||||
import { LayoutService } from '../services/layout.service';
|
||||
import { AuthService, User } from '../../shared/services/auth.service';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
AppBarModule,
|
||||
ButtonsModule,
|
||||
IndicatorsModule,
|
||||
InputsModule,
|
||||
IconsModule,
|
||||
DropDownsModule
|
||||
],
|
||||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.scss']
|
||||
})
|
||||
export class HeaderComponent implements OnInit, OnDestroy {
|
||||
public menuIcon: SVGIcon = menuIcon;
|
||||
public searchIcon: SVGIcon = searchIcon;
|
||||
public bellIcon: SVGIcon = bellIcon;
|
||||
public userIcon: SVGIcon = userIcon;
|
||||
public logoutIcon: SVGIcon = logoutIcon;
|
||||
|
||||
public userMenuItems: any[] = [];
|
||||
public currentUser: User | null = null;
|
||||
public isAuthenticated = false;
|
||||
|
||||
public badgeAlign = {
|
||||
vertical: 'top' as const,
|
||||
horizontal: 'end' as const
|
||||
};
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
public layoutService: LayoutService,
|
||||
private authService: AuthService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to authentication state changes
|
||||
this.authService.currentUser$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(user => {
|
||||
this.currentUser = user;
|
||||
this.isAuthenticated = !!user;
|
||||
this.updateUserMenu();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
public onMenuClick(): void {
|
||||
this.layoutService.toggleDrawer();
|
||||
}
|
||||
|
||||
public onLogout(): void {
|
||||
this.authService.logout();
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
|
||||
public onUserMenuClick(item: any): void {
|
||||
if (item.click) {
|
||||
item.click();
|
||||
}
|
||||
}
|
||||
|
||||
public getDisplayName(): string {
|
||||
if (this.currentUser) {
|
||||
const fullName = `${this.currentUser.firstName} ${this.currentUser.lastName}`.trim();
|
||||
return fullName || this.currentUser.email;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private updateUserMenu(): void {
|
||||
if (this.isAuthenticated && this.currentUser) {
|
||||
this.userMenuItems = [
|
||||
{
|
||||
text: `Welcome, ${this.getDisplayName() || this.currentUser.email}`,
|
||||
disabled: true
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
text: 'Profile',
|
||||
icon: 'user',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
text: 'Settings',
|
||||
icon: 'settings',
|
||||
disabled: true
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
text: 'Logout',
|
||||
icon: 'logout',
|
||||
click: () => this.onLogout()
|
||||
}
|
||||
];
|
||||
} else {
|
||||
this.userMenuItems = [
|
||||
{
|
||||
text: 'Sign In',
|
||||
click: () => this.router.navigate(['/login'])
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<kendo-drawer-container>
|
||||
<kendo-drawer class="!k-flex-none k-overflow-y-auto !k-pos-sticky" [items]="drawerItems"
|
||||
[mode]="layoutService.drawerMode()" [mini]="true" [expanded]="layoutService.drawerExpanded()"
|
||||
(select)="onSelect($event)" [autoCollapse]="layoutService.drawerAutoCollapse()"
|
||||
[isItemExpanded]="isItemExpanded" [width]="248" [style.height]="'calc(100vh - 46px)'">
|
||||
<ng-template kendoDrawerItemTemplate let-item let-hasChildren="hasChildren" let-isItemExpanded="isItemExpanded">
|
||||
@if (item.svgIcon) {
|
||||
<kendo-svgicon [icon]="item.svgIcon"></kendo-svgicon>
|
||||
}
|
||||
<span class="k-item-text">{{item.text}}</span>
|
||||
@if (hasChildren) {
|
||||
<span class="k-spacer"></span>
|
||||
<span class="k-drawer-toggle">
|
||||
<kendo-svgicon [icon]="isItemExpanded ? chevronUpIcon : chevronDownIcon"></kendo-svgicon>
|
||||
</span>
|
||||
}
|
||||
</ng-template>
|
||||
</kendo-drawer>
|
||||
<kendo-drawer-content>
|
||||
<router-outlet></router-outlet>
|
||||
<app-footer></app-footer>
|
||||
</kendo-drawer-content>
|
||||
</kendo-drawer-container>
|
||||
@@ -0,0 +1,20 @@
|
||||
/* Drawer animation */
|
||||
kendo-drawer {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
@media (max-width: 767px) {
|
||||
/* Ensure drawer overlay has proper z-index */
|
||||
kendo-drawer.k-drawer-overlay {
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet optimizations */
|
||||
@media (min-width: 768px) and (max-width: 1023px) {
|
||||
/* Adjust drawer width for tablets if needed */
|
||||
kendo-drawer {
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router, RouterOutlet } from '@angular/router';
|
||||
import { LayoutModule } from '@progress/kendo-angular-layout';
|
||||
import { IconsModule } from '@progress/kendo-angular-icons';
|
||||
import { SVGIcon, chevronDownIcon, chevronUpIcon } from '@progress/kendo-svg-icons';
|
||||
import { DrawerItemExpandedFn, DrawerSelectEvent } from '@progress/kendo-angular-layout';
|
||||
import { LayoutService } from '../services/layout.service';
|
||||
import { drawerItems } from '../../features/dashboard/models';
|
||||
import { FooterComponent } from '../footer/footer.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navbar',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterOutlet,
|
||||
LayoutModule,
|
||||
IconsModule,
|
||||
FooterComponent
|
||||
],
|
||||
templateUrl: './navbar.component.html',
|
||||
styleUrls: ['./navbar.component.scss']
|
||||
})
|
||||
export class NavbarComponent {
|
||||
public chevronUpIcon: SVGIcon = chevronUpIcon;
|
||||
public chevronDownIcon: SVGIcon = chevronDownIcon;
|
||||
|
||||
public drawerItems = drawerItems;
|
||||
public selectedDrawerItem = 'Dashboard';
|
||||
public expandedItems: Array<number> = [4];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
public layoutService: LayoutService
|
||||
) { }
|
||||
|
||||
public onSelect(ev: DrawerSelectEvent): void {
|
||||
this.selectedDrawerItem = ev.item.text;
|
||||
const current = ev.item.id;
|
||||
|
||||
if (this.expandedItems.indexOf(current) >= 0) {
|
||||
this.expandedItems = this.expandedItems.filter((id) => id !== current);
|
||||
} else {
|
||||
this.expandedItems.push(current);
|
||||
}
|
||||
|
||||
// Auto-collapse drawer on mobile after selection
|
||||
if (this.layoutService.isMobile()) {
|
||||
this.layoutService.closeDrawer();
|
||||
}
|
||||
|
||||
// Navigate based on the selected item
|
||||
const routeMap: { [key: string]: string } = {
|
||||
'Dashboard': '/dashboard',
|
||||
'Schedule': '/schedule',
|
||||
'Patients': '/patients',
|
||||
'Bed Management': '/bed-management',
|
||||
'Staff': '/staff',
|
||||
'Pharmacy': '/pharmacy',
|
||||
'Reports': '/reports',
|
||||
'Departments': '/departments',
|
||||
'Payments': '/payments',
|
||||
'Support': '/support'
|
||||
};
|
||||
|
||||
const route = routeMap[ev.item.text];
|
||||
if (route) {
|
||||
this.router.navigate([route]);
|
||||
}
|
||||
}
|
||||
|
||||
public isItemExpanded: DrawerItemExpandedFn = (item): boolean => {
|
||||
return this.expandedItems.indexOf(item.id) >= 0;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Injectable, signal, computed } from '@angular/core';
|
||||
import { DrawerMode } from '@progress/kendo-angular-layout';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class LayoutService {
|
||||
// Signals for reactive state management
|
||||
private readonly windowWidth = signal<number>(typeof window !== 'undefined' ? window.innerWidth : 1024);
|
||||
|
||||
// Computed signals for responsive breakpoints
|
||||
public readonly isMobile = computed(() => this.windowWidth() < 768);
|
||||
public readonly isTablet = computed(() => this.windowWidth() >= 768 && this.windowWidth() < 1024);
|
||||
public readonly isDesktop = computed(() => this.windowWidth() >= 1024);
|
||||
|
||||
// Drawer state
|
||||
public readonly drawerExpanded = signal<boolean>(true);
|
||||
public readonly drawerMode = computed<DrawerMode>(() =>
|
||||
this.isMobile() ? 'overlay' : 'push'
|
||||
);
|
||||
public readonly drawerAutoCollapse = computed<boolean>(() => this.isMobile());
|
||||
|
||||
constructor() {
|
||||
this.initializeResizeListener();
|
||||
this.updateDrawerState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize window resize listener
|
||||
*/
|
||||
private initializeResizeListener(): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', () => this.handleResize());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle window resize events
|
||||
*/
|
||||
private handleResize(): void {
|
||||
this.windowWidth.set(window.innerWidth);
|
||||
this.updateDrawerState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update drawer state based on screen size
|
||||
*/
|
||||
private updateDrawerState(): void {
|
||||
if (this.isMobile()) {
|
||||
this.drawerExpanded.set(false);
|
||||
} else {
|
||||
this.drawerExpanded.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle drawer open/closed state
|
||||
*/
|
||||
public toggleDrawer(): void {
|
||||
this.drawerExpanded.update(expanded => !expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close drawer (useful for mobile after navigation)
|
||||
*/
|
||||
public closeDrawer(): void {
|
||||
this.drawerExpanded.set(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open drawer
|
||||
*/
|
||||
public openDrawer(): void {
|
||||
this.drawerExpanded.set(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current window width
|
||||
*/
|
||||
public getWindowWidth(): number {
|
||||
return this.windowWidth();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user