diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6362cdd..b0e23b1 100755 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,7 @@ "@ng-icons/core": "^29.2.1", "@ng-icons/heroicons": "^29.2.1", "@ng-icons/remixicon": "^29.2.1", + "@stomp/stompjs": "^7.0.0", "date-fns": "^3.6.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", @@ -4684,6 +4685,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@stomp/stompjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.0.0.tgz", + "integrity": "sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw==", + "license": "Apache-2.0" + }, "node_modules/@tailwindcss/typography": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz", diff --git a/frontend/package.json b/frontend/package.json index 4f4c9db..16f43a8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "@ng-icons/core": "^29.2.1", "@ng-icons/heroicons": "^29.2.1", "@ng-icons/remixicon": "^29.2.1", + "@stomp/stompjs": "^7.0.0", "date-fns": "^3.6.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 1f33789..aadf44f 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,6 +1,7 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { RouterOutlet } from '@angular/router'; -import { ToastComponent } from "./shared/components/toast/toast.component"; +import { ToastComponent } from './shared/components/toast/toast.component'; +import { WebsocketService } from './core/services/websocket.service'; @Component({ selector: 'app-root', @@ -9,6 +10,13 @@ import { ToastComponent } from "./shared/components/toast/toast.component"; templateUrl: './app.component.html', styleUrl: './app.component.css', }) -export class AppComponent { +export class AppComponent implements OnInit { title = 'frontend'; + + constructor(private websocketService: WebsocketService) {} + + ngOnInit(): void { + console.log('Connecting to websocket'); + this.websocketService.connect(); + } } diff --git a/frontend/src/app/core/services/order.service.ts b/frontend/src/app/core/services/order.service.ts index ddb6fd7..eb377cb 100644 --- a/frontend/src/app/core/services/order.service.ts +++ b/frontend/src/app/core/services/order.service.ts @@ -13,6 +13,7 @@ export class OrderService { return this.httpClient.post("/orders", data); } - cancelOrder - () {} + cancelOrder(orderId: number) { + return this.httpClient.delete(`/orders/${orderId}`); + } } diff --git a/frontend/src/app/core/services/websocket.service.ts b/frontend/src/app/core/services/websocket.service.ts new file mode 100644 index 0000000..f1c09dd --- /dev/null +++ b/frontend/src/app/core/services/websocket.service.ts @@ -0,0 +1,54 @@ +import { Injectable, OnInit, signal } from '@angular/core'; +import { Client } from '@stomp/stompjs'; + +@Injectable({ + providedIn: 'root', +}) +export class WebsocketService implements OnInit { + constructor() {} + + ngOnInit(): void { + console.log('Setting up client'); + this.client.set(this.setupClient()); + } + + client = signal(null); + connected = signal(false); + + setupClient() { + console.log('Setting up client'); + const client = new Client({ + brokerURL: 'ws://localhost:5002/api/v1/report/ws', + }); + + client.onConnect = (frame) => { + this.connected.set(true); + console.log('Connected: ' + client.connected + ' : ' + frame); + client.subscribe('/topic/greetings', (greeting) => { + console.log(greeting); + }); + }; + + client.onWebSocketError = (error) => { + console.error('Error with websocket', error); + }; + + client.onStompError = (frame) => { + console.error('Broker reported error: ' + frame.headers['message']); + console.error('Additional details: ' + frame.body); + }; + + return client; + } + + connect() { + console.log('Connecting'); + this.client()?.activate(); + } + + disconnect() { + this.client()?.deactivate(); + this.connected.set(false); + console.log('Disconnected'); + } +} diff --git a/frontend/src/app/features/dashboard/dashboard.component.html b/frontend/src/app/features/dashboard/dashboard.component.html index bf17950..2d719fa 100644 --- a/frontend/src/app/features/dashboard/dashboard.component.html +++ b/frontend/src/app/features/dashboard/dashboard.component.html @@ -89,14 +89,14 @@
-
+
-
+
diff --git a/frontend/src/app/features/dashboard/dashboard.component.ts b/frontend/src/app/features/dashboard/dashboard.component.ts index e3e4011..ab0592e 100644 --- a/frontend/src/app/features/dashboard/dashboard.component.ts +++ b/frontend/src/app/features/dashboard/dashboard.component.ts @@ -9,6 +9,8 @@ import { StockPriceCardComponent } from '../../shared/components/stock-price-car import { ToastService, ToastVariant } from '../../core/services/toast.service'; import { Stock } from '../../shared/models/stock.model'; import { OrderRequest } from '../../shared/models/order.model'; +import { AuthService } from '../../core/services/auth.service'; +import { AccountType } from '../../shared/models/user.model'; @Component({ selector: 'app-dashboard', @@ -27,9 +29,12 @@ export class DashboardComponent { constructor( private themeService: ThemeService, private orderService: OrderService, - private toastService: ToastService + private toastService: ToastService, + private authService: AuthService ) {} + isAdmin = computed(() => this.authService.role() == AccountType.ADMIN); + stocks: Stock[] = [ { name: 'Amazon', @@ -90,7 +95,7 @@ export class DashboardComponent { ]; placeOrder($event: OrderRequest) { - console.log("placing order", $event); + console.log('placing order', $event); this.orderService.createOrder($event).subscribe({ next: () => { this.toastService.initiate({ diff --git a/frontend/src/app/shared/components/order-form/order-form.component.html b/frontend/src/app/shared/components/order-form/order-form.component.html index d2a8b99..01b8578 100644 --- a/frontend/src/app/shared/components/order-form/order-form.component.html +++ b/frontend/src/app/shared/components/order-form/order-form.component.html @@ -16,9 +16,14 @@

