WIP
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
<main
|
||||
class="k-px-2 k-px-sm-4.5 k-px-md-6 k-px-lg-4 k-px-xl-7.5 k-py-2 k-py-sm-4.5 k-py-md-6 k-py-lg-4 k-py-xl-7.5 k-pt-8 k-bg-light">
|
||||
<h1 class="k-h1 k-color-primary-emphasis k-overflow-hidden k-text-ellipsis">Dashboard</h1>
|
||||
<div class="k-d-grid k-grid-cols-12 k-gap-4 k-py-4">
|
||||
|
||||
<!-- Start of CMPCTCARD-1 -->
|
||||
<div *ngFor="let card of compactCards; let i = index;"
|
||||
class="{{cardClasses}} k-col-span-12 k-col-span-md-6 k-col-span-lg-3">
|
||||
<kendo-svgicon [icon]="card.svgIcon" themeColor="primary" size="xxlarge"></kendo-svgicon>
|
||||
<div class="k-d-flex k-flex-col">
|
||||
<span
|
||||
class="k-font-size-lg k-line-height-lg k-font-semibold k-color-primary-emphasis">{{card.title}}</span>
|
||||
<span class="k-font-size-sm k-line-height-lg k-color-subtle">{{card.info}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of CMPCTCARD-1 -->
|
||||
|
||||
<!-- Start of DASHBRDCARD-10 -->
|
||||
<div class="{{dashboardClasses}} k-col-span-12 k-col-span-md-6 k-col-span-lg-3">
|
||||
<div class="k-d-flex k-align-items-center k-p-3">
|
||||
<span class="k-font-size-lg k-line-height-lg k-font-semibold k-color-primary-emphasis">Calendar</span>
|
||||
</div>
|
||||
<div class="k-flex-1 k-px-3 k-pb-3 k-d-flex k-justify-content-center">
|
||||
<kendo-calendar [showOtherMonthDays]="false" type="classic" [(ngModel)]="date2">
|
||||
</kendo-calendar>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of DASHBRDCARD-10 -->
|
||||
|
||||
<!-- Start of DASHBRDCARD-11 -->
|
||||
<div class="{{dashboardClasses}} k-col-span-12 k-col-span-md-6">
|
||||
<div class="k-d-flex k-justify-content-between k-align-items-center k-p-3">
|
||||
<span class="k-font-size-lg k-line-height-lg k-font-semibold k-color-primary-emphasis">Bed
|
||||
Occupancy</span>
|
||||
<kendo-datepicker format="yyyy" [(ngModel)]="date" [fillMode]="'flat'" [style.width.px]="164"
|
||||
[clearButton]="true" [inputAttributes]="{'aria-label': 'Select date'}"></kendo-datepicker>
|
||||
</div>
|
||||
<div class="k-flex-1 k-px-3 k-pb-3">
|
||||
<kendo-chart style="height: 257px;">
|
||||
<kendo-chart-category-axis>
|
||||
<kendo-chart-category-axis-item
|
||||
[categories]="['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']">
|
||||
</kendo-chart-category-axis-item>
|
||||
</kendo-chart-category-axis>
|
||||
<kendo-chart-value-axis>
|
||||
<kendo-chart-value-axis-item [max]="100" [min]="0" [majorTicks]="{step: 10}">
|
||||
</kendo-chart-value-axis-item>
|
||||
</kendo-chart-value-axis>
|
||||
<kendo-chart-series>
|
||||
<kendo-chart-series-item type="column" name="Occupied" [spacing]="0"
|
||||
[legendItem]="{type: 'line' }" [data]="[67, 78, 47, 41, 38, 33]">
|
||||
</kendo-chart-series-item>
|
||||
<kendo-chart-series-item type="column" name="Free" [legendItem]="{type: 'line' }"
|
||||
[data]="[21, 10, 44, 40, 48, 60]">
|
||||
</kendo-chart-series-item>
|
||||
</kendo-chart-series>
|
||||
<kendo-chart-legend position="bottom" orientation="horizontal" align="start"></kendo-chart-legend>
|
||||
</kendo-chart>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of DASHBRDCARD-11 -->
|
||||
|
||||
<!-- Start of DASHBRDCARD-1 -->
|
||||
<div class="{{dashboardClasses}} k-col-span-12 k-col-span-md-6 k-col-span-lg-3">
|
||||
<div class="k-d-flex k-justify-content-between k-align-items-center k-p-3">
|
||||
<span class="k-font-size-lg k-line-height-lg k-font-semibold k-color-primary-emphasis">Staff</span>
|
||||
<kendo-dropdownlist [data]="ddlData" [value]="ddlValue" fillMode="flat" [style.width.px]="164"
|
||||
[attr.aria-label]="'Select'"></kendo-dropdownlist>
|
||||
</div>
|
||||
<div class="k-flex-1 k-px-3">
|
||||
<kendo-listview [data]="listItems" layout="flex" flexDirection="col" [bordered]="false">
|
||||
<ng-template kendoListViewItemTemplate let-dataItem>
|
||||
<div
|
||||
class="k-d-flex k-border-b k-border-b-solid k-border-border k-gap-3 k-p-2 k-align-items-center">
|
||||
<kendo-badge-container>
|
||||
<kendo-avatar [imageSrc]="dataItem.imageSrc"></kendo-avatar>
|
||||
<kendo-badge rounded="medium" position="inside" [align]="badgeAlignBottomEnd"
|
||||
themeColor="success"></kendo-badge>
|
||||
</kendo-badge-container>
|
||||
<div class="k-d-flex k-flex-col">
|
||||
<div class="k-font-size-lg">{{dataItem.name}}</div>
|
||||
<div class="k-font-size-sm k-color-subtle">{{dataItem.specialty}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</kendo-listview>
|
||||
</div>
|
||||
<div class="k-p-3">
|
||||
<button kendoButton fillMode="flat" themeColor="primary">View all</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of DASHBRDCARD-1 -->
|
||||
|
||||
<!-- Start of DASHBRDCARD-4 -->
|
||||
<div class="{{dashboardClasses}} k-col-span-12 k-col-span-md-6 k-col-span-lg-7">
|
||||
<div class="k-d-flex k-justify-content-between k-align-items-center k-p-3">
|
||||
<span
|
||||
class="k-font-size-lg k-line-height-lg k-font-semibold k-color-primary-emphasis">Appointments</span>
|
||||
<kendo-datepicker [(ngModel)]="date" [fillMode]="'flat'" [style.width.px]="164" [clearButton]="true"
|
||||
[inputAttributes]="{'aria-label': 'Select date'}"></kendo-datepicker>
|
||||
</div>
|
||||
<div class="k-d-grid k-grid-cols-12 k-p-4 k-gap-2">
|
||||
<div *ngFor="let appointment of appointments; let last = last"
|
||||
[ngClass]="{ 'k-d-none k-d-lg-block' : last }"
|
||||
class=" k-col-span-12 k-col-span-lg-4 k-bg-light k-border k-border-solid k-border-border k-rounded-sm k-d-flex k-flex-col k-flex-1">
|
||||
<div class="k-d-flex k-justify-content-between k-p-1.5 k-h-12">
|
||||
<span class="k-font-medium">{{appointment.doctor}}</span>
|
||||
<div class="k-flex-shrink-0">
|
||||
<span class="k-badge k-badge-md k-badge-solid k-badge-solid-primary k-rounded-full">
|
||||
{{appointment.start}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="k-d-flex k-flex-col k-flex-1 k-gap-1.5 k-px-1.5">
|
||||
<div>Appointment with {{appointment.patient.name}}.</div>
|
||||
<div class="k-font-size-sm">
|
||||
<div class="k-color-subtle k-d-flex k-gap-1 k-align-items-center k-line-height-lg">
|
||||
<kendo-svgicon [icon]="envelopeIcon"></kendo-svgicon>
|
||||
<a class="k-color-inherit" href="#">{{appointment.patient.phone}}</a>
|
||||
</div>
|
||||
<div class="k-color-subtle k-d-flex k-gap-1 k-align-items-center k-line-height-lg">
|
||||
<kendo-svgicon [icon]="envelopeIcon"></kendo-svgicon>
|
||||
<a class="k-color-inherit" href="#">{{appointment.patient.email}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="k-d-flex k-flex-shrink-0 k-p-1.5">
|
||||
<button kendoButton fillMode="clear" themeColor="primary">Edit</button>
|
||||
<button kendoButton fillMode="clear">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="k-p-3">
|
||||
<button kendoButton fillMode="clear" themeColor="primary">View all appointments</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of DASHBRDCARD-4 -->
|
||||
|
||||
<!-- Start of DASHBRDCARD-11 -->
|
||||
<div class="{{dashboardClasses}} k-col-span-12 k-col-span-lg-5">
|
||||
<div class="k-d-flex k-justify-content-between k-align-items-center k-p-3">
|
||||
<span class="k-font-size-lg k-line-height-lg k-font-semibold k-color-primary-emphasis">Infection
|
||||
Rate</span>
|
||||
</div>
|
||||
<div class="k-flex-1 k-px-3 k-pb-3">
|
||||
<kendo-chart [style.height.px]="240">
|
||||
<kendo-chart-x-axis>
|
||||
<kendo-chart-x-axis-item [labels]="{rotation: -45}"></kendo-chart-x-axis-item>
|
||||
</kendo-chart-x-axis>
|
||||
<kendo-chart-series>
|
||||
<kendo-chart-series-item
|
||||
*ngFor="let dataSet of ['RSV', 'CDC', 'Measles', 'Influenza', 'Campylobacteriosis', 'Hepatitis']"
|
||||
type="heatmap" [data]="heatmapData(dataSet)" xField="a" yField="b"
|
||||
valueField="value"></kendo-chart-series-item>
|
||||
</kendo-chart-series>
|
||||
</kendo-chart>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of DASHBRDCARD-11 -->
|
||||
|
||||
<!-- Start of DASHBRDCARD-11 -->
|
||||
<div class="{{dashboardClasses}} k-col-span-12 k-col-span-lg-5">
|
||||
<div class="k-d-flex k-justify-content-between k-align-items-center k-p-3">
|
||||
<span class="k-font-size-lg k-line-height-lg k-font-semibold k-color-primary-emphasis">Equipment
|
||||
Availability</span>
|
||||
<kendo-datepicker [(ngModel)]="date" format="yyyy" [fillMode]="'flat'" [style.width.px]="164"
|
||||
[clearButton]="true" [inputAttributes]="{'aria-label': 'Select date'}"></kendo-datepicker>
|
||||
</div>
|
||||
<div class="k-flex-1 k-px-3 k-pb-3">
|
||||
<kendo-chart [style.height.px]="240">
|
||||
<kendo-chart-series>
|
||||
<kendo-chart-series-item [autoFit]="true" type="donut" [holeSize]="50" [data]="donutData"
|
||||
categoryField="kind" field="share">
|
||||
<kendo-chart-series-item-labels position="outsideEnd" color="#000"
|
||||
[content]="chartLabelContent"></kendo-chart-series-item-labels>
|
||||
</kendo-chart-series-item>
|
||||
</kendo-chart-series>
|
||||
<kendo-chart-legend [visible]="false"></kendo-chart-legend>
|
||||
</kendo-chart>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of DASHBRDCARD-11 -->
|
||||
|
||||
<!-- Start of DASHBRDCARD-11 -->
|
||||
<div class="{{dashboardClasses}} k-col-span-12 k-col-span-lg-7">
|
||||
<div class="k-d-flex k-justify-content-between k-align-items-center k-p-3">
|
||||
<span class="k-font-size-lg k-line-height-lg k-font-semibold k-color-primary-emphasis">Average Length of
|
||||
Stay</span>
|
||||
<kendo-datepicker [(ngModel)]="date" format="yyyy" [fillMode]="'flat'" [style.width.px]="164"
|
||||
[clearButton]="true" [inputAttributes]="{'aria-label': 'Select date'}"></kendo-datepicker>
|
||||
</div>
|
||||
<div class="k-flex-1 k-px-3 k-pb-3">
|
||||
<kendo-chart [style.height.px]="240">
|
||||
<kendo-chart-category-axis>
|
||||
<kendo-chart-category-axis-item [categories]="departments">
|
||||
</kendo-chart-category-axis-item>
|
||||
</kendo-chart-category-axis>
|
||||
<kendo-chart-value-axis>
|
||||
<kendo-chart-value-axis-item [max]="14" [majorUnit]="1">
|
||||
</kendo-chart-value-axis-item>
|
||||
</kendo-chart-value-axis>
|
||||
<kendo-chart-series>
|
||||
<kendo-chart-series-item type="bar" [data]="averageStay">
|
||||
</kendo-chart-series-item>
|
||||
</kendo-chart-series>
|
||||
</kendo-chart>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of DASHBRDCARD-11 -->
|
||||
|
||||
<!-- Start of DASHBRDCARD-11 -->
|
||||
<div class="{{dashboardClasses}} k-col-span-12">
|
||||
<div class="k-d-flex k-justify-content-between k-align-items-center k-p-3">
|
||||
<span class="k-font-size-lg k-line-height-lg k-font-semibold k-color-primary-emphasis">Hospital
|
||||
Visits</span>
|
||||
<kendo-datepicker [(ngModel)]="date" format="yyyy" [fillMode]="'flat'" [style.width.px]="164"
|
||||
[clearButton]="true" [inputAttributes]="{'aria-label': 'Select date'}"></kendo-datepicker>
|
||||
</div>
|
||||
<div class="k-flex-1 k-px-3 k-pb-3">
|
||||
<kendo-chart [style.height.px]="330">
|
||||
<kendo-chart-category-axis>
|
||||
<kendo-chart-category-axis-item [categories]="hours" baseUnit="hours"
|
||||
[labels]="{rotation: 270, position: 'start', format: 'h:mm'}">
|
||||
</kendo-chart-category-axis-item>
|
||||
</kendo-chart-category-axis>
|
||||
<kendo-chart-value-axis>
|
||||
<kendo-chart-value-axis-item [max]="100">
|
||||
</kendo-chart-value-axis-item>
|
||||
</kendo-chart-value-axis>
|
||||
<kendo-chart-series>
|
||||
<kendo-chart-series-item type="line" [data]="hospitalVisits">
|
||||
</kendo-chart-series-item>
|
||||
</kendo-chart-series>
|
||||
</kendo-chart>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of DASHBRDCARD-11 -->
|
||||
|
||||
<!-- Start of DASHBRDCARD-11 -->
|
||||
<div class="{{dashboardClasses}} k-col-span-12 k-col-span-lg-5">
|
||||
<div class="k-d-flex k-justify-content-between k-align-items-center k-p-3">
|
||||
<span class="k-font-size-lg k-line-height-lg k-font-semibold k-color-primary-emphasis">Satisfaction
|
||||
Score</span>
|
||||
<kendo-dropdownlist [value]="'2023'" [fillMode]="'flat'" [style.width.px]="164"
|
||||
[attr.aria-label]="'Select'"></kendo-dropdownlist>
|
||||
</div>
|
||||
<div class="k-flex-1 k-px-3 k-pb-3">
|
||||
<kendo-chart [style.height.px]="288">
|
||||
<kendo-chart-series>
|
||||
<kendo-chart-series-item type="pie" [legendItem]="{type: 'line' }" [data]="satisfaction"
|
||||
categoryField="kind" field="share" [padding]="10" [border]="{width: 3, color: '#fff'}">
|
||||
<kendo-chart-series-item-labels position="center">
|
||||
</kendo-chart-series-item-labels>
|
||||
</kendo-chart-series-item>
|
||||
</kendo-chart-series>
|
||||
<kendo-chart-legend position="bottom"></kendo-chart-legend>
|
||||
</kendo-chart>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of DASHBRDCARD-11 -->
|
||||
|
||||
<!-- Start of DASHBRDCARD-11 -->
|
||||
<div class="{{dashboardClasses}} k-col-span-12 k-col-span-lg-7">
|
||||
<div class="k-d-flex k-justify-content-between k-align-items-center k-p-3">
|
||||
<span class="k-font-size-lg k-line-height-lg k-font-semibold k-color-primary-emphasis">Mortality
|
||||
Rate</span>
|
||||
<kendo-datepicker format="yyyy" [(ngModel)]="date" [fillMode]="'flat'" [style.width.px]="164"
|
||||
[clearButton]="true" [inputAttributes]="{'aria-label': 'Select date'}"></kendo-datepicker>
|
||||
</div>
|
||||
<div class="k-flex-1 k-px-3 k-pb-3">
|
||||
<kendo-chart [style.height.px]="288">
|
||||
<kendo-chart-category-axis>
|
||||
<kendo-chart-category-axis-item [categories]="mortalityCauses">
|
||||
</kendo-chart-category-axis-item>
|
||||
</kendo-chart-category-axis>
|
||||
<kendo-chart-value-axis>
|
||||
<kendo-chart-value-axis-item [max]="100" [min]="0"
|
||||
[majorTicks]="{step: 10}"></kendo-chart-value-axis-item>
|
||||
</kendo-chart-value-axis>
|
||||
<kendo-chart-series>
|
||||
<kendo-chart-series-item type="bar" [legendItem]="{type: 'line' }" name="Male"
|
||||
[data]="[25, 35, 36, 42, 85, 12, 4, 17, 19, 49, 28]">
|
||||
</kendo-chart-series-item>
|
||||
<kendo-chart-series-item type="bar" [legendItem]="{type: 'line' }" name="Female"
|
||||
[data]="[23, 40, 38, 30, 81, 18, 3, 21, 22, 45, 24]">
|
||||
</kendo-chart-series-item>
|
||||
</kendo-chart-series>
|
||||
<kendo-chart-legend position="bottom" orientation="horizontal" align="start"></kendo-chart-legend>
|
||||
</kendo-chart>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of DASHBRDCARD-11 -->
|
||||
</div>
|
||||
</main>
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ChartsModule, SeriesLabelsContentArgs } from '@progress/kendo-angular-charts';
|
||||
import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
|
||||
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
|
||||
import { ListViewModule } from '@progress/kendo-angular-listview';
|
||||
import { BadgeAlign, IndicatorsModule } from '@progress/kendo-angular-indicators';
|
||||
import { ButtonsModule } from '@progress/kendo-angular-buttons';
|
||||
import { IconsModule } from '@progress/kendo-angular-icons';
|
||||
import { LayoutModule } from '@progress/kendo-angular-layout';
|
||||
import { SVGIcon, envelopeIcon } from '@progress/kendo-svg-icons';
|
||||
import {
|
||||
appointments,
|
||||
averageStay,
|
||||
compactCards,
|
||||
departments,
|
||||
donutData,
|
||||
heatmapDataCDC,
|
||||
heatmapDataCampylobacteriosis,
|
||||
heatmapDataHepatitis,
|
||||
heatmapDataInfluenza,
|
||||
heatmapDataMeasles,
|
||||
heatmapDataRSV,
|
||||
hospitalVisits,
|
||||
hours,
|
||||
listItems,
|
||||
mortalityCauses,
|
||||
satisfaction
|
||||
} from './models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ChartsModule,
|
||||
DateInputsModule,
|
||||
DropDownsModule,
|
||||
ListViewModule,
|
||||
IndicatorsModule,
|
||||
ButtonsModule,
|
||||
IconsModule,
|
||||
LayoutModule
|
||||
],
|
||||
templateUrl: './dashboard.html',
|
||||
styleUrl: './dashboard.css'
|
||||
})
|
||||
export class Dashboard {
|
||||
public cardClasses = 'k-d-flex k-border k-border-solid k-border-border k-bg-surface-alt k-align-items-center k-overflow-x-auto k-p-3 k-gap-6 k-elevation-1 k-rounded-md';
|
||||
public dashboardClasses = 'k-d-flex k-flex-col k-border k-border-solid k-border-border k-bg-surface-alt k-overflow-x-auto k-elevation-1 k-rounded-md';
|
||||
|
||||
public envelopeIcon: SVGIcon = envelopeIcon;
|
||||
|
||||
public badgeAlignBottomEnd: BadgeAlign = {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'end'
|
||||
};
|
||||
|
||||
public chartLabelContent(e: SeriesLabelsContentArgs): string {
|
||||
return e.category;
|
||||
}
|
||||
|
||||
public date = new Date(2023, 5, 14);
|
||||
public date2 = new Date(2023, 5, 15);
|
||||
public averageStay = averageStay;
|
||||
public hours = hours
|
||||
public hospitalVisits = hospitalVisits;
|
||||
public departments = departments;
|
||||
public mortalityCauses = mortalityCauses;
|
||||
public satisfaction = satisfaction;
|
||||
public donutData = donutData;
|
||||
public heatmapDataRSV = heatmapDataRSV;
|
||||
public heatmapDataCDC = heatmapDataCDC;
|
||||
public heatmapDataMeasles = heatmapDataMeasles;
|
||||
public heatmapDataInfluenza = heatmapDataInfluenza;
|
||||
public heatmapDataHepatitis = heatmapDataHepatitis
|
||||
public heatmapDataCampylobacteriosis = heatmapDataCampylobacteriosis;
|
||||
public heatmapData = (dataset: string): any[] => (this as any)[`heatmapData${dataset}`];
|
||||
public appointments = appointments;
|
||||
public ddlData = ['All Departments'];
|
||||
public ddlValue = 'All Departments'
|
||||
public compactCards = compactCards;
|
||||
public listItems: any[] = listItems;
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
import { accessibilityIcon, calendarDateIcon, calendarIcon, displayBlockIcon, dollarIcon, fileIcon, inboxIcon, myspaceIcon, pencilIcon, starOutlineIcon } from "@progress/kendo-svg-icons";
|
||||
|
||||
export const menuItems = [
|
||||
"Settings",
|
||||
"Support",
|
||||
"Log out"
|
||||
];
|
||||
|
||||
export const averageStay = [4, 3, 2, 14, 5, 7, 5, 6, 12, 1, 4];
|
||||
|
||||
export const hours = Array(48).fill({}).map((_, idx) => `${Math.floor(idx / 2)}:${idx % 2 ? '30': '00'}`);
|
||||
|
||||
export const hospitalVisits = [14, 20, 20, 26, 30, 26, 29, 32, 31, 29, 31, 35, 36, 40, 42, 45, 61, 63, 65, 66, 67, 67, 63, 64, 63, 62, 60, 45, 52, 55, 48, 44, 38, 35, 31, 35, 36, 40, 42, 55, 50, 41, 41, 39, 31, 32, 23, 27];
|
||||
|
||||
export const departments = [
|
||||
'Pharmacology & Toxicology',
|
||||
'Gastroenterology',
|
||||
'Radiology',
|
||||
'Orthopedics',
|
||||
'Outpatient',
|
||||
'Oncology',
|
||||
'Neurology',
|
||||
'ICU',
|
||||
'Cardiology',
|
||||
'Emergency',
|
||||
'Delivery'
|
||||
];
|
||||
|
||||
export const mortalityCauses = [
|
||||
'Pharmacology & Toxicology',
|
||||
'Oncological diseases',
|
||||
'Circulatory diseases',
|
||||
'Injury and poisoning',
|
||||
'Respiratory diseases',
|
||||
'Endocrine diseases',
|
||||
'Digestive diseases',
|
||||
'Nervous system diseases',
|
||||
'Infectious diseases',
|
||||
'Kidney diseases',
|
||||
'Other causes'
|
||||
];
|
||||
|
||||
export const satisfaction = [
|
||||
{
|
||||
kind: 'Very dissatisfied',
|
||||
share: 60
|
||||
},
|
||||
{
|
||||
kind: 'Dissatisfied',
|
||||
share: 60
|
||||
},
|
||||
{
|
||||
kind: 'Neutral',
|
||||
share: 60
|
||||
},
|
||||
{
|
||||
kind: 'Satisfied',
|
||||
share: 60
|
||||
},
|
||||
{
|
||||
kind: 'Very satisfied',
|
||||
share: 60
|
||||
},
|
||||
{
|
||||
kind: 'Didn\'t answer',
|
||||
share: 60
|
||||
}];
|
||||
|
||||
export const donutData = [
|
||||
{
|
||||
kind: 'Imaging Equipment',
|
||||
share: 0.17,
|
||||
},
|
||||
{
|
||||
kind: 'Surgical Instruments',
|
||||
share: 0.17,
|
||||
},
|
||||
{
|
||||
kind: 'Electromedical Equipment',
|
||||
share: 0.17,
|
||||
},
|
||||
{
|
||||
kind: 'Transport and Storage',
|
||||
share: 0.17,
|
||||
},
|
||||
{
|
||||
kind: 'Endoscopic Instruments',
|
||||
share: 0.17,
|
||||
},
|
||||
{
|
||||
kind: 'Others',
|
||||
share: 0.17,
|
||||
}];
|
||||
|
||||
export const heatmapDataRSV = [{
|
||||
a: 'June 2023',
|
||||
b: 'RSV',
|
||||
value: 66
|
||||
}, {
|
||||
a: 'May 2023',
|
||||
b: 'RSV',
|
||||
value: 34
|
||||
}, {
|
||||
a: 'Apr 2023',
|
||||
b: 'RSV',
|
||||
value: 13
|
||||
}, {
|
||||
a: 'Mar 2023',
|
||||
b: 'RSV',
|
||||
value: 49
|
||||
}, {
|
||||
a: 'Feb 2023',
|
||||
b: 'RSV',
|
||||
value: 22
|
||||
}, {
|
||||
a: 'Jan 2023',
|
||||
b: 'RSV',
|
||||
value: 66
|
||||
}, {
|
||||
a: 'Dec 2022',
|
||||
b: 'RSV',
|
||||
value: 78
|
||||
}, {
|
||||
a: 'Nov 2022',
|
||||
b: 'RSV',
|
||||
value: 89
|
||||
}, {
|
||||
a: 'Oct 2022',
|
||||
b: 'RSV',
|
||||
value: 27
|
||||
}, {
|
||||
a: 'Sep 2022',
|
||||
b: 'RSV',
|
||||
value: 83
|
||||
}];
|
||||
|
||||
export const heatmapDataCDC = [{
|
||||
a: 'June 2023',
|
||||
b: 'CDC',
|
||||
value: 51
|
||||
}, {
|
||||
a: 'May 2023',
|
||||
b: 'CDC',
|
||||
value: 84
|
||||
}, {
|
||||
a: 'Apr 2023',
|
||||
b: 'CDC',
|
||||
value: 32
|
||||
}, {
|
||||
a: 'Mar 2023',
|
||||
b: 'CDC',
|
||||
value: 16
|
||||
}, {
|
||||
a: 'Feb 2023',
|
||||
b: 'CDC',
|
||||
value: 11
|
||||
}, {
|
||||
a: 'Jan 2023',
|
||||
b: 'CDC',
|
||||
value: 55
|
||||
}, {
|
||||
a: 'Dec 2022',
|
||||
b: 'CDC',
|
||||
value: 99
|
||||
}, {
|
||||
a: 'Nov 2022',
|
||||
b: 'CDC',
|
||||
value: 42
|
||||
}, {
|
||||
a: 'Oct 2022',
|
||||
b: 'CDC',
|
||||
value: 30
|
||||
}, {
|
||||
a: 'Sep 2022',
|
||||
b: 'CDC',
|
||||
value: 10
|
||||
}];
|
||||
|
||||
export const heatmapDataMeasles = [{
|
||||
a: 'June 2023',
|
||||
b: 'Measles',
|
||||
value: 80
|
||||
}, {
|
||||
a: 'May 2023',
|
||||
b: 'Measles',
|
||||
value: 56
|
||||
}, {
|
||||
a: 'Apr 2023',
|
||||
b: 'Measles',
|
||||
value: 78
|
||||
}, {
|
||||
a: 'Mar 2023',
|
||||
b: 'Measles',
|
||||
value: 63
|
||||
}, {
|
||||
a: 'Feb 2023',
|
||||
b: 'Measles',
|
||||
value: 24
|
||||
}, {
|
||||
a: 'Jan 2023',
|
||||
b: 'Measles',
|
||||
value: 33
|
||||
}, {
|
||||
a: 'Dec 2022',
|
||||
b: 'Measles',
|
||||
value: 38
|
||||
}, {
|
||||
a: 'Nov 2022',
|
||||
b: 'Measles',
|
||||
value: 17
|
||||
}, {
|
||||
a: 'Oct 2022',
|
||||
b: 'Measles',
|
||||
value: 62
|
||||
}, {
|
||||
a: 'Sep 2022',
|
||||
b: 'Measles',
|
||||
value: 82
|
||||
}];
|
||||
|
||||
export const heatmapDataInfluenza = [{
|
||||
a: 'June 2023',
|
||||
b: 'Influenza',
|
||||
value: 84
|
||||
}, {
|
||||
a: 'May 2023',
|
||||
b: 'Influenza',
|
||||
value: 25
|
||||
}, {
|
||||
a: 'Apr 2023',
|
||||
b: 'Influenza',
|
||||
value: 59
|
||||
}, {
|
||||
a: 'Mar 2023',
|
||||
b: 'Influenza',
|
||||
value: 74
|
||||
}, {
|
||||
a: 'Feb 2023',
|
||||
b: 'Influenza',
|
||||
value: 41
|
||||
}, {
|
||||
a: 'Jan 2023',
|
||||
b: 'Influenza',
|
||||
value: 69
|
||||
}, {
|
||||
a: 'Dec 2022',
|
||||
b: 'Influenza',
|
||||
value: 71
|
||||
}, {
|
||||
a: 'Nov 2022',
|
||||
b: 'Influenza',
|
||||
value: 11
|
||||
}, {
|
||||
a: 'Oct 2022',
|
||||
b: 'Influenza',
|
||||
value: 23
|
||||
}, {
|
||||
a: 'Sep 2022',
|
||||
b: 'Influenza',
|
||||
value: 43
|
||||
}];
|
||||
|
||||
export const heatmapDataHepatitis = [{
|
||||
a: 'June 2023',
|
||||
b: 'Hepatitis',
|
||||
value: 31
|
||||
}, {
|
||||
a: 'May 2023',
|
||||
b: 'Hepatitis',
|
||||
value: 27
|
||||
}, {
|
||||
a: 'Apr 2023',
|
||||
b: 'Hepatitis',
|
||||
value: 16
|
||||
}, {
|
||||
a: 'Mar 2023',
|
||||
b: 'Hepatitis',
|
||||
value: 74
|
||||
}, {
|
||||
a: 'Feb 2023',
|
||||
b: 'Hepatitis',
|
||||
value: 50
|
||||
}, {
|
||||
a: 'Jan 2023',
|
||||
b: 'Hepatitis',
|
||||
value: 6
|
||||
}, {
|
||||
a: 'Dec 2022',
|
||||
b: 'Hepatitis',
|
||||
value: 22
|
||||
}, {
|
||||
a: 'Nov 2022',
|
||||
b: 'Hepatitis',
|
||||
value: 65
|
||||
}, {
|
||||
a: 'Oct 2022',
|
||||
b: 'Hepatitis',
|
||||
value: 37
|
||||
}, {
|
||||
a: 'Sep 2022',
|
||||
b: 'Hepatitis',
|
||||
value: 13
|
||||
}];
|
||||
|
||||
export const heatmapDataCampylobacteriosis = [{
|
||||
a: 'June 2023',
|
||||
b: 'Campylobacteriosis',
|
||||
value: 66
|
||||
}, {
|
||||
a: 'May 2023',
|
||||
b: 'Campylobacteriosis',
|
||||
value: 21
|
||||
}, {
|
||||
a: 'Apr 2023',
|
||||
b: 'Campylobacteriosis',
|
||||
value: 52
|
||||
}, {
|
||||
a: 'Mar 2023',
|
||||
b: 'Campylobacteriosis',
|
||||
value: 43
|
||||
}, {
|
||||
a: 'Feb 2023',
|
||||
b: 'Campylobacteriosis',
|
||||
value: 97
|
||||
}, {
|
||||
a: 'Jan 2023',
|
||||
b: 'Campylobacteriosis',
|
||||
value: 81
|
||||
}, {
|
||||
a: 'Dec 2022',
|
||||
b: 'Campylobacteriosis',
|
||||
value: 28
|
||||
}, {
|
||||
a: 'Nov 2022',
|
||||
b: 'Campylobacteriosis',
|
||||
value: 34
|
||||
}, {
|
||||
a: 'Oct 2022',
|
||||
b: 'Campylobacteriosis',
|
||||
value: 45
|
||||
}, {
|
||||
a: 'Sep 2022',
|
||||
b: 'Campylobacteriosis',
|
||||
value: 18
|
||||
}];
|
||||
|
||||
export const appointments = [{
|
||||
doctor: 'Dr. Terrell Fashey',
|
||||
start: '8:30 AM',
|
||||
patient: {
|
||||
name: 'Flora Strosin',
|
||||
phone: '679-747-6105',
|
||||
email: 'flora.strosin@email.com'
|
||||
}
|
||||
}, {
|
||||
doctor: 'Dr. Clarence Gulgowski',
|
||||
start: '9:10 AM',
|
||||
patient: {
|
||||
name: 'Michele Nicolas',
|
||||
phone: '884-528-7089',
|
||||
email: 'm.nicolas@email.com'
|
||||
}
|
||||
}, {
|
||||
doctor: 'Dr. Jay Mohr',
|
||||
start: '9:45 AM',
|
||||
patient: {
|
||||
name: 'Joseph Pacocha',
|
||||
phone: '777-284-2912',
|
||||
email: 'j.pacocha@email.com'
|
||||
}
|
||||
}];
|
||||
|
||||
export const compactCards = [{
|
||||
svgIcon: calendarIcon,
|
||||
title: 'Appointments',
|
||||
info: '78 appointments today'
|
||||
}, {
|
||||
svgIcon: accessibilityIcon,
|
||||
title: 'Patients',
|
||||
info: '1234 active cases'
|
||||
}, {
|
||||
svgIcon: displayBlockIcon,
|
||||
title: 'Beds',
|
||||
info: '56 occupied beds'
|
||||
}, {
|
||||
svgIcon: myspaceIcon,
|
||||
title: 'Staff',
|
||||
info: '78 colleagues at work'
|
||||
}];
|
||||
|
||||
export const listItems = [{
|
||||
name: 'Dr. Teresa Conn',
|
||||
specialty: 'Internal medicine',
|
||||
imageSrc: 'assets/healthcare-dashboard/avatar_1.png'
|
||||
}, {
|
||||
name: 'Dr. Mitchell Robel',
|
||||
specialty: 'Pediatrics',
|
||||
imageSrc: 'assets/healthcare-dashboard/avatar_2.png'
|
||||
}, {
|
||||
name: 'Dr. Barry Jacobs',
|
||||
specialty: 'Gastroenterology',
|
||||
imageSrc: 'assets/healthcare-dashboard/avatar_3.png'
|
||||
}, {
|
||||
name: 'Dr. Nina Bosco',
|
||||
specialty: 'Cardiology',
|
||||
imageSrc: 'assets/healthcare-dashboard/avatar_4.png'
|
||||
}];
|
||||
|
||||
export const drawerItems = [{
|
||||
text: 'Dashboard',
|
||||
svgIcon: inboxIcon,
|
||||
selected: true,
|
||||
id: 0,
|
||||
}, {
|
||||
text: 'Schedule',
|
||||
svgIcon: calendarDateIcon,
|
||||
id: 1
|
||||
}, {
|
||||
text: 'Patients',
|
||||
svgIcon: accessibilityIcon,
|
||||
id: 2,
|
||||
}, {
|
||||
text: 'Bed Management',
|
||||
svgIcon: displayBlockIcon,
|
||||
id: 3
|
||||
}, {
|
||||
text: 'Staff',
|
||||
svgIcon: myspaceIcon,
|
||||
id: 4,
|
||||
}, {
|
||||
text: 'Doctors',
|
||||
svgIcon: accessibilityIcon,
|
||||
id: 40,
|
||||
parentId: 4
|
||||
}, {
|
||||
text: 'Nurses',
|
||||
svgIcon: accessibilityIcon,
|
||||
id: 41,
|
||||
parentId: 4
|
||||
}, {
|
||||
text: 'Therapists',
|
||||
svgIcon: accessibilityIcon,
|
||||
id: 42,
|
||||
parentId: 4
|
||||
}, {
|
||||
text: 'Technicians',
|
||||
svgIcon: accessibilityIcon,
|
||||
id: 43,
|
||||
parentId: 4
|
||||
}, {
|
||||
text: 'Information technology',
|
||||
svgIcon: accessibilityIcon,
|
||||
id: 44,
|
||||
parentId: 4
|
||||
}, {
|
||||
text: 'Food services',
|
||||
svgIcon: accessibilityIcon,
|
||||
id: 45,
|
||||
parentId: 4
|
||||
}, {
|
||||
text: 'Environmental services',
|
||||
svgIcon: accessibilityIcon,
|
||||
id: 46,
|
||||
parentId: 4
|
||||
}, {
|
||||
text: 'Pharmacy',
|
||||
svgIcon: pencilIcon,
|
||||
id: 5,
|
||||
}, {
|
||||
text: 'Reports',
|
||||
svgIcon: fileIcon,
|
||||
id: 6,
|
||||
}, {
|
||||
text: 'Report 1',
|
||||
svgIcon: fileIcon,
|
||||
id: 60,
|
||||
parentId: 6
|
||||
}, {
|
||||
text: 'Departments',
|
||||
svgIcon: calendarIcon,
|
||||
id: 7,
|
||||
}, {
|
||||
text: 'Report 1',
|
||||
svgIcon: calendarIcon,
|
||||
id: 70,
|
||||
parentId: 7
|
||||
}, {
|
||||
text: 'Payments',
|
||||
svgIcon: dollarIcon,
|
||||
id: 8,
|
||||
}, {
|
||||
text: 'Payments 1',
|
||||
svgIcon: dollarIcon,
|
||||
id: 80,
|
||||
parentId: 8
|
||||
}, {
|
||||
separator: true
|
||||
}, {
|
||||
text: 'Support',
|
||||
svgIcon: starOutlineIcon,
|
||||
id: 9,
|
||||
}];
|
||||
@@ -0,0 +1,238 @@
|
||||
<div class="login-page-container">
|
||||
<!-- Background Elements -->
|
||||
<div class="background-shapes">
|
||||
<div class="shape shape-1"></div>
|
||||
<div class="shape shape-2"></div>
|
||||
<div class="shape shape-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="login-content">
|
||||
<!-- Left Side - Branding -->
|
||||
<div class="branding-section">
|
||||
<div class="branding-content">
|
||||
<div class="logo-container">
|
||||
<img src="assets/rbj-logo.svg" alt="RBJ Logo" class="logo-image">
|
||||
<div class="logo-text">
|
||||
<h1>RBJ Identity</h1>
|
||||
<span class="tagline">Escrow Management Portal</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="welcome-text">
|
||||
<h2>Welcome Back</h2>
|
||||
<p>Access your escrow transactions, manage client communications, and track document workflows
|
||||
securely.</p>
|
||||
</div>
|
||||
|
||||
<div class="features-list">
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">🔒</div>
|
||||
<span>Secure Escrow Management</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">💬</div>
|
||||
<span>Client Communication</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">📄</div>
|
||||
<span>Document Management</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">📋</div>
|
||||
<span>Task Tracking</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Side - Login Form -->
|
||||
<div class="login-section">
|
||||
<div class="login-card">
|
||||
<!-- Initial State -->
|
||||
<div *ngIf="!showLoginForm" class="initial-state">
|
||||
<div class="login-header">
|
||||
<h3>Access Your Account</h3>
|
||||
<p>Sign in to manage your escrow transactions and client communications</p>
|
||||
</div>
|
||||
|
||||
<div class="login-actions">
|
||||
<button kendoButton themeColor="primary" size="large" (click)="showLoginFormView()"
|
||||
class="signin-button">
|
||||
<span class="button-content">
|
||||
<svg class="button-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path>
|
||||
<polyline points="10,17 15,12 10,7"></polyline>
|
||||
<line x1="15" y1="12" x2="3" y2="12"></line>
|
||||
</svg>
|
||||
Sign In
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login Form State -->
|
||||
<div *ngIf="showLoginForm" class="login-form-state">
|
||||
<div class="login-header">
|
||||
<button class="back-button" (click)="goBackToInitialState()" title="Go back">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="15,18 9,12 15,6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<h3>Sign In</h3>
|
||||
<p>Enter your credentials to access your account</p>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" class="login-form">
|
||||
<!-- Error Message -->
|
||||
<div *ngIf="showError" class="error-message">
|
||||
<svg class="error-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||
</svg>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<!-- Email Field -->
|
||||
<div class="form-field">
|
||||
<kendo-label for="email">Email Address</kendo-label>
|
||||
<kendo-textbox id="email" formControlName="email" placeholder="Enter your email address"
|
||||
[clearButton]="false">
|
||||
</kendo-textbox>
|
||||
<div *ngIf="emailControl?.invalid && emailControl?.touched" class="field-error">
|
||||
<span *ngIf="emailControl?.errors?.['required']">Email is required</span>
|
||||
<span *ngIf="emailControl?.errors?.['email']">Please enter a valid email address</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="form-field">
|
||||
<kendo-label for="password">Password</kendo-label>
|
||||
<kendo-textbox id="password" formControlName="password" placeholder="Enter your password"
|
||||
type="password" [clearButton]="false">
|
||||
</kendo-textbox>
|
||||
<div *ngIf="passwordControl?.invalid && passwordControl?.touched" class="field-error">
|
||||
<span *ngIf="passwordControl?.errors?.['required']">Password is required</span>
|
||||
<span *ngIf="passwordControl?.errors?.['minlength']">Password must be at least 6
|
||||
characters</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="form-field checkbox-field">
|
||||
<label class="checkbox-container">
|
||||
<kendo-checkbox formControlName="rememberMe"></kendo-checkbox>
|
||||
<span class="checkbox-label">Remember me</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="form-actions">
|
||||
<button kendoButton themeColor="primary" size="large" type="submit"
|
||||
[disabled]="loginForm.invalid || isProcessing" class="submit-button">
|
||||
<span class="button-content">
|
||||
<kendo-loader *ngIf="isProcessing" size="small"></kendo-loader>
|
||||
<svg *ngIf="!isProcessing" class="button-icon" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path>
|
||||
<polyline points="10,17 15,12 10,7"></polyline>
|
||||
<line x1="15" y1="12" x2="3" y2="12"></line>
|
||||
</svg>
|
||||
{{ isProcessing ? 'Signing In...' : 'Sign In' }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Demo Credentials *ngIf="!showLoginForm"-->
|
||||
<div class="demo-section" *ngIf="false">
|
||||
<div class="demo-header">
|
||||
<svg class="demo-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 12l2 2 4-4"></path>
|
||||
<path d="M21 12c-1 0-3-1-3-3s2-3 3-3 3 1 3 3-2 3-3 3"></path>
|
||||
<path d="M3 12c1 0 3-1 3-3s-2-3-3-3-3 1-3 3 2 3 3 3"></path>
|
||||
<path d="M12 3c0 1-1 3-3 3s-3-2-3-3 1-3 3-3 3 2 3 3"></path>
|
||||
<path d="M12 21c0-1 1-3 3-3s3 2 3 3-1 3-3 3-3-2-3-3"></path>
|
||||
</svg>
|
||||
<span>Demo Access</span>
|
||||
</div>
|
||||
|
||||
<div class="credential-tabs">
|
||||
<button class="tab-button active" (click)="setActiveTab('user')"
|
||||
[class.active]="activeTab === 'user'">
|
||||
Client Access
|
||||
</button>
|
||||
<button class="tab-button" (click)="setActiveTab('admin')"
|
||||
[class.active]="activeTab === 'admin'">
|
||||
Admin Access
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="credential-content" *ngIf="activeTab === 'user'">
|
||||
<div class="credential-item">
|
||||
<span class="label">Client Email:</span>
|
||||
<span class="value">client@example.com</span>
|
||||
<button class="copy-btn" (click)="copyToClipboard('client@example.com')" title="Copy email">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="credential-item">
|
||||
<span class="label">Password:</span>
|
||||
<span class="value">password123</span>
|
||||
<button class="copy-btn" (click)="copyToClipboard('password123')" title="Copy password">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="credential-content" *ngIf="activeTab === 'admin'">
|
||||
<div class="credential-item">
|
||||
<span class="label">Admin Email:</span>
|
||||
<span class="value">admin@example.com</span>
|
||||
<button class="copy-btn" (click)="copyToClipboard('admin@example.com')" title="Copy email">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="credential-item">
|
||||
<span class="label">Password:</span>
|
||||
<span class="value">password123</span>
|
||||
<button class="copy-btn" (click)="copyToClipboard('password123')" title="Copy password">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="credential-item">
|
||||
<span class="label">Security Code:</span>
|
||||
<span class="value">123456</span>
|
||||
<button class="copy-btn" (click)="copyToClipboard('123456')" title="Copy security code">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MFA Dialog -->
|
||||
<app-mfa-dialog #mfaDialog (mfaSuccess)="onMfaSuccess($event)" (mfaCancel)="onMfaCancel()">
|
||||
</app-mfa-dialog>
|
||||
</div>
|
||||
@@ -0,0 +1,727 @@
|
||||
.login-page-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 50%, #3b82f6 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
// Background Shapes
|
||||
.background-shapes {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
|
||||
&.shape-1 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
&.shape-2 {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
top: 60%;
|
||||
right: 15%;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
&.shape-3 {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
bottom: 20%;
|
||||
left: 20%;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Main Content
|
||||
.login-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
// Branding Section
|
||||
.branding-section {
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 50%, #3b82f6 100%);
|
||||
padding: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="white" opacity="0.1"/><circle cx="75" cy="75" r="1" fill="white" opacity="0.1"/><circle cx="50" cy="10" r="0.5" fill="white" opacity="0.1"/><circle cx="10" cy="60" r="0.5" fill="white" opacity="0.1"/><circle cx="90" cy="40" r="0.5" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.branding-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 2rem;
|
||||
gap: 1rem;
|
||||
|
||||
.logo-image {
|
||||
height: 60px;
|
||||
width: auto;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 1rem;
|
||||
opacity: 0.9;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 1rem 0;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.features-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 1.5rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Login Section
|
||||
.login-section {
|
||||
padding: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
h3 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
margin: 0 0 0.5rem 0;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.signin-button {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 50%, #3b82f6 100%);
|
||||
border: none;
|
||||
box-shadow: 0 8px 25px rgba(30, 58, 138, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 35px rgba(30, 58, 138, 0.4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.button-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// Demo Section
|
||||
.demo-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #495057;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
|
||||
.demo-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.credential-tabs {
|
||||
display: flex;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: #6c757d;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
background: #1e40af;
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(30, 64, 175, 0.2);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
background: #f8f9fa;
|
||||
color: #495057;
|
||||
}
|
||||
}
|
||||
|
||||
.credential-content {
|
||||
.credential-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #1e40af;
|
||||
box-shadow: 0 2px 8px rgba(30, 64, 175, 0.1);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
min-width: 60px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
||||
font-size: 0.9rem;
|
||||
color: #1a1a1a;
|
||||
background: #f8f9fa;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: #1e40af;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: #1e3a8a;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Login Form Styles
|
||||
.login-form-state {
|
||||
.login-header {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
.back-button {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: #f8f9fa;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 1.5rem 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
.error-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #fecaca;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
|
||||
.error-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
kendo-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
kendo-textbox {
|
||||
width: 100%;
|
||||
|
||||
.k-textbox {
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #e5e7eb;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:focus {
|
||||
border-color: #1e40af;
|
||||
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
|
||||
}
|
||||
|
||||
&.k-invalid {
|
||||
border-color: #dc2626;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-error {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
color: #dc2626;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.checkbox-field {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
kendo-checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 0.9rem;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:hover .checkbox-label {
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
// Style when checkbox is checked
|
||||
&:has(kendo-checkbox:checked) .checkbox-label {
|
||||
color: #1e40af;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 2rem;
|
||||
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 50%, #3b82f6 100%);
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(30, 58, 138, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(30, 58, 138, 0.4);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.button-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile Responsive
|
||||
@media (max-width: 768px) {
|
||||
.login-page-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.login-content {
|
||||
grid-template-columns: 1fr;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.branding-section {
|
||||
padding: 2rem 1.5rem;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.login-section {
|
||||
padding: 2rem 1.5rem;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
.logo-text h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.features-list {
|
||||
.feature-item {
|
||||
padding: 0.5rem;
|
||||
|
||||
.feature-icon {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-header h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.signin-button {
|
||||
height: 50px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.credential-tabs {
|
||||
.tab-button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
.credential-content {
|
||||
.credential-item {
|
||||
padding: 0.5rem;
|
||||
|
||||
.label {
|
||||
min-width: 50px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Login form mobile styles
|
||||
.login-form-state {
|
||||
.login-header {
|
||||
.back-button {
|
||||
padding: 0.4rem;
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
.form-field {
|
||||
margin-bottom: 1.25rem;
|
||||
|
||||
kendo-textbox .k-textbox {
|
||||
height: 44px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions .submit-button {
|
||||
height: 44px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-page-container {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.branding-section,
|
||||
.login-section {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.logo-container .logo-text h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.welcome-text h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.features-list {
|
||||
.feature-item {
|
||||
.feature-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Login form extra small mobile styles
|
||||
.login-form {
|
||||
.form-field {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
kendo-textbox .k-textbox {
|
||||
height: 40px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions .submit-button {
|
||||
height: 40px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DialogModule, DialogService } from '@progress/kendo-angular-dialog';
|
||||
import { ButtonsModule } from '@progress/kendo-angular-buttons';
|
||||
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { InputsModule } from '@progress/kendo-angular-inputs';
|
||||
import { LabelModule } from '@progress/kendo-angular-label';
|
||||
import { IndicatorsModule } from '@progress/kendo-angular-indicators';
|
||||
import { MfaDialogComponent } from '../../shared/mfa-dialog/mfa-dialog.component';
|
||||
import { AuthService, LoginCredentials, LoginResultType, TokenVerificationResult } from '../../shared/services/auth.service';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login-page',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
DialogModule,
|
||||
ButtonsModule,
|
||||
ReactiveFormsModule,
|
||||
InputsModule,
|
||||
LabelModule,
|
||||
IndicatorsModule,
|
||||
MfaDialogComponent
|
||||
],
|
||||
templateUrl: './login-page.component.html',
|
||||
styleUrls: ['./login-page.component.scss']
|
||||
})
|
||||
export class LoginPage implements OnInit {
|
||||
@ViewChild('mfaDialog') mfaDialog!: MfaDialogComponent;
|
||||
|
||||
activeTab: 'user' | 'admin' = 'user';
|
||||
showLoginForm = false;
|
||||
loginForm: FormGroup;
|
||||
isProcessing = false;
|
||||
showError = false;
|
||||
errorMessage = '';
|
||||
|
||||
constructor(
|
||||
private dialogService: DialogService,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private fb: FormBuilder
|
||||
) {
|
||||
this.loginForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required, Validators.minLength(6)]],
|
||||
rememberMe: [false]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Check if user is already logged in
|
||||
if (this.authService.isAuthenticated()) {
|
||||
this.redirectToDashboard();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for token in URL parameters
|
||||
this.route.queryParams.subscribe(params => {
|
||||
const token = params['token'];
|
||||
if (token) {
|
||||
this.verifySecretLinkToken(token);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setActiveTab(tab: 'user' | 'admin'): void {
|
||||
this.activeTab = tab;
|
||||
}
|
||||
|
||||
copyToClipboard(text: string): void {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
// You could add a toast notification here
|
||||
console.log('Copied to clipboard:', text);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy text: ', err);
|
||||
});
|
||||
}
|
||||
|
||||
showLoginFormView(): void {
|
||||
this.showLoginForm = true;
|
||||
// Focus on email input when form appears
|
||||
setTimeout(() => {
|
||||
const emailInput = document.querySelector('input[formControlName="email"]') as HTMLInputElement;
|
||||
if (emailInput) {
|
||||
emailInput.focus();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
goBackToInitialState(): void {
|
||||
this.showLoginForm = false;
|
||||
this.loginForm.reset();
|
||||
this.showError = false;
|
||||
this.errorMessage = '';
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.loginForm.valid && !this.isProcessing) {
|
||||
this.isProcessing = true;
|
||||
this.showError = false;
|
||||
|
||||
const credentials: LoginCredentials = this.loginForm.value;
|
||||
|
||||
this.authService.login(credentials).subscribe({
|
||||
next: (result) => {
|
||||
this.isProcessing = false;
|
||||
|
||||
if (result.result === LoginResultType.Success) {
|
||||
this.authService.setCurrentUser(result.responseData!);
|
||||
this.redirectToDashboard();
|
||||
} else if (result.result === LoginResultType.MfaRequired) {
|
||||
this.showMfaDialog(credentials);
|
||||
} else {
|
||||
this.showError = true;
|
||||
this.errorMessage = result.message || 'Invalid email or password';
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
this.isProcessing = false;
|
||||
this.showError = true;
|
||||
this.errorMessage = 'An error occurred during login. Please try again.';
|
||||
console.error('Login error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get emailControl() {
|
||||
return this.loginForm.get('email');
|
||||
}
|
||||
|
||||
get passwordControl() {
|
||||
return this.loginForm.get('password');
|
||||
}
|
||||
|
||||
private showMfaDialog(credentials: LoginCredentials): void {
|
||||
if (this.mfaDialog) {
|
||||
// Set the login data for MFA dialog
|
||||
(this.mfaDialog as any).loginData = credentials;
|
||||
|
||||
// Show MFA dialog
|
||||
this.mfaDialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
onMfaSuccess(userData: any): void {
|
||||
this.authService.setCurrentUser(userData);
|
||||
this.redirectToDashboard();
|
||||
}
|
||||
|
||||
onMfaCancel(): void {
|
||||
// Reset form and focus on email
|
||||
this.loginForm.reset();
|
||||
setTimeout(() => {
|
||||
const emailInput = document.querySelector('input[formControlName="email"]') as HTMLInputElement;
|
||||
if (emailInput) {
|
||||
emailInput.focus();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
|
||||
private verifySecretLinkToken(token: string): void {
|
||||
this.isProcessing = true;
|
||||
this.showError = false;
|
||||
|
||||
// First check if token is expired locally
|
||||
if (this.authService.isTokenExpired(token)) {
|
||||
this.isProcessing = false;
|
||||
this.showError = true;
|
||||
this.errorMessage = 'This link has expired. Please request a new one.';
|
||||
return;
|
||||
}
|
||||
|
||||
this.authService.verifySecretLinkToken(token).subscribe({
|
||||
next: (result: TokenVerificationResult) => {
|
||||
this.isProcessing = false;
|
||||
|
||||
if (result.isValid && result.user) {
|
||||
// Token is valid, set user and redirect
|
||||
this.authService.setCurrentUser(result.user);
|
||||
this.redirectToDashboard();
|
||||
} else {
|
||||
// Token verification failed
|
||||
this.showError = true;
|
||||
this.errorMessage = result.message || 'Invalid or expired link. Please request a new one.';
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
this.isProcessing = false;
|
||||
this.showError = true;
|
||||
this.errorMessage = 'An error occurred while verifying the link. Please try again.';
|
||||
console.error('Token verification error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private redirectToDashboard(): void {
|
||||
const redirectUrl = this.authService.getRedirectUrl();
|
||||
this.router.navigate([redirectUrl || '/dashboard']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user