fixed rout
This commit is contained in:
13
src/app/app.config.ts
Normal file
13
src/app/app.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideHttpClient, withFetch } from '@angular/common/http';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideRouter(routes),
|
||||
provideHttpClient(withFetch())
|
||||
]
|
||||
};
|
||||
1
src/app/app.html
Normal file
1
src/app/app.html
Normal file
@@ -0,0 +1 @@
|
||||
<router-outlet></router-outlet>
|
||||
38
src/app/app.routes.ts
Normal file
38
src/app/app.routes.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { authGuard, loginGuard } from './guards/auth.guard';
|
||||
import { LoginComponent } from './components/login/login.component';
|
||||
import { DashboardComponent } from './components/dashboard/dashboard.component';
|
||||
import { ActiveChecksComponent } from './components/active-checks/active-checks.component';
|
||||
import { HistoryComponent } from './components/history/history.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: '/login',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
component: LoginComponent,
|
||||
canActivate: [loginGuard]
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: DashboardComponent,
|
||||
canActivate: [authGuard]
|
||||
},
|
||||
{
|
||||
path: 'active-checks',
|
||||
component: ActiveChecksComponent,
|
||||
canActivate: [authGuard]
|
||||
},
|
||||
{
|
||||
path: 'history',
|
||||
component: HistoryComponent,
|
||||
canActivate: [authGuard]
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: '/login'
|
||||
}
|
||||
];
|
||||
18
src/app/app.scss
Normal file
18
src/app/app.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
margin: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
23
src/app/app.spec.ts
Normal file
23
src/app/app.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { App } from './app';
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [App],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render title', async () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, FastCheck');
|
||||
});
|
||||
});
|
||||
12
src/app/app.ts
Normal file
12
src/app/app.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.scss'
|
||||
})
|
||||
export class App {
|
||||
protected readonly title = signal('FastCheck');
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<div class="page-container">
|
||||
<header class="header">
|
||||
<div class="logo">FastCheck</div>
|
||||
<nav class="nav">
|
||||
<a routerLink="/dashboard" class="nav-link">Dashboard</a>
|
||||
<a routerLink="/active-checks" class="nav-link active">Active Checks</a>
|
||||
<a routerLink="/history" class="nav-link">History</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="page-header">
|
||||
<h1>Active FastChecks</h1>
|
||||
<p>View all your unused FastChecks</p>
|
||||
</div>
|
||||
|
||||
@if (isLoading()) {
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading active checks...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<div class="error-card">
|
||||
<p>{{ error() }}</p>
|
||||
<button (click)="loadActiveChecks()" class="btn-retry">Retry</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!isLoading() && !error()) {
|
||||
@if (checks().length === 0) {
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📭</div>
|
||||
<h3>No Active Checks</h3>
|
||||
<p>You don't have any active FastChecks at the moment.</p>
|
||||
<a routerLink="/dashboard" class="btn-primary">Create FastCheck</a>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="checks-grid">
|
||||
@for (check of checks(); track check.fastcheck) {
|
||||
<div class="check-card">
|
||||
<div class="check-header">
|
||||
<span class="check-badge">Active</span>
|
||||
<span class="check-amount">{{ formatAmount(check.amount) }} ₽</span>
|
||||
</div>
|
||||
|
||||
<div class="check-details">
|
||||
<div class="detail-item">
|
||||
<span class="label">FastCheck Number</span>
|
||||
<div class="value-copy">
|
||||
<span class="value">{{ check.fastcheck }}</span>
|
||||
<button
|
||||
(click)="copyToClipboard(check.fastcheck, 'Number')"
|
||||
class="btn-copy"
|
||||
title="Copy">
|
||||
📋
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<span class="label">Code</span>
|
||||
<div class="value-copy">
|
||||
<span class="value code">{{ check.code }}</span>
|
||||
<button
|
||||
(click)="copyToClipboard(check.code!, 'Code')"
|
||||
class="btn-copy"
|
||||
title="Copy">
|
||||
📋
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<span class="label">Created</span>
|
||||
<span class="value">{{ check.createdAt | date:'short' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<span class="label">Expires</span>
|
||||
<span class="value">{{ check.expiration | date:'short' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="check-warning">
|
||||
⚠️ Keep this information secure. Anyone with these credentials can claim the money.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
280
src/app/components/active-checks/active-checks.component.scss
Normal file
280
src/app/components/active-checks/active-checks.component.scss
Normal file
@@ -0,0 +1,280 @@
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
padding: 20px 40px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #667eea;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #667eea;
|
||||
background: #e8ebff;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 40px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 40px;
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||||
|
||||
p {
|
||||
color: #c33;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-retry {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #764ba2;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-block;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 14px 30px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #764ba2;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.checks-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 30px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.check-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||||
border: 2px solid #e8ebff;
|
||||
transition: all 0.3s;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.check-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.check-badge {
|
||||
background: #52c41a;
|
||||
color: white;
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.check-amount {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.check-details {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
|
||||
&.code {
|
||||
font-size: 20px;
|
||||
color: #667eea;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.value-copy {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-copy {
|
||||
background: #f0f0f0;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #e0e0e0;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.check-warning {
|
||||
background: #fffbe6;
|
||||
border-left: 4px solid #faad14;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
51
src/app/components/active-checks/active-checks.component.ts
Normal file
51
src/app/components/active-checks/active-checks.component.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Component, OnInit, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { FastCheckService } from '../../services/fastcheck.service';
|
||||
import { FastCheck } from '../../models/fastcheck.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-active-checks',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink],
|
||||
templateUrl: './active-checks.component.html',
|
||||
styleUrls: ['./active-checks.component.scss']
|
||||
})
|
||||
export class ActiveChecksComponent implements OnInit {
|
||||
checks = signal<FastCheck[]>([]);
|
||||
isLoading = signal<boolean>(true);
|
||||
error = signal<string>('');
|
||||
|
||||
constructor(private fastCheckService: FastCheckService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadActiveChecks();
|
||||
}
|
||||
|
||||
loadActiveChecks(): void {
|
||||
this.isLoading.set(true);
|
||||
this.error.set('');
|
||||
|
||||
this.fastCheckService.getActiveFastChecks().subscribe({
|
||||
next: (response) => {
|
||||
this.checks.set(response.checks);
|
||||
this.isLoading.set(false);
|
||||
},
|
||||
error: (err) => {
|
||||
this.error.set('Failed to load active checks');
|
||||
this.isLoading.set(false);
|
||||
console.error('Load error:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
formatAmount(amount: number): string {
|
||||
return new Intl.NumberFormat('ru-RU').format(amount);
|
||||
}
|
||||
|
||||
copyToClipboard(text: string, type: string): void {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
alert(`${type} copied to clipboard!`);
|
||||
});
|
||||
}
|
||||
}
|
||||
142
src/app/components/dashboard/dashboard.component.html
Normal file
142
src/app/components/dashboard/dashboard.component.html
Normal file
@@ -0,0 +1,142 @@
|
||||
<div class="dashboard-container">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="logo">FastCheck</div>
|
||||
<nav class="nav">
|
||||
<a routerLink="/dashboard" class="nav-link active">Dashboard</a>
|
||||
<a routerLink="/active-checks" class="nav-link">Active Checks</a>
|
||||
<a routerLink="/history" class="nav-link">History</a>
|
||||
<button (click)="logout()" class="btn-logout">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<!-- Balance Card -->
|
||||
<div class="balance-card">
|
||||
@if (isLoadingBalance()) {
|
||||
<div class="loading-small">
|
||||
<div class="spinner-small"></div>
|
||||
</div>
|
||||
} @else if (balance()) {
|
||||
<div class="balance-info">
|
||||
<span class="balance-label">Current Balance</span>
|
||||
<h2 class="balance-amount">{{ formatAmount(balance()!.balance) }} ₽</h2>
|
||||
<button (click)="topUpBalance()" class="btn-topup">+ Top Up Balance</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="actions-grid">
|
||||
<!-- Create FastCheck -->
|
||||
<div class="card">
|
||||
<h3 class="card-title">Create New FastCheck</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Amount (RUB)</label>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="createAmount"
|
||||
placeholder="Enter amount"
|
||||
class="input"
|
||||
[disabled]="isCreating()">
|
||||
</div>
|
||||
|
||||
@if (createError()) {
|
||||
<div class="error-message">{{ createError() }}</div>
|
||||
}
|
||||
|
||||
<button
|
||||
(click)="createFastCheck()"
|
||||
[disabled]="isCreating() || !createAmount()"
|
||||
class="btn-primary">
|
||||
@if (isCreating()) {
|
||||
<span>Creating...</span>
|
||||
} @else {
|
||||
<span>Create FastCheck</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Accept FastCheck -->
|
||||
<div class="card">
|
||||
<h3 class="card-title">Accept FastCheck</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>FastCheck Number</label>
|
||||
<input
|
||||
type="text"
|
||||
[value]="acceptNumber()"
|
||||
(input)="onFastCheckNumberInput($event)"
|
||||
placeholder="xxxx-xxxx-xxxx"
|
||||
maxlength="14"
|
||||
class="input"
|
||||
[disabled]="isAccepting()">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Code</label>
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="acceptCode"
|
||||
placeholder="Enter 4-digit code"
|
||||
maxlength="4"
|
||||
class="input"
|
||||
[disabled]="isAccepting()">
|
||||
</div>
|
||||
|
||||
@if (acceptError()) {
|
||||
<div class="error-message">{{ acceptError() }}</div>
|
||||
}
|
||||
|
||||
@if (acceptSuccess()) {
|
||||
<div class="success-message">FastCheck accepted successfully!</div>
|
||||
}
|
||||
|
||||
<button
|
||||
(click)="acceptFastCheck()"
|
||||
[disabled]="isAccepting() || !acceptNumber() || !acceptCode()"
|
||||
class="btn-primary">
|
||||
@if (isAccepting()) {
|
||||
<span>Accepting...</span>
|
||||
} @else {
|
||||
<span>Accept FastCheck</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Created Check Modal -->
|
||||
@if (createdCheck()) {
|
||||
<div class="modal-overlay" (click)="closeCreatedCheckModal()">
|
||||
<div class="modal" (click)="$event.stopPropagation()">
|
||||
<div class="modal-header">
|
||||
<h3>FastCheck Created!</h3>
|
||||
<button class="close-btn" (click)="closeCreatedCheckModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="check-details">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">FastCheck Number:</span>
|
||||
<span class="detail-value">{{ createdCheck()!.fastcheck }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Code:</span>
|
||||
<span class="detail-value code">{{ createdCheck()!.code }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Expires:</span>
|
||||
<span class="detail-value">{{ createdCheck()!.expiration | date:'short' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-note">
|
||||
<p>⚠️ Save this information securely. Anyone with the number and code can claim this FastCheck.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button (click)="closeCreatedCheckModal()" class="btn-primary">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
363
src/app/components/dashboard/dashboard.component.scss
Normal file
363
src/app/components/dashboard/dashboard.component.scss
Normal file
@@ -0,0 +1,363 @@
|
||||
.dashboard-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
padding: 20px 40px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 15px 20px;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #667eea;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #667eea;
|
||||
background: #e8ebff;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
background: #ff4d4f;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #ff7875;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 40px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
color: white;
|
||||
margin-bottom: 40px;
|
||||
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 30px 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.balance-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
margin: 10px 0 30px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 36px;
|
||||
margin: 10px 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-topup {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: 2px solid white;
|
||||
padding: 12px 30px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: white;
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 30px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 25px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
margin-top: 10px;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #764ba2;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #fee;
|
||||
color: #c33;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: #efe;
|
||||
color: #3c3;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.loading-small {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
|
||||
.spinner-small {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 3px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// Modal
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 30px 30px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.check-details {
|
||||
background: #f9f9f9;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
|
||||
&.code {
|
||||
font-size: 20px;
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-note {
|
||||
background: #fffbe6;
|
||||
border-left: 4px solid #faad14;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20px 30px 30px;
|
||||
text-align: center;
|
||||
}
|
||||
169
src/app/components/dashboard/dashboard.component.ts
Normal file
169
src/app/components/dashboard/dashboard.component.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Component, OnInit, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { FastCheckService } from '../../services/fastcheck.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { Balance, CreateFastCheckResponse } from '../../models/fastcheck.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, RouterLink],
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrls: ['./dashboard.component.scss']
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
balance = signal<Balance | null>(null);
|
||||
isLoadingBalance = signal<boolean>(true);
|
||||
|
||||
// Create FastCheck
|
||||
createAmount = signal<number>(0);
|
||||
isCreating = signal<boolean>(false);
|
||||
createdCheck = signal<CreateFastCheckResponse | null>(null);
|
||||
createError = signal<string>('');
|
||||
|
||||
// Accept FastCheck
|
||||
acceptNumber = signal<string>('');
|
||||
acceptCode = signal<string>('');
|
||||
isAccepting = signal<boolean>(false);
|
||||
acceptSuccess = signal<boolean>(false);
|
||||
acceptError = signal<string>('');
|
||||
|
||||
constructor(
|
||||
private fastCheckService: FastCheckService,
|
||||
private authService: AuthService,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadBalance();
|
||||
}
|
||||
|
||||
loadBalance(): void {
|
||||
this.isLoadingBalance.set(true);
|
||||
this.fastCheckService.getBalance().subscribe({
|
||||
next: (balance) => {
|
||||
this.balance.set(balance);
|
||||
this.isLoadingBalance.set(false);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Failed to load balance:', err);
|
||||
this.isLoadingBalance.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createFastCheck(): void {
|
||||
const amount = this.createAmount();
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
this.createError.set('Please enter a valid amount');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentBalance = this.balance();
|
||||
if (currentBalance && amount > currentBalance.balance) {
|
||||
this.createError.set('Insufficient balance');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isCreating.set(true);
|
||||
this.createError.set('');
|
||||
this.createdCheck.set(null);
|
||||
|
||||
this.fastCheckService.createFastCheck({
|
||||
amount: amount,
|
||||
currency: 'RUB'
|
||||
}).subscribe({
|
||||
next: (response) => {
|
||||
this.createdCheck.set(response);
|
||||
this.isCreating.set(false);
|
||||
this.createAmount.set(0);
|
||||
this.loadBalance(); // Refresh balance
|
||||
},
|
||||
error: (err) => {
|
||||
this.createError.set('Failed to create FastCheck. Please try again.');
|
||||
this.isCreating.set(false);
|
||||
console.error('Create error:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
acceptFastCheck(): void {
|
||||
const number = this.acceptNumber().trim();
|
||||
const code = this.acceptCode().trim();
|
||||
|
||||
if (!number || !code) {
|
||||
this.acceptError.set('Please enter both FastCheck number and code');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isAccepting.set(true);
|
||||
this.acceptError.set('');
|
||||
this.acceptSuccess.set(false);
|
||||
|
||||
this.fastCheckService.acceptFastCheck({
|
||||
fastcheck: number,
|
||||
code: code
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
this.acceptSuccess.set(true);
|
||||
this.isAccepting.set(false);
|
||||
this.acceptNumber.set('');
|
||||
this.acceptCode.set('');
|
||||
this.loadBalance(); // Refresh balance
|
||||
|
||||
setTimeout(() => {
|
||||
this.acceptSuccess.set(false);
|
||||
}, 3000);
|
||||
},
|
||||
error: (err) => {
|
||||
this.acceptError.set('Failed to accept FastCheck. Check your credentials.');
|
||||
this.isAccepting.set(false);
|
||||
console.error('Accept error:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
formatAmount(amount: number): string {
|
||||
return new Intl.NumberFormat('ru-RU').format(amount);
|
||||
}
|
||||
|
||||
formatFastCheckNumber(input: string): string {
|
||||
const cleaned = input.replace(/\D/g, '');
|
||||
const formatted = cleaned.match(/.{1,4}/g)?.join('-') || '';
|
||||
return formatted.slice(0, 14); // xxxx-xxxx-xxxx
|
||||
}
|
||||
|
||||
onFastCheckNumberInput(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const formatted = this.formatFastCheckNumber(input.value);
|
||||
this.acceptNumber.set(formatted);
|
||||
}
|
||||
|
||||
closeCreatedCheckModal(): void {
|
||||
this.createdCheck.set(null);
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
const sessionId = this.authService.getSessionId();
|
||||
if (sessionId) {
|
||||
this.authService.deleteWebSession(sessionId).subscribe({
|
||||
next: () => {
|
||||
this.router.navigate(['/login']);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Logout error:', err);
|
||||
this.authService.clearAuthentication();
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
topUpBalance(): void {
|
||||
// TODO: Implement bank integration
|
||||
alert('Bank integration will be implemented. You will be redirected to bank payment page.');
|
||||
}
|
||||
}
|
||||
86
src/app/components/history/history.component.html
Normal file
86
src/app/components/history/history.component.html
Normal file
@@ -0,0 +1,86 @@
|
||||
<div class="page-container">
|
||||
<header class="header">
|
||||
<div class="logo">FastCheck</div>
|
||||
<nav class="nav">
|
||||
<a routerLink="/dashboard" class="nav-link">Dashboard</a>
|
||||
<a routerLink="/active-checks" class="nav-link">Active Checks</a>
|
||||
<a routerLink="/history" class="nav-link active">History</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="page-header">
|
||||
<h1>Transaction History</h1>
|
||||
<p>View all used and expired FastChecks</p>
|
||||
</div>
|
||||
|
||||
@if (isLoading()) {
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading history...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<div class="error-card">
|
||||
<p>{{ error() }}</p>
|
||||
<button (click)="loadHistory()" class="btn-retry">Retry</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!isLoading() && !error()) {
|
||||
@if (checks().length === 0) {
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📜</div>
|
||||
<h3>No History</h3>
|
||||
<p>Your transaction history will appear here.</p>
|
||||
<a routerLink="/dashboard" class="btn-primary">Go to Dashboard</a>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="history-list">
|
||||
@for (check of checks(); track check.fastcheck) {
|
||||
<div class="history-item">
|
||||
<div class="item-header">
|
||||
<div class="item-info">
|
||||
<span [class]="'type-badge ' + getTypeClass(check.type)">
|
||||
{{ getTypeLabel(check.type) }}
|
||||
</span>
|
||||
<span class="item-number">{{ check.fastcheck }}</span>
|
||||
</div>
|
||||
<span class="item-amount">{{ formatAmount(check.amount) }} ₽</span>
|
||||
</div>
|
||||
|
||||
<div class="item-details">
|
||||
@if (check.createdAt) {
|
||||
<div class="detail">
|
||||
<span class="detail-label">Created:</span>
|
||||
<span class="detail-value">{{ check.createdAt | date:'short' }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (check.usedAt) {
|
||||
<div class="detail">
|
||||
<span class="detail-label">Used:</span>
|
||||
<span class="detail-value">{{ check.usedAt | date:'short' }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (check.acceptedAt) {
|
||||
<div class="detail">
|
||||
<span class="detail-label">Accepted:</span>
|
||||
<span class="detail-value">{{ check.acceptedAt | date:'short' }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="detail">
|
||||
<span class="detail-label">Status:</span>
|
||||
<span class="status-badge">{{ check.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
270
src/app/components/history/history.component.scss
Normal file
270
src/app/components/history/history.component.scss
Normal file
@@ -0,0 +1,270 @@
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
padding: 20px 40px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #667eea;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #667eea;
|
||||
background: #e8ebff;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 40px;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 40px;
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||||
|
||||
p {
|
||||
color: #c33;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-retry {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #764ba2;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-block;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 14px 30px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #764ba2;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
|
||||
&.type-created {
|
||||
background: #e8ebff;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
&.type-accepted {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.item-number {
|
||||
font-family: monospace;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-amount {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
background: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
53
src/app/components/history/history.component.ts
Normal file
53
src/app/components/history/history.component.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Component, OnInit, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { FastCheckService } from '../../services/fastcheck.service';
|
||||
import { FastCheck } from '../../models/fastcheck.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-history',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink],
|
||||
templateUrl: './history.component.html',
|
||||
styleUrls: ['./history.component.scss']
|
||||
})
|
||||
export class HistoryComponent implements OnInit {
|
||||
checks = signal<FastCheck[]>([]);
|
||||
isLoading = signal<boolean>(true);
|
||||
error = signal<string>('');
|
||||
|
||||
constructor(private fastCheckService: FastCheckService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadHistory();
|
||||
}
|
||||
|
||||
loadHistory(): void {
|
||||
this.isLoading.set(true);
|
||||
this.error.set('');
|
||||
|
||||
this.fastCheckService.getFastCheckHistory().subscribe({
|
||||
next: (response) => {
|
||||
this.checks.set(response.checks);
|
||||
this.isLoading.set(false);
|
||||
},
|
||||
error: (err) => {
|
||||
this.error.set('Failed to load history');
|
||||
this.isLoading.set(false);
|
||||
console.error('Load error:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
formatAmount(amount: number): string {
|
||||
return new Intl.NumberFormat('ru-RU').format(amount);
|
||||
}
|
||||
|
||||
getTypeLabel(type?: string): string {
|
||||
return type === 'created' ? 'Created' : 'Accepted';
|
||||
}
|
||||
|
||||
getTypeClass(type?: string): string {
|
||||
return type === 'created' ? 'type-created' : 'type-accepted';
|
||||
}
|
||||
}
|
||||
39
src/app/components/login/login.component.html
Normal file
39
src/app/components/login/login.component.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<h1 class="title">FastCheck</h1>
|
||||
<p class="subtitle">Scan QR code to login</p>
|
||||
|
||||
@if (isLoading()) {
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Generating QR code...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<div class="error-message">
|
||||
<p>{{ error() }}</p>
|
||||
<button (click)="refreshQR()" class="btn-secondary">Try Again</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (qrData() && !isLoading()) {
|
||||
<div class="qr-section">
|
||||
<div class="qr-wrapper">
|
||||
<qrcode
|
||||
[qrdata]="qrData()"
|
||||
[width]="250"
|
||||
[errorCorrectionLevel]="'M'">
|
||||
</qrcode>
|
||||
</div>
|
||||
|
||||
<div class="status-indicator">
|
||||
<div class="pulse"></div>
|
||||
<span>Waiting for scan...</span>
|
||||
</div>
|
||||
|
||||
<button (click)="refreshQR()" class="btn-link">Refresh QR Code</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
177
src/app/components/login/login.component.scss
Normal file
177
src/app/components/login/login.component.scss
Normal file
@@ -0,0 +1,177 @@
|
||||
.login-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 30px 20px;
|
||||
border-radius: 15px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 40px 0;
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 20px;
|
||||
background: #fee;
|
||||
border-radius: 10px;
|
||||
color: #c33;
|
||||
margin: 20px 0;
|
||||
|
||||
p {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-section {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.qr-wrapper {
|
||||
display: inline-block;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
::ng-deep canvas {
|
||||
max-width: 100%;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
margin: 20px 0;
|
||||
|
||||
.pulse {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #667eea;
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #667eea;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
|
||||
&:hover {
|
||||
color: #764ba2;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #764ba2;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
73
src/app/components/login/login.component.ts
Normal file
73
src/app/components/login/login.component.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Component, OnInit, OnDestroy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { QRCodeComponent } from 'angularx-qrcode';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
standalone: true,
|
||||
imports: [CommonModule, QRCodeComponent],
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss']
|
||||
})
|
||||
export class LoginComponent implements OnInit, OnDestroy {
|
||||
qrData = signal<string>('');
|
||||
sessionId = signal<string>('');
|
||||
isLoading = signal<boolean>(true);
|
||||
error = signal<string>('');
|
||||
|
||||
private pollSubscription?: Subscription;
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.createSession();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.pollSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
createSession(): void {
|
||||
this.isLoading.set(true);
|
||||
this.error.set('');
|
||||
|
||||
this.authService.createWebSession().subscribe({
|
||||
next: (session) => {
|
||||
this.sessionId.set(session.sessionId);
|
||||
this.qrData.set(`fastcheck://login?session=${session.sessionId}`);
|
||||
this.isLoading.set(false);
|
||||
this.startPolling(session.sessionId);
|
||||
},
|
||||
error: (err) => {
|
||||
this.error.set('Failed to create session. Please try again.');
|
||||
this.isLoading.set(false);
|
||||
console.error('Session creation error:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private startPolling(sessionId: string): void {
|
||||
this.pollSubscription = this.authService.startPolling(sessionId).subscribe({
|
||||
next: (session) => {
|
||||
if (session.Status) {
|
||||
this.router.navigate(['/dashboard']);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
this.error.set('Authentication failed. Please try again.');
|
||||
console.error('Polling error:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refreshQR(): void {
|
||||
this.pollSubscription?.unsubscribe();
|
||||
this.createSession();
|
||||
}
|
||||
}
|
||||
27
src/app/guards/auth.guard.ts
Normal file
27
src/app/guards/auth.guard.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { Router, CanActivateFn } from '@angular/router';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
export const authGuard: CanActivateFn = () => {
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
if (authService.isAuthenticated().isAuthenticated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
router.navigate(['/login']);
|
||||
return false;
|
||||
};
|
||||
|
||||
export const loginGuard: CanActivateFn = () => {
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
if (!authService.isAuthenticated().isAuthenticated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
router.navigate(['/dashboard']);
|
||||
return false;
|
||||
};
|
||||
9
src/app/models/api.model.ts
Normal file
9
src/app/models/api.model.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface ApiResponse<T = any> {
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface PingResponse {
|
||||
message: string;
|
||||
}
|
||||
44
src/app/models/fastcheck.model.ts
Normal file
44
src/app/models/fastcheck.model.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export interface FastCheck {
|
||||
fastcheck: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
code?: string;
|
||||
expiration: string;
|
||||
status: 'active' | 'used' | 'expired';
|
||||
createdAt?: string;
|
||||
usedAt?: string;
|
||||
acceptedAt?: string;
|
||||
type?: 'created' | 'accepted';
|
||||
}
|
||||
|
||||
export interface CreateFastCheckRequest {
|
||||
amount: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface CreateFastCheckResponse {
|
||||
fastcheck: string;
|
||||
expiration: string;
|
||||
code: string;
|
||||
Status: boolean;
|
||||
}
|
||||
|
||||
export interface AcceptFastCheckRequest {
|
||||
fastcheck: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface CheckStatusResponse {
|
||||
fastcheck: string;
|
||||
expiration: string;
|
||||
Status: boolean;
|
||||
}
|
||||
|
||||
export interface Balance {
|
||||
balance: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface FastCheckListResponse {
|
||||
checks: FastCheck[];
|
||||
}
|
||||
13
src/app/models/session.model.ts
Normal file
13
src/app/models/session.model.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface WebSession {
|
||||
sessionId: string;
|
||||
userId: string;
|
||||
expires: string;
|
||||
userSessionId: string;
|
||||
Status: boolean;
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
sessionId: string | null;
|
||||
userSessionId: string | null;
|
||||
}
|
||||
39
src/app/services/api.service.ts
Normal file
39
src/app/services/api.service.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
private readonly API_URL = 'https://api.fastcheck.store';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
ping(): Observable<{ message: string }> {
|
||||
return this.http.get<{ message: string }>(`${this.API_URL}/ping`);
|
||||
}
|
||||
|
||||
get<T>(path: string, sessionId?: string): Observable<T> {
|
||||
const headers = sessionId ? this.createAuthHeaders(sessionId) : undefined;
|
||||
return this.http.get<T>(`${this.API_URL}${path}`, { headers });
|
||||
}
|
||||
|
||||
post<T>(path: string, body: any, sessionId?: string): Observable<T> {
|
||||
const headers = sessionId ? this.createAuthHeaders(sessionId) : undefined;
|
||||
return this.http.post<T>(`${this.API_URL}${path}`, body, { headers });
|
||||
}
|
||||
|
||||
delete<T>(path: string, sessionId?: string): Observable<T> {
|
||||
const headers = sessionId ? this.createAuthHeaders(sessionId) : undefined;
|
||||
return this.http.delete<T>(`${this.API_URL}${path}`, { headers });
|
||||
}
|
||||
|
||||
private createAuthHeaders(sessionId: string): HttpHeaders {
|
||||
return new HttpHeaders({
|
||||
'Authorization': JSON.stringify({ sessionID: sessionId }),
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
}
|
||||
}
|
||||
77
src/app/services/auth.service.ts
Normal file
77
src/app/services/auth.service.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { Observable, interval, switchMap, takeWhile, tap } from 'rxjs';
|
||||
import { ApiService } from './api.service';
|
||||
import { WebSession, AuthState } from '../models/session.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private authState = signal<AuthState>({
|
||||
isAuthenticated: false,
|
||||
sessionId: null,
|
||||
userSessionId: null
|
||||
});
|
||||
|
||||
readonly isAuthenticated = this.authState.asReadonly();
|
||||
|
||||
constructor(private apiService: ApiService) {
|
||||
this.loadSessionFromStorage();
|
||||
}
|
||||
|
||||
createWebSession(): Observable<WebSession> {
|
||||
return this.apiService.get<WebSession>('/websession');
|
||||
}
|
||||
|
||||
checkWebSessionStatus(sessionId: string): Observable<WebSession> {
|
||||
return this.apiService.get<WebSession>(`/websession/${sessionId}`);
|
||||
}
|
||||
|
||||
startPolling(sessionId: string): Observable<WebSession> {
|
||||
return interval(2000).pipe(
|
||||
switchMap(() => this.checkWebSessionStatus(sessionId)),
|
||||
tap(session => {
|
||||
if (session.Status) {
|
||||
this.setAuthenticated(session);
|
||||
}
|
||||
}),
|
||||
takeWhile(session => !session.Status, true)
|
||||
);
|
||||
}
|
||||
|
||||
deleteWebSession(sessionId: string): Observable<any> {
|
||||
return this.apiService.delete(`/websession/${sessionId}`, sessionId).pipe(
|
||||
tap(() => this.clearAuthentication())
|
||||
);
|
||||
}
|
||||
|
||||
private setAuthenticated(session: WebSession): void {
|
||||
const state = {
|
||||
isAuthenticated: true,
|
||||
sessionId: session.sessionId,
|
||||
userSessionId: session.userSessionId
|
||||
};
|
||||
this.authState.set(state);
|
||||
sessionStorage.setItem('authState', JSON.stringify(state));
|
||||
}
|
||||
|
||||
private loadSessionFromStorage(): void {
|
||||
const stored = sessionStorage.getItem('authState');
|
||||
if (stored) {
|
||||
this.authState.set(JSON.parse(stored));
|
||||
}
|
||||
}
|
||||
|
||||
clearAuthentication(): void {
|
||||
this.authState.set({
|
||||
isAuthenticated: false,
|
||||
sessionId: null,
|
||||
userSessionId: null
|
||||
});
|
||||
sessionStorage.removeItem('authState');
|
||||
}
|
||||
|
||||
getSessionId(): string | null {
|
||||
return this.authState().sessionId;
|
||||
}
|
||||
}
|
||||
142
src/app/services/fastcheck.service.ts
Normal file
142
src/app/services/fastcheck.service.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { ApiService } from './api.service';
|
||||
import { AuthService } from './auth.service';
|
||||
import {
|
||||
FastCheck,
|
||||
CreateFastCheckRequest,
|
||||
CreateFastCheckResponse,
|
||||
AcceptFastCheckRequest,
|
||||
CheckStatusResponse,
|
||||
Balance,
|
||||
FastCheckListResponse
|
||||
} from '../models/fastcheck.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FastCheckService {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private authService: AuthService
|
||||
) {}
|
||||
|
||||
checkStatus(fastcheckNumber: string): Observable<CheckStatusResponse> {
|
||||
return this.apiService.post<CheckStatusResponse>(
|
||||
'/fastcheck',
|
||||
{ fastcheck: fastcheckNumber }
|
||||
);
|
||||
}
|
||||
|
||||
createFastCheck(request: CreateFastCheckRequest): Observable<CreateFastCheckResponse> {
|
||||
const sessionId = this.authService.getSessionId();
|
||||
if (!sessionId) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
return this.apiService.post<CreateFastCheckResponse>(
|
||||
'/fastcheck',
|
||||
request,
|
||||
sessionId
|
||||
);
|
||||
}
|
||||
|
||||
acceptFastCheck(request: AcceptFastCheckRequest): Observable<{ message: string }> {
|
||||
const sessionId = this.authService.getSessionId();
|
||||
if (!sessionId) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
return this.apiService.post<{ message: string }>(
|
||||
'/fastcheck',
|
||||
request,
|
||||
sessionId
|
||||
);
|
||||
}
|
||||
|
||||
// MOCKED - Backend needs to implement
|
||||
getBalance(): Observable<Balance> {
|
||||
const sessionId = this.authService.getSessionId();
|
||||
if (!sessionId) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
// TODO: Replace with real API call
|
||||
// return this.apiService.get<Balance>('/balance', sessionId);
|
||||
|
||||
// MOCK DATA
|
||||
return of({
|
||||
balance: 150000,
|
||||
currency: 'RUB'
|
||||
});
|
||||
}
|
||||
|
||||
// MOCKED - Backend needs to implement
|
||||
getActiveFastChecks(): Observable<FastCheckListResponse> {
|
||||
const sessionId = this.authService.getSessionId();
|
||||
if (!sessionId) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
// TODO: Replace with real API call
|
||||
// return this.apiService.get<FastCheckListResponse>('/fastcheck/active', sessionId);
|
||||
|
||||
// MOCK DATA
|
||||
return of({
|
||||
checks: [
|
||||
{
|
||||
fastcheck: '4568-1109-3402',
|
||||
amount: 15000,
|
||||
currency: 'RUB',
|
||||
code: '5568',
|
||||
expiration: '2026-01-26T09:08:18Z',
|
||||
status: 'active',
|
||||
createdAt: '2026-01-19T09:08:18Z'
|
||||
},
|
||||
{
|
||||
fastcheck: '7890-2234-5566',
|
||||
amount: 25000,
|
||||
currency: 'RUB',
|
||||
code: '1234',
|
||||
expiration: '2026-01-26T10:15:30Z',
|
||||
status: 'active',
|
||||
createdAt: '2026-01-19T10:15:30Z'
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// MOCKED - Backend needs to implement
|
||||
getFastCheckHistory(): Observable<FastCheckListResponse> {
|
||||
const sessionId = this.authService.getSessionId();
|
||||
if (!sessionId) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
// TODO: Replace with real API call
|
||||
// return this.apiService.get<FastCheckListResponse>('/fastcheck/history', sessionId);
|
||||
|
||||
// MOCK DATA
|
||||
return of({
|
||||
checks: [
|
||||
{
|
||||
fastcheck: '1234-5678-0003',
|
||||
amount: 5000,
|
||||
currency: 'RUB',
|
||||
type: 'created',
|
||||
createdAt: '2026-01-15T09:08:18Z',
|
||||
usedAt: '2026-01-15T10:20:00Z',
|
||||
status: 'used',
|
||||
expiration: '2026-01-22T09:08:18Z'
|
||||
},
|
||||
{
|
||||
fastcheck: '9876-5432-0100',
|
||||
amount: 10000,
|
||||
currency: 'RUB',
|
||||
type: 'accepted',
|
||||
acceptedAt: '2026-01-14T14:30:00Z',
|
||||
status: 'used',
|
||||
expiration: '2026-01-21T14:30:00Z'
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
4
src/environments/environment.ts
Normal file
4
src/environments/environment.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiUrl: 'https://api.fastcheck.store'
|
||||
};
|
||||
13
src/index.html
Normal file
13
src/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>FastCheck</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
6
src/main.ts
Normal file
6
src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
|
||||
bootstrapApplication(App, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
21
src/styles.scss
Normal file
21
src/styles.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
Reference in New Issue
Block a user