Place Order

} - @if (orderForm.get('portfolio')?.touched && orderForm.get('portfolio')?.invalid) { + @if (orderForm.get('portfolioId')?.touched && orderForm.get('portfolioId')?.invalid) { Portfolio is required } + @if (orderForm.get('portfolioId')?.touched && orderForm.get('portfolioId')?.invalid) { + + {{ orderForm.get('portfolioId')?.errors?.['message'] }} + + }
@@ -36,6 +41,11 @@

Place Order

+ @if (orderForm.get('ticker')?.touched && orderForm.get('ticker')?.invalid) { + + {{ orderForm.get('ticker')?.errors?.['message'] }} + + }
@@ -47,6 +57,11 @@

Place Order

+ @if (orderForm.get('orderType')?.touched && orderForm.get('orderType')?.invalid) { + + {{ orderForm.get('orderType')?.errors?.['message'] }} + + }
@@ -58,6 +73,11 @@

Place Order

+ @if (orderForm.get('side')?.touched && orderForm.get('side')?.invalid) { + + {{ orderForm.get('side')?.errors?.['message'] }} + + }
@@ -70,9 +90,7 @@

Place Order

/> @if (orderForm.get('unitPrice')?.touched && orderForm.get('unitPrice')?.invalid) { - {{ orderForm.get('unitPrice')?.errors?.['required'] - ? 'Price is required' - : 'Price must be a number' }} + {{ orderForm.get('unitPrice')?.errors?.['message'] }} }
diff --git a/frontend/src/app/shared/components/order-form/order-form.component.ts b/frontend/src/app/shared/components/order-form/order-form.component.ts index 8e17d75..b774b77 100644 --- a/frontend/src/app/shared/components/order-form/order-form.component.ts +++ b/frontend/src/app/shared/components/order-form/order-form.component.ts @@ -16,7 +16,12 @@ import { import * as v from 'valibot'; import { PortfolioService } from '../../../core/services/portfolio.service'; import { PortfolioListResponse } from '../../models/portfolio.model'; -import { OrderRequest, OrderRequestSchema, OrderType } from '../../models/order.model'; +import { + OrderRequest, + OrderRequestSchema, + OrderType, +} from '../../models/order.model'; +import { validateField } from '../../../core/validators/validate'; @Component({ selector: 'app-order-form', @@ -40,18 +45,27 @@ export class OrderFormComponent implements OnInit { defaultside = input<'BUY' | 'SELL' | undefined | null>('BUY'); orderForm: FormGroup = new FormGroup({ - portfolioId: new FormControl('', Validators.required), - ticker: new FormControl('', Validators.required), - orderType: new FormControl('MARKET', Validators.required), - side: new FormControl('', Validators.required), - unitPrice: new FormControl('', [ - Validators.required, - Validators.pattern(/^[0-9]+(\.[0-9]{1,2})?$/), - ]), - quantity: new FormControl('', [ - Validators.required, - Validators.pattern(/^[0-9]+$/), - ]), + portfolioId: new FormControl( + '', + validateField(OrderRequestSchema.entries.portfolioId) + ), + ticker: new FormControl( + '', + validateField(OrderRequestSchema.entries.ticker) + ), + orderType: new FormControl( + 'MARKET', + validateField(OrderRequestSchema.entries.orderType) + ), + side: new FormControl('', validateField(OrderRequestSchema.entries.side)), + unitPrice: new FormControl( + '', + validateField(OrderRequestSchema.entries.unitPrice) + ), + quantity: new FormControl( + '', + validateField(OrderRequestSchema.entries.quantity) + ), }); constructor(private portfolioService: PortfolioService) { @@ -59,22 +73,28 @@ export class OrderFormComponent implements OnInit { this.orderForm = new FormGroup({ portfolioId: new FormControl( this.defaultportfolio() || undefined, - Validators.required + validateField(OrderRequestSchema.entries.portfolioId) ), ticker: new FormControl( this.defaultstock() || 'MSFT', - Validators.required + validateField(OrderRequestSchema.entries.ticker) + ), + orderType: new FormControl( + 'MARKET', + validateField(OrderRequestSchema.entries.orderType) + ), + side: new FormControl( + this.defaultside() || 'BUY', + validateField(OrderRequestSchema.entries.side) + ), + unitPrice: new FormControl( + '', + validateField(OrderRequestSchema.entries.unitPrice) + ), + quantity: new FormControl( + '', + validateField(OrderRequestSchema.entries.quantity) ), - orderType: new FormControl('MARKET', Validators.required), - side: new FormControl(this.defaultside() || 'BUY', Validators.required), - unitPrice: new FormControl('', [ - Validators.required, - Validators.pattern(/^[0-9]+(\.[0-9]{1,2})?$/), - ]), - quantity: new FormControl('', [ - Validators.required, - Validators.pattern(/^[0-9]+$/), - ]), }); }); } @@ -106,6 +126,7 @@ export class OrderFormComponent implements OnInit { this.orderForm.reset(); } else { console.error(result.issues); + console.log(this.orderForm.value); } } } diff --git a/frontend/src/app/shared/models/order.model.ts b/frontend/src/app/shared/models/order.model.ts index 26fa2b1..f71d28a 100644 --- a/frontend/src/app/shared/models/order.model.ts +++ b/frontend/src/app/shared/models/order.model.ts @@ -35,7 +35,10 @@ export const TradeSchema = v.object({ export type Trade = v.InferOutput; export const OrderRequestSchema = v.object({ - portfolioId: v.pipe(v.string(), v.transform(Number.parseInt)), + portfolioId: v.pipe( + v.union([v.number(), v.string()]), + v.transform(Number.parseInt) + ), ticker: v.enum(StockSymbol), quantity: v.pipe( v.union([v.number(), v.string()]), @@ -43,7 +46,7 @@ export const OrderRequestSchema = v.object({ ), orderType: v.enum(OrderType), side: v.enum(OrderSide), - unitPrice: v.pipe(v.string(), v.transform(parseFloat)), + unitPrice: v.pipe(v.string(), v.nonEmpty(), v.transform(parseFloat)), }); export type OrderRequest = v.InferOutput;