Replace submodules with full folder contents
This commit is contained in:
1
machine-operations/src/app/app.component.html
Normal file
1
machine-operations/src/app/app.component.html
Normal file
@ -0,0 +1 @@
|
||||
<router-outlet></router-outlet>
|
||||
0
machine-operations/src/app/app.component.scss
Normal file
0
machine-operations/src/app/app.component.scss
Normal file
29
machine-operations/src/app/app.component.spec.ts
Normal file
29
machine-operations/src/app/app.component.spec.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AppComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have the 'machine-operations' title`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('machine-operations');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, machine-operations');
|
||||
});
|
||||
});
|
||||
13
machine-operations/src/app/app.component.ts
Normal file
13
machine-operations/src/app/app.component.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
standalone:true,
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss'
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'machine-operations';
|
||||
}
|
||||
11
machine-operations/src/app/app.config.server.ts
Normal file
11
machine-operations/src/app/app.config.server.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
|
||||
import { provideServerRendering } from '@angular/platform-server';
|
||||
import { appConfig } from './app.config';
|
||||
|
||||
const serverConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideServerRendering(),
|
||||
]
|
||||
};
|
||||
|
||||
export const config = mergeApplicationConfig(appConfig, serverConfig);
|
||||
9
machine-operations/src/app/app.config.ts
Normal file
9
machine-operations/src/app/app.config.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { routes } from './app.routes';
|
||||
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideClientHydration(withEventReplay()), provideHttpClient()]
|
||||
};
|
||||
13
machine-operations/src/app/app.routes.ts
Normal file
13
machine-operations/src/app/app.routes.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { MachineSlotsComponent } from './machine-slots/machine-slots.component';
|
||||
import { PaymentSuccessComponent } from './payment-success/payment-success.component';
|
||||
import { PaymentFailureComponent } from './payment-failure/payment-failure.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: 'login', component: LoginComponent},
|
||||
{ path: '', redirectTo: 'login', pathMatch: 'full' },
|
||||
{ path: 'machine-slots/:id', component: MachineSlotsComponent},
|
||||
{ path: 'payment/success', component: PaymentSuccessComponent },
|
||||
{ path: 'payment/failure', component: PaymentFailureComponent }
|
||||
];
|
||||
81
machine-operations/src/app/login/login.component.html
Normal file
81
machine-operations/src/app/login/login.component.html
Normal file
@ -0,0 +1,81 @@
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<!-- Title -->
|
||||
<h1 class="title">Sign in</h1>
|
||||
|
||||
<!-- Alert message (shows conditionally) -->
|
||||
@if (showAlert) {
|
||||
<div class="alert" [ngClass]="alert.type">
|
||||
{{ alert.message }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Sign in form -->
|
||||
<form [formGroup]="signInForm" #signInNgForm="ngForm">
|
||||
<!-- Machine ID field -->
|
||||
<div class="form-field">
|
||||
<label for="machineId">Machine ID</label>
|
||||
<input
|
||||
id="machineId"
|
||||
type="text"
|
||||
formControlName="machineId"
|
||||
placeholder="Enter your Machine ID"
|
||||
/>
|
||||
@if (signInForm.get('machineId')?.hasError('required') && signInForm.get('machineId')?.touched) {
|
||||
<div class="error-message">Machine ID is required</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Password field -->
|
||||
<div class="form-field">
|
||||
<label for="password">Password</label>
|
||||
<div class="password-container">
|
||||
<input
|
||||
id="password"
|
||||
[type]="passwordVisible ? 'text' : 'password'"
|
||||
formControlName="password"
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-visibility"
|
||||
(click)="togglePasswordVisibility()"
|
||||
>
|
||||
@if (!passwordVisible) {
|
||||
<span class="icon">👁️</span>
|
||||
} @else {
|
||||
<span class="icon">🔒</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
@if (signInForm.get('password')?.hasError('required') && signInForm.get('password')?.touched) {
|
||||
<div class="error-message">Password is required</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Remember me checkbox -->
|
||||
<div class="remember-me">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" formControlName="rememberMe" />
|
||||
<span class="checkbox-label">Remember me</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Submit button -->
|
||||
<button
|
||||
type="submit"
|
||||
class="submit-button"
|
||||
[disabled]="signInForm.invalid || isLoading"
|
||||
(click)="signIn()"
|
||||
>
|
||||
@if (isLoading) {
|
||||
<div class="loader"></div>
|
||||
} @else {
|
||||
<span>Sign in</span>
|
||||
}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
171
machine-operations/src/app/login/login.component.scss
Normal file
171
machine-operations/src/app/login/login.component.scss
Normal file
@ -0,0 +1,171 @@
|
||||
//login.scss
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 1rem;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.alert.error {
|
||||
background-color: rgba(244, 67, 54, 0.1);
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
.alert.success {
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="text"] {
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #4285f4;
|
||||
}
|
||||
|
||||
.password-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.password-container input {
|
||||
flex: 1;
|
||||
padding-right: 48px;
|
||||
}
|
||||
|
||||
.toggle-visibility {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 18px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.remember-me {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: -0.5rem;
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #d32f2f;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
height: 48px;
|
||||
border-radius: 6px;
|
||||
background-color: #4285f4;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background-color: #3367d6;
|
||||
}
|
||||
|
||||
.submit-button:disabled {
|
||||
background-color: #a1c1f1;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
23
machine-operations/src/app/login/login.component.spec.ts
Normal file
23
machine-operations/src/app/login/login.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LoginComponent } from './login.component';
|
||||
|
||||
describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoginComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
66
machine-operations/src/app/login/login.component.ts
Normal file
66
machine-operations/src/app/login/login.component.ts
Normal file
@ -0,0 +1,66 @@
|
||||
// src/app/login/login.component.ts
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss']
|
||||
})
|
||||
export class LoginComponent {
|
||||
signInForm: FormGroup;
|
||||
passwordVisible: boolean = false;
|
||||
isLoading: boolean = false;
|
||||
showAlert: boolean = false;
|
||||
alert = { type: '', message: '' };
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private authService: AuthService,
|
||||
private router: Router
|
||||
) {
|
||||
this.signInForm = this.fb.group({
|
||||
machineId: ['', Validators.required],
|
||||
password: ['', Validators.required],
|
||||
rememberMe: [false]
|
||||
});
|
||||
}
|
||||
|
||||
togglePasswordVisibility() {
|
||||
this.passwordVisible = !this.passwordVisible;
|
||||
}
|
||||
|
||||
signIn() {
|
||||
if (this.signInForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.showAlert = false;
|
||||
|
||||
const { machineId, password } = this.signInForm.value;
|
||||
|
||||
this.authService.login(machineId, password).subscribe(
|
||||
(response: any) => {
|
||||
this.isLoading = false;
|
||||
if (response.message === 'Login successful') {
|
||||
this.authService.setLoggedInMachine(response.machine_id, response.id);
|
||||
this.router.navigate(['/machine-slots', response.machine_id]); // Redirect to dashboard on success
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
this.isLoading = false;
|
||||
this.showAlert = true;
|
||||
this.alert = {
|
||||
type: 'error',
|
||||
message: error.error.error || 'Login failed. Please try again.'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
<div class="vending-container" *ngIf="!error">
|
||||
<div class="header">
|
||||
<button class="logout-button" (click)="logout()">Logout</button>
|
||||
<button class="cart-button" (click)="toggleCart()">
|
||||
<span class="cart-icon">🛒</span>
|
||||
<span class="cart-count" *ngIf="cart.length > 0">{{ cart.length }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Vending Machine Grid -->
|
||||
@for (row of ['A', 'B', 'C', 'D', 'E', 'F']; track row) {
|
||||
<div class="vending-row">
|
||||
<div class="row-title">
|
||||
<span>Row {{ row }}</span>
|
||||
<div class="scroll-hint">
|
||||
<span>Scroll</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slots-container">
|
||||
@for (col of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; track col) {
|
||||
<div class="slot-card" [ngClass]="{'disabled': !getSlot(row, col)?.enabled}">
|
||||
<div class="slot-header">
|
||||
<span class="slot-name">{{ row + col }}</span>
|
||||
</div>
|
||||
<div class="slot-body">
|
||||
@if (getSlot(row, col)?.product_id) {
|
||||
<div class="product-image-container">
|
||||
<img *ngIf="getSlot(row, col)?.product_image"
|
||||
[src]="'http://localhost:5001/' + getSlot(row, col)?.product_image"
|
||||
[attr.alt]="getSlot(row, col)?.product_name" class="product-image">
|
||||
</div>
|
||||
|
||||
<div class="product-details">
|
||||
<p class="product-name">{{ getSlot(row, col)?.product_name }}</p>
|
||||
|
||||
<!-- Price and Quantity Selector -->
|
||||
<div class="price-quantity-row">
|
||||
<p class="product-price">{{ getSlot(row, col)?.price }} ₹</p>
|
||||
|
||||
<div class="quantity-selector"
|
||||
*ngIf="getSlot(row, col)?.enabled && getSlot(row, col)?.units && getSlot(row, col)!.units > 0">
|
||||
<button class="quantity-btn minus-btn" (click)="decrementQuantity(row, col)"
|
||||
[disabled]="selectedQuantities[row + col] <= 1">
|
||||
<span class="minus-icon">-</span>
|
||||
</button>
|
||||
|
||||
<span class="quantity-display">{{ selectedQuantities[row + col] || 1 }}</span>
|
||||
|
||||
<button class="quantity-btn plus-btn" (click)="incrementQuantity(row, col)"
|
||||
[disabled]="selectedQuantities[row + col] >= getSlot(row, col)!.units">
|
||||
<span class="plus-icon">+</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-actions">
|
||||
<button class="add-to-cart-btn"
|
||||
[disabled]="!getSlot(row, col)?.enabled || !getSlot(row, col)?.units || getSlot(row, col)!.units <= 0"
|
||||
(click)="addToCart(getSlot(row, col)!, row + col)">
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="empty-slot">
|
||||
<p class="add-product-text">No Product Assigned</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="scroll-shadows"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Shopping Cart Sidebar -->
|
||||
<div class="shopping-cart" [ngClass]="{'open': cartOpen}">
|
||||
<div class="cart-header">
|
||||
<h2>Shopping Cart</h2>
|
||||
<button class="close-cart" (click)="toggleCart()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="cart-items">
|
||||
@if (cart.length === 0) {
|
||||
<p class="empty-cart-message">Your cart is empty</p>
|
||||
} @else {
|
||||
@for (item of cart; track item; let i = $index) {
|
||||
<div class="cart-item">
|
||||
<div class="cart-item-image">
|
||||
<img *ngIf="item.productImage" [src]="'http://localhost:5001/' + item.productImage"
|
||||
[attr.alt]="item.productName">
|
||||
</div>
|
||||
<div class="cart-item-details">
|
||||
<p class="cart-item-name">{{ item.productName }}</p>
|
||||
<div class="cart-item-controls">
|
||||
<button class="quantity-btn" (click)="updateCartItemQuantity(item, item.quantity - 1)"
|
||||
[disabled]="item.quantity <= 1">-</button>
|
||||
<span class="quantity">{{ item.quantity }}</span>
|
||||
<button class="quantity-btn" (click)="updateCartItemQuantity(item, item.quantity + 1)"
|
||||
[disabled]="item.quantity >= (getSlot(item.slotId[0], +item.slotId.substring(1))?.units || 0)">+</button>
|
||||
</div>
|
||||
<p class="cart-item-price">₹{{ item.price.toFixed(2) }} × {{ item.quantity }} = ₹{{ item.totalPrice.toFixed(2) }}</p>
|
||||
</div>
|
||||
<button class="remove-item" (click)="removeFromCart(i)">×</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="cart-footer" *ngIf="cart.length > 0">
|
||||
<div class="cart-total">
|
||||
<span>Total:</span>
|
||||
<span class="total-price">₹{{ totalCartPrice.toFixed(2) }}</span>
|
||||
</div>
|
||||
<button class="checkout-btn" (click)="proceedToPayment()">
|
||||
Proceed to Payment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cart Overlay -->
|
||||
<div class="cart-overlay" *ngIf="cartOpen" (click)="toggleCart()"></div>
|
||||
|
||||
<!-- Payment Confirmation Dialog -->
|
||||
<div class="payment-dialog-overlay" *ngIf="showPaymentDialog">
|
||||
<div class="payment-dialog">
|
||||
<div class="dialog-header">
|
||||
<h2>Confirm Payment</h2>
|
||||
<button class="close-dialog" (click)="closePaymentDialog()" [disabled]="processingTransaction">×</button>
|
||||
</div>
|
||||
|
||||
<div class="dialog-body">
|
||||
<div class="order-summary">
|
||||
<h3>Order Summary</h3>
|
||||
<div class="summary-items">
|
||||
@for (item of paymentDialogData?.cart; track item) {
|
||||
<div class="summary-item">
|
||||
<div class="summary-item-left">
|
||||
<span class="item-name">{{ item.productName }}</span>
|
||||
<span class="item-quantity">Qty: {{ item.quantity }}</span>
|
||||
</div>
|
||||
<span class="item-price">₹{{ item.totalPrice.toFixed(2) }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="summary-total">
|
||||
<span class="total-label">Total Amount:</span>
|
||||
<span class="total-amount">₹{{ formattedTotalAmount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-footer">
|
||||
<button
|
||||
class="dialog-btn cancel-btn"
|
||||
(click)="handlePaymentCancel()"
|
||||
[disabled]="processingTransaction">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="dialog-btn paid-btn"
|
||||
(click)="handlePaymentPaid()"
|
||||
[disabled]="processingTransaction">
|
||||
{{ processingTransaction ? 'Processing...' : 'Paid' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="error" class="error">{{ error }}</div>
|
||||
<p *ngIf="loading">Loading...</p>
|
||||
@ -0,0 +1,777 @@
|
||||
//machine slot.scss
|
||||
.vending-container {
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.logout-button, .cart-button {
|
||||
padding: 8px 16px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #0069d9;
|
||||
}
|
||||
}
|
||||
|
||||
.cart-button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cart-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.cart-count {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background-color: red;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.vending-row {
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.row-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
background: #e9ecef;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.scroll-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.slots-container {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.slot-card {
|
||||
min-width: 180px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.slot-card.disabled {
|
||||
background: #f9f9f9;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.slot-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.slot-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.status-text.enabled {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.status-text.disabled {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.slot-body {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.product-image-container {
|
||||
margin-bottom: 10px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.product-details {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-weight: bold;
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
|
||||
.product-stats {
|
||||
margin: 0 0 10px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.units-display {
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.product-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.quantity-selector {
|
||||
margin-bottom: 5px;
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.add-to-cart-btn {
|
||||
padding: 5px 10px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-slot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.add-product-text {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.scroll-shadows {
|
||||
height: 10px;
|
||||
background: linear-gradient(to right, rgba(0,0,0,0.1), transparent);
|
||||
}
|
||||
|
||||
/* Shopping Cart Styles */
|
||||
.shopping-cart {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -400px;
|
||||
width: 350px;
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
box-shadow: -2px 0 5px rgba(0,0,0,0.2);
|
||||
transition: right 0.3s ease-in-out;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.shopping-cart.open {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.cart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.close-cart {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #6c757d;
|
||||
|
||||
&:hover {
|
||||
color: #343a40;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cart-items {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.empty-cart-message {
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.cart-item {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cart-item-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.cart-item-details {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.cart-item-name {
|
||||
font-weight: bold;
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
|
||||
.cart-item-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.quantity-btn {
|
||||
background-color: #e9ecef;
|
||||
border: 1px solid #ddd;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.quantity {
|
||||
margin: 0 10px;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cart-item-price {
|
||||
margin: 0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.remove-item {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
color: #dc3545;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #c82333;
|
||||
}
|
||||
}
|
||||
|
||||
.cart-footer {
|
||||
padding: 15px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.cart-total {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.checkout-btn {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
background-color: #0069d9;
|
||||
}
|
||||
}
|
||||
|
||||
.cart-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
z-index: 999;
|
||||
}
|
||||
// Add/Update these styles in your machine-slots.component.scss file
|
||||
|
||||
.price-quantity-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
flex-shrink: 0; // Prevents price from shrinking
|
||||
}
|
||||
|
||||
// Update your existing quantity-selector styles
|
||||
.quantity-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px; // Reduced gap for tighter layout
|
||||
font-family: Arial, sans-serif;
|
||||
flex-shrink: 0; // Prevents quantity selector from shrinking
|
||||
}
|
||||
|
||||
.quantity-btn {
|
||||
width: 28px; // Slightly smaller for better fit
|
||||
height: 28px;
|
||||
border: 2px solid #ddd;
|
||||
background-color: white;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 16px; // Slightly smaller font
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #f5f5f5;
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #f9f9f9;
|
||||
border-color: #e5e5e5;
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.quantity-display {
|
||||
font-size: 14px; // Smaller font for compact layout
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
padding: 0 6px; // Reduced padding
|
||||
}
|
||||
|
||||
.minus-icon,
|
||||
.plus-icon {
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// Make sure the product-actions only contains the button now
|
||||
.product-actions {
|
||||
margin-top: 8px;
|
||||
|
||||
.add-to-cart-btn {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #bdc3c7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
PAYMENT DIALOG
|
||||
============================================ */
|
||||
|
||||
.payment-dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 99999;
|
||||
padding: 20px;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.payment-dialog {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
||||
animation: slideUp 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-dialog {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 32px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.order-summary {
|
||||
h3 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-items {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.summary-item-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-quantity {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-total {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 0 0;
|
||||
margin-top: 12px;
|
||||
border-top: 2px solid #333;
|
||||
|
||||
.total-label {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #4CAF50;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 20px 24px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.dialog-btn {
|
||||
flex: 1;
|
||||
padding: 14px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.paid-btn {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #45a049;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
.dark .payment-dialog {
|
||||
background: #1e293b;
|
||||
|
||||
.dialog-header {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.1);
|
||||
|
||||
h2 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.close-dialog {
|
||||
color: #999;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-items {
|
||||
border-top-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.05);
|
||||
|
||||
.item-name {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.item-quantity {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-total {
|
||||
border-top-color: rgba(255, 255, 255, 0.2);
|
||||
|
||||
.total-label {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
border-top-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.payment-dialog {
|
||||
max-width: 95vw;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
padding: 16px 20px;
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.summary-total {
|
||||
.total-label {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.dialog-btn {
|
||||
padding: 12px 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MachineSlotsComponent } from './machine-slots.component';
|
||||
|
||||
describe('MachineSlotsComponent', () => {
|
||||
let component: MachineSlotsComponent;
|
||||
let fixture: ComponentFixture<MachineSlotsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MachineSlotsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MachineSlotsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,627 @@
|
||||
// src/app/machine-slots/machine-slots.component.ts
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
interface Slot {
|
||||
slot_name: string; // row_id (A-F)
|
||||
column: number;
|
||||
enabled: boolean;
|
||||
product_id: string | null;
|
||||
units: number;
|
||||
price: string;
|
||||
product_name: string;
|
||||
product_image: string | null;
|
||||
}
|
||||
|
||||
interface CartItem {
|
||||
slotId: string;
|
||||
productId: string;
|
||||
productName: string;
|
||||
productImage: string | null;
|
||||
quantity: number;
|
||||
price: number;
|
||||
totalPrice: number;
|
||||
}
|
||||
|
||||
interface PaymentDialogData {
|
||||
cart: CartItem[];
|
||||
totalAmount: number;
|
||||
machineId: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-machine-slots',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
templateUrl: './machine-slots.component.html',
|
||||
styleUrls: ['./machine-slots.component.scss']
|
||||
})
|
||||
export class MachineSlotsComponent implements OnInit {
|
||||
machineId: string | null = null;
|
||||
slots: Slot[] = [];
|
||||
loading = false;
|
||||
error = '';
|
||||
cartOpen = false;
|
||||
cart: CartItem[] = [];
|
||||
selectedQuantities: { [key: string]: number } = {};
|
||||
totalCartPrice = 0;
|
||||
|
||||
// Payment dialog properties
|
||||
showPaymentDialog = false;
|
||||
paymentDialogData: PaymentDialogData | null = null;
|
||||
processingTransaction = false;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private http: HttpClient,
|
||||
private authService: AuthService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.machineId = this.route.snapshot.paramMap.get('id') || this.authService.getLoggedInMachineId();
|
||||
console.log('Using machineId:', this.machineId);
|
||||
|
||||
if (this.machineId) {
|
||||
this.loadMachineSlots();
|
||||
} else {
|
||||
this.error = 'Machine ID not found';
|
||||
}
|
||||
}
|
||||
|
||||
loadMachineSlots() {
|
||||
this.loading = true;
|
||||
this.http.get<Slot[]>(`${environment.apiUrl}/machine-slots/${this.machineId}`).subscribe(
|
||||
(data) => {
|
||||
this.slots = data;
|
||||
this.slots.forEach(slot => {
|
||||
if (slot.product_id) {
|
||||
this.selectedQuantities[`${slot.slot_name}${slot.column}`] = 1;
|
||||
}
|
||||
});
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
this.error = 'Failed to load machine slots';
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getSlot(row: string, column: number): Slot | undefined {
|
||||
return this.slots.find(s => s.slot_name === row && s.column === column);
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.authService.logout();
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
|
||||
toggleCart() {
|
||||
this.cartOpen = !this.cartOpen;
|
||||
}
|
||||
|
||||
addToCart(slot: Slot, slotId: string) {
|
||||
if (!slot.enabled || !slot.product_id || slot.units <= 0) return;
|
||||
|
||||
const quantity = this.selectedQuantities[slotId];
|
||||
if (!quantity || quantity <= 0 || quantity > slot.units) return;
|
||||
|
||||
const existingItemIndex = this.cart.findIndex(item => item.slotId === slotId);
|
||||
|
||||
if (existingItemIndex !== -1) {
|
||||
this.cart[existingItemIndex].quantity = quantity;
|
||||
this.cart[existingItemIndex].totalPrice = quantity * parseFloat(slot.price);
|
||||
} else {
|
||||
this.cart.push({
|
||||
slotId,
|
||||
productId: slot.product_id,
|
||||
productName: slot.product_name,
|
||||
productImage: slot.product_image,
|
||||
quantity,
|
||||
price: parseFloat(slot.price),
|
||||
totalPrice: quantity * parseFloat(slot.price)
|
||||
});
|
||||
}
|
||||
|
||||
this.calculateTotalCartPrice();
|
||||
this.debugCart();
|
||||
}
|
||||
|
||||
removeFromCart(index: number) {
|
||||
this.cart.splice(index, 1);
|
||||
this.calculateTotalCartPrice();
|
||||
}
|
||||
|
||||
updateCartItemQuantity(item: CartItem, quantity: number) {
|
||||
if (quantity > 0) {
|
||||
const slot = this.slots.find(s => s.slot_name + s.column === item.slotId);
|
||||
if (slot && quantity <= slot.units) {
|
||||
item.quantity = quantity;
|
||||
item.totalPrice = quantity * item.price;
|
||||
this.calculateTotalCartPrice();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
calculateTotalCartPrice() {
|
||||
this.totalCartPrice = this.cart.reduce((total, item) => total + item.totalPrice, 0);
|
||||
}
|
||||
|
||||
incrementQuantity(row: string, col: number): void {
|
||||
const slot = this.getSlot(row, col);
|
||||
if (!slot || !slot.units) return;
|
||||
|
||||
const key = `${row}${col}`;
|
||||
const currentQuantity = this.selectedQuantities[key] || 1;
|
||||
|
||||
if (currentQuantity < slot.units) {
|
||||
this.selectedQuantities[key] = currentQuantity + 1;
|
||||
}
|
||||
}
|
||||
|
||||
decrementQuantity(row: string, col: number): void {
|
||||
const key = `${row}${col}`;
|
||||
const currentQuantity = this.selectedQuantities[key] || 1;
|
||||
|
||||
if (currentQuantity > 1) {
|
||||
this.selectedQuantities[key] = currentQuantity - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Simple payment dialog approach
|
||||
proceedToPayment() {
|
||||
console.log('=== PROCEED TO PAYMENT CLICKED ===');
|
||||
console.log('Machine ID:', this.machineId);
|
||||
console.log('Cart:', this.cart);
|
||||
console.log('Cart length:', this.cart.length);
|
||||
|
||||
if (!this.machineId || this.cart.length === 0) {
|
||||
console.log('BLOCKED: Missing machine ID or empty cart');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Opening payment dialog...');
|
||||
this.paymentDialogData = {
|
||||
cart: [...this.cart],
|
||||
totalAmount: this.totalCartPrice,
|
||||
machineId: this.machineId
|
||||
};
|
||||
|
||||
console.log('Payment dialog data:', this.paymentDialogData);
|
||||
this.showPaymentDialog = true;
|
||||
console.log('showPaymentDialog set to:', this.showPaymentDialog);
|
||||
}
|
||||
|
||||
closePaymentDialog() {
|
||||
this.showPaymentDialog = false;
|
||||
this.paymentDialogData = null;
|
||||
}
|
||||
|
||||
handlePaymentPaid() {
|
||||
if (!this.paymentDialogData || this.processingTransaction) return;
|
||||
|
||||
this.processingTransaction = true;
|
||||
|
||||
// Prepare inventory update data
|
||||
const inventoryUpdates = this.paymentDialogData.cart.map(item => ({
|
||||
slotId: item.slotId,
|
||||
quantityDispensed: item.quantity
|
||||
}));
|
||||
|
||||
const updateData = {
|
||||
machineId: this.machineId,
|
||||
inventoryUpdates: inventoryUpdates
|
||||
};
|
||||
|
||||
// STEP 1: Update inventory FIRST
|
||||
this.http.post(`${environment.apiUrl}/machine-slots/update-inventory`, updateData).subscribe({
|
||||
next: (inventoryResponse: any) => {
|
||||
console.log('✓ Server inventory updated successfully:', inventoryResponse);
|
||||
|
||||
// STEP 2: Now create transactions (only if inventory update succeeded)
|
||||
const transactionData = this.paymentDialogData!.cart.map(item => ({
|
||||
machine_id: this.paymentDialogData!.machineId,
|
||||
product_name: item.productName,
|
||||
quantity: item.quantity,
|
||||
amount: item.totalPrice,
|
||||
payment_type: 'cash',
|
||||
status: 'Success'
|
||||
}));
|
||||
|
||||
this.http.post(`${environment.apiUrl}/transactions/bulk-create`, {
|
||||
transactions: transactionData
|
||||
}).subscribe({
|
||||
next: (transactionResponse: any) => {
|
||||
console.log('✓ Transactions created successfully:', transactionResponse);
|
||||
|
||||
// STEP 3: Update local inventory
|
||||
this.updateLocalInventory(this.paymentDialogData!.cart);
|
||||
|
||||
// STEP 4: Clear cart and close dialog
|
||||
this.clearCart();
|
||||
this.closePaymentDialog();
|
||||
|
||||
// STEP 5: Show success message
|
||||
alert('Payment successful! Items dispensed.');
|
||||
|
||||
// STEP 6: Reload slots to sync with server
|
||||
this.loadMachineSlots();
|
||||
|
||||
this.processingTransaction = false;
|
||||
},
|
||||
error: (transactionError) => {
|
||||
console.error('✗ Error creating transactions:', transactionError);
|
||||
this.error = 'Inventory updated but transaction recording failed';
|
||||
this.processingTransaction = false;
|
||||
|
||||
// Still reload to show updated inventory
|
||||
this.loadMachineSlots();
|
||||
}
|
||||
});
|
||||
},
|
||||
error: (inventoryError) => {
|
||||
console.error('✗ Failed to update server inventory:', inventoryError);
|
||||
this.error = 'Failed to update inventory. Payment not processed.';
|
||||
this.processingTransaction = false;
|
||||
|
||||
// Don't create transactions if inventory update failed
|
||||
// This prevents selling items that couldn't be deducted from inventory
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handlePaymentCancel() {
|
||||
this.closePaymentDialog();
|
||||
}
|
||||
|
||||
private updateLocalInventory(paidCart: CartItem[]) {
|
||||
paidCart.forEach(cartItem => {
|
||||
const slot = this.slots.find(s =>
|
||||
s.slot_name + s.column === cartItem.slotId
|
||||
);
|
||||
|
||||
if (slot && slot.units >= cartItem.quantity) {
|
||||
slot.units -= cartItem.quantity;
|
||||
|
||||
if (slot.units <= 0) {
|
||||
slot.enabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update selectedQuantities
|
||||
this.slots.forEach(slot => {
|
||||
if (slot.product_id && slot.units > 0) {
|
||||
this.selectedQuantities[`${slot.slot_name}${slot.column}`] = 1;
|
||||
} else {
|
||||
delete this.selectedQuantities[`${slot.slot_name}${slot.column}`];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private showSuccessMessage(message: string) {
|
||||
alert(message);
|
||||
}
|
||||
|
||||
clearCart() {
|
||||
this.cart = [];
|
||||
this.totalCartPrice = 0;
|
||||
}
|
||||
|
||||
generateQuantityOptions(slot: Slot): number[] {
|
||||
const options: number[] = [];
|
||||
for (let i = 1; i <= slot.units; i++) options.push(i);
|
||||
return options;
|
||||
}
|
||||
|
||||
getUnitStatusClass(units: number): string {
|
||||
if (units <= 0) return 'empty';
|
||||
if (units <= 3) return 'low-stock';
|
||||
return '';
|
||||
}
|
||||
|
||||
get formattedTotalAmount(): string {
|
||||
return this.paymentDialogData?.totalAmount !== undefined
|
||||
? this.paymentDialogData.totalAmount.toFixed(2)
|
||||
: '0.00';
|
||||
}
|
||||
|
||||
debugCart() {
|
||||
console.log('Cart state:');
|
||||
console.log('- Length:', this.cart.length);
|
||||
console.log('- Items:', this.cart);
|
||||
console.log('- Total:', this.totalCartPrice);
|
||||
console.log('- Machine ID:', this.machineId);
|
||||
}
|
||||
|
||||
|
||||
/* ========================================
|
||||
PAYU PAYMENT GATEWAY CODE (HIDDEN FOR NOW)
|
||||
========================================
|
||||
|
||||
// Uncomment this section when you want to use PayU payment gateway
|
||||
|
||||
interface PayUResponse {
|
||||
key: string;
|
||||
txnid: string;
|
||||
amount: string;
|
||||
productinfo: string;
|
||||
firstname: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
surl: string;
|
||||
furl: string;
|
||||
hash: string;
|
||||
service_provider?: string;
|
||||
udf1?: string;
|
||||
udf2?: string;
|
||||
udf3?: string;
|
||||
udf4?: string;
|
||||
udf5?: string;
|
||||
}
|
||||
|
||||
showingPaymentMessage = false;
|
||||
paymentSuccessMessage = false;
|
||||
paymentFailureMessage = false;
|
||||
|
||||
proceedToPaymentWithPayU() {
|
||||
if (!this.machineId || this.cart.length === 0 || this.processingTransaction) return;
|
||||
|
||||
this.processingTransaction = true;
|
||||
this.error = '';
|
||||
|
||||
const dispensingInstructions = this.cart.map(item => ({
|
||||
slotId: item.slotId,
|
||||
rowId: item.slotId[0],
|
||||
column: parseInt(item.slotId.substring(1)),
|
||||
quantity: item.quantity
|
||||
}));
|
||||
|
||||
const orderData = {
|
||||
machineId: this.machineId,
|
||||
dispensingInstructions,
|
||||
totalAmount: this.totalCartPrice.toFixed(2),
|
||||
productInfo: this.cart.map(item => `${item.productName} x ${item.quantity}`).join(', '),
|
||||
cartData: JSON.stringify(this.cart),
|
||||
userDetails: {
|
||||
firstname: 'Customer',
|
||||
email: 'customer@example.com',
|
||||
phone: '1234567890'
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.setItem('pendingCart', JSON.stringify(this.cart));
|
||||
sessionStorage.setItem('pendingMachineId', this.machineId);
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
this.http.post<PayUResponse>(`${environment.apiUrl}/create-payu-order`, orderData, { headers }).subscribe({
|
||||
next: (payuData) => {
|
||||
if (!payuData || !payuData.key || !payuData.hash || !payuData.amount) {
|
||||
this.error = 'Invalid payment data received. Please try again.';
|
||||
this.processingTransaction = false;
|
||||
return;
|
||||
}
|
||||
this.redirectToPayU(payuData);
|
||||
},
|
||||
error: (error) => {
|
||||
if (error.status === 0) {
|
||||
this.error = 'Unable to connect to payment service. Please check your connection.';
|
||||
} else if (error.status === 404) {
|
||||
this.error = 'Payment service not found. Please contact support.';
|
||||
} else if (error.status >= 500) {
|
||||
this.error = 'Payment service is temporarily unavailable. Please try again.';
|
||||
} else {
|
||||
this.error = error.error?.error || 'There was a problem creating the payment order. Please try again.';
|
||||
}
|
||||
this.processingTransaction = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private redirectToPayU(payuData: PayUResponse) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = environment.payuUrl || 'https://test.payu.in/_payment';
|
||||
form.target = '_blank';
|
||||
form.style.display = 'none';
|
||||
|
||||
const fields = [
|
||||
{ name: 'key', value: payuData.key || '' },
|
||||
{ name: 'txnid', value: payuData.txnid || '' },
|
||||
{ name: 'amount', value: payuData.amount || '' },
|
||||
{ name: 'productinfo', value: payuData.productinfo || '' },
|
||||
{ name: 'firstname', value: payuData.firstname || '' },
|
||||
{ name: 'email', value: payuData.email || '' },
|
||||
{ name: 'phone', value: payuData.phone || '' },
|
||||
{ name: 'surl', value: payuData.surl || '' },
|
||||
{ name: 'furl', value: payuData.furl || '' },
|
||||
{ name: 'hash', value: typeof payuData.hash === 'string' ? payuData.hash : String(payuData.hash) },
|
||||
{ name: 'udf1', value: payuData.udf1 || this.machineId || '' },
|
||||
{ name: 'udf2', value: payuData.udf2 || JSON.stringify(this.cart.map(item => ({
|
||||
slotId: item.slotId,
|
||||
quantity: item.quantity,
|
||||
productName: item.productName
|
||||
})))
|
||||
},
|
||||
{ name: 'udf3', value: payuData.udf3 || '' },
|
||||
{ name: 'udf4', value: payuData.udf4 || '' },
|
||||
{ name: 'udf5', value: payuData.udf5 || '' }
|
||||
];
|
||||
|
||||
if (payuData.service_provider) {
|
||||
fields.push({ name: 'service_provider', value: payuData.service_provider });
|
||||
}
|
||||
|
||||
fields.forEach(field => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = field.name;
|
||||
input.value = field.value;
|
||||
form.appendChild(input);
|
||||
});
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(form)) {
|
||||
document.body.removeChild(form);
|
||||
}
|
||||
this.processingTransaction = false;
|
||||
}, 100);
|
||||
|
||||
this.showPaymentTabMessage();
|
||||
}
|
||||
|
||||
private showPaymentTabMessage() {
|
||||
this.showingPaymentMessage = true;
|
||||
setTimeout(() => {
|
||||
this.showingPaymentMessage = false;
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
private checkForPaymentSuccess() {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const paymentStatus = urlParams.get('status');
|
||||
const txnid = urlParams.get('txnid');
|
||||
|
||||
if (paymentStatus === 'success' && txnid) {
|
||||
this.handlePaymentSuccess();
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('status');
|
||||
url.searchParams.delete('txnid');
|
||||
url.searchParams.delete('amount');
|
||||
window.history.replaceState({}, document.title, url.toString());
|
||||
} else if (paymentStatus === 'failure') {
|
||||
this.showPaymentFailureMessage();
|
||||
}
|
||||
}
|
||||
|
||||
handlePaymentSuccess() {
|
||||
if (typeof window !== 'undefined') {
|
||||
const pendingCart = sessionStorage.getItem('pendingCart');
|
||||
const pendingMachineId = sessionStorage.getItem('pendingMachineId');
|
||||
|
||||
if (pendingCart && pendingMachineId && pendingMachineId === this.machineId) {
|
||||
try {
|
||||
const cartToProcess: CartItem[] = JSON.parse(pendingCart);
|
||||
this.processSuccessfulPayment(cartToProcess);
|
||||
} catch (e) {
|
||||
console.error('Error parsing pending cart for payment success:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private processSuccessfulPayment(paidCart: CartItem[]) {
|
||||
paidCart.forEach(cartItem => {
|
||||
const slot = this.slots.find(s =>
|
||||
s.slot_name + s.column === cartItem.slotId
|
||||
);
|
||||
|
||||
if (slot && slot.units >= cartItem.quantity) {
|
||||
slot.units -= cartItem.quantity;
|
||||
if (slot.units <= 0) {
|
||||
slot.enabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.clearCart();
|
||||
|
||||
this.slots.forEach(slot => {
|
||||
if (slot.product_id && slot.units > 0) {
|
||||
this.selectedQuantities[`${slot.slot_name}${slot.column}`] = 1;
|
||||
} else {
|
||||
delete this.selectedQuantities[`${slot.slot_name}${slot.column}`];
|
||||
}
|
||||
});
|
||||
|
||||
this.updateServerInventory(paidCart);
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.removeItem('pendingCart');
|
||||
sessionStorage.removeItem('pendingMachineId');
|
||||
}
|
||||
|
||||
this.showPaymentSuccessMessage();
|
||||
}
|
||||
|
||||
private updateServerInventory(paidCart: CartItem[]) {
|
||||
const inventoryUpdates = paidCart.map(item => ({
|
||||
slotId: item.slotId,
|
||||
rowId: item.slotId[0],
|
||||
column: parseInt(item.slotId.substring(1)),
|
||||
quantityDispensed: item.quantity
|
||||
}));
|
||||
|
||||
const updateData = {
|
||||
machineId: this.machineId,
|
||||
inventoryUpdates: inventoryUpdates
|
||||
};
|
||||
|
||||
this.http.post(`${environment.apiUrl}/update-inventory`, updateData).subscribe({
|
||||
next: (response) => {
|
||||
console.log('Server inventory updated successfully:', response);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to update server inventory:', error);
|
||||
this.loadMachineSlots();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private showPaymentSuccessMessage() {
|
||||
this.paymentSuccessMessage = true;
|
||||
setTimeout(() => {
|
||||
this.paymentSuccessMessage = false;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
private showPaymentFailureMessage() {
|
||||
this.paymentFailureMessage = true;
|
||||
setTimeout(() => {
|
||||
this.paymentFailureMessage = false;
|
||||
}, 8000);
|
||||
}
|
||||
|
||||
restoreCartFromSession() {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const pendingCart = sessionStorage.getItem('pendingCart');
|
||||
const pendingMachineId = sessionStorage.getItem('pendingMachineId');
|
||||
|
||||
if (pendingCart && pendingMachineId && pendingMachineId === this.machineId) {
|
||||
try {
|
||||
this.cart = JSON.parse(pendingCart);
|
||||
this.calculateTotalCartPrice();
|
||||
|
||||
this.cart.forEach(item => {
|
||||
this.selectedQuantities[item.slotId] = item.quantity;
|
||||
});
|
||||
|
||||
sessionStorage.removeItem('pendingCart');
|
||||
sessionStorage.removeItem('pendingMachineId');
|
||||
} catch (e) {
|
||||
console.error('Error parsing pending cart:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
======================================== */
|
||||
}
|
||||
@ -0,0 +1,169 @@
|
||||
<!-- payment-failure.component.html -->
|
||||
<div class="payment-result-container">
|
||||
<div class="failure-card" *ngIf="transactionDetails">
|
||||
<div class="failure-header">
|
||||
<div class="failure-icon" [ngClass]="getFailureReasonClass()">
|
||||
{{ getFailureIcon() }}
|
||||
</div>
|
||||
<h1>Payment Failed</h1>
|
||||
<p class="failure-message">Unfortunately, your payment could not be processed. Don't worry, no money has been charged.</p>
|
||||
</div>
|
||||
|
||||
<div class="transaction-details">
|
||||
<h2>Transaction Details</h2>
|
||||
<div class="details-grid">
|
||||
<div class="detail-item">
|
||||
<span class="label">Transaction ID:</span>
|
||||
<span class="value">{{ transactionDetails.transactionId }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Amount:</span>
|
||||
<span class="value amount">₹{{ transactionDetails.amount }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Status:</span>
|
||||
<span class="value status-failed">{{ transactionDetails.status }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Customer Name:</span>
|
||||
<span class="value">{{ transactionDetails.customerName }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Email:</span>
|
||||
<span class="value">{{ transactionDetails.customerEmail }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Phone:</span>
|
||||
<span class="value">{{ transactionDetails.customerPhone }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Products:</span>
|
||||
<span class="value">{{ transactionDetails.products }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Transaction Date:</span>
|
||||
<span class="value">{{ transactionDetails.transactionDate }}</span>
|
||||
</div>
|
||||
<div class="detail-item" *ngIf="transactionDetails.errorCode">
|
||||
<span class="label">Error Code:</span>
|
||||
<span class="value error-code">{{ transactionDetails.errorCode }}</span>
|
||||
</div>
|
||||
<div class="detail-item failure-reason-item">
|
||||
<span class="label">Failure Reason:</span>
|
||||
<span class="value failure-reason">{{ transactionDetails.failureReason }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="failure-info" [ngClass]="getFailureReasonClass()">
|
||||
<h3>What happened?</h3>
|
||||
<div class="failure-explanation">
|
||||
<ng-container [ngSwitch]="getFailureReasonClass()">
|
||||
<div *ngSwitchCase="'insufficient-funds'">
|
||||
<p><strong>Insufficient Funds:</strong> Your account doesn't have enough balance for this transaction.</p>
|
||||
<ul>
|
||||
<li>Check your account balance</li>
|
||||
<li>Try using a different payment method</li>
|
||||
<li>Contact your bank if you believe this is an error</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div *ngSwitchCase="'timeout'">
|
||||
<p><strong>Transaction Timeout:</strong> The payment took too long to process.</p>
|
||||
<ul>
|
||||
<li>Check your internet connection</li>
|
||||
<li>Try the transaction again</li>
|
||||
<li>Contact your bank if the issue persists</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div *ngSwitchCase="'cancelled'">
|
||||
<p><strong>Transaction Cancelled:</strong> The payment was cancelled during processing.</p>
|
||||
<ul>
|
||||
<li>You may have cancelled the transaction</li>
|
||||
<li>Browser or app was closed during payment</li>
|
||||
<li>Security system blocked the transaction</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div *ngSwitchCase="'network'">
|
||||
<p><strong>Network Error:</strong> Connection issues prevented payment completion.</p>
|
||||
<ul>
|
||||
<li>Check your internet connection</li>
|
||||
<li>Try again with a stable connection</li>
|
||||
<li>Switch to mobile data if using WiFi</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div *ngSwitchCase="'authentication'">
|
||||
<p><strong>Authentication Failed:</strong> Card or payment details couldn't be verified.</p>
|
||||
<ul>
|
||||
<li>Verify your card details are correct</li>
|
||||
<li>Check if your card is activated for online transactions</li>
|
||||
<li>Contact your bank to enable online payments</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div *ngSwitchDefault>
|
||||
<p><strong>Payment Failed:</strong> Your payment could not be processed.</p>
|
||||
<ul>
|
||||
<li>Check your payment details</li>
|
||||
<li>Ensure your card is valid and active</li>
|
||||
<li>Try using a different payment method</li>
|
||||
<li>Contact your bank if the problem continues</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quick-tips">
|
||||
<h4>💡 Quick Tips for Successful Payment:</h4>
|
||||
<div class="tips-grid">
|
||||
<div class="tip-item">
|
||||
<span class="tip-icon">🔒</span>
|
||||
<span>Ensure your card is enabled for online transactions</span>
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<span class="tip-icon">📶</span>
|
||||
<span>Use a stable internet connection</span>
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<span class="tip-icon">💰</span>
|
||||
<span>Check if you have sufficient balance</span>
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<span class="tip-icon">⏱️</span>
|
||||
<span>Complete the payment within the time limit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="btn-primary" (click)="retryPayment()">
|
||||
<span class="btn-icon">🔄</span>
|
||||
Try Again
|
||||
</button>
|
||||
<button class="btn-secondary" (click)="goToMachine()">
|
||||
<span class="btn-icon">🏪</span>
|
||||
Return to Machine
|
||||
</button>
|
||||
<button class="btn-tertiary" (click)="contactSupport()">
|
||||
<span class="btn-icon">📞</span>
|
||||
Contact Support
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loading-card" *ngIf="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Processing transaction details...</p>
|
||||
</div>
|
||||
|
||||
<div class="error-card" *ngIf="error">
|
||||
<div class="error-icon">⚠</div>
|
||||
<h2>Error Loading Transaction Details</h2>
|
||||
<p>{{ error }}</p>
|
||||
<button class="btn-primary" (click)="goToMachine()">Return to Machine</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,445 @@
|
||||
/* payment-failure.component.scss */
|
||||
.payment-result-container {
|
||||
max-width: 900px;
|
||||
margin: 2rem auto;
|
||||
padding: 1rem;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.failure-card, .loading-card, .error-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
padding: 2.5rem;
|
||||
text-align: center;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.failure-header {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.failure-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3.5rem;
|
||||
margin: 0 auto 1.5rem;
|
||||
animation: shake 1s ease-in-out;
|
||||
|
||||
// Different styles based on failure reason
|
||||
&.insufficient-funds {
|
||||
background: linear-gradient(135deg, #ff6b35, #f7931e);
|
||||
box-shadow: 0 4px 20px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
&.timeout {
|
||||
background: linear-gradient(135deg, #ff9800, #f57c00);
|
||||
box-shadow: 0 4px 20px rgba(255, 152, 0, 0.3);
|
||||
}
|
||||
|
||||
&.cancelled {
|
||||
background: linear-gradient(135deg, #9c27b0, #7b1fa2);
|
||||
box-shadow: 0 4px 20px rgba(156, 39, 176, 0.3);
|
||||
}
|
||||
|
||||
&.network {
|
||||
background: linear-gradient(135deg, #607d8b, #455a64);
|
||||
box-shadow: 0 4px 20px rgba(96, 125, 139, 0.3);
|
||||
}
|
||||
|
||||
&.authentication {
|
||||
background: linear-gradient(135deg, #e91e63, #c2185b);
|
||||
box-shadow: 0 4px 20px rgba(233, 30, 99, 0.3);
|
||||
}
|
||||
|
||||
&.general-error {
|
||||
background: linear-gradient(135deg, #f44336, #d32f2f);
|
||||
box-shadow: 0 4px 20px rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(5px); }
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, #ff9800, #f57c00);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h2, h3, h4 {
|
||||
color: #34495e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.failure-message {
|
||||
color: #7f8c8d;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.transaction-details {
|
||||
text-align: left;
|
||||
margin-bottom: 2.5rem;
|
||||
background: #f8f9fa;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #e74c3c;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&.failure-reason-item {
|
||||
border-left-color: #e74c3c;
|
||||
background: #fdf2f2;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-item:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #34495e;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
word-break: break-word;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.amount {
|
||||
color: #e74c3c;
|
||||
font-weight: 700;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
color: #e74c3c;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
background: #ffeaea;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
color: #ff5722;
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
background: #fff3e0;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.failure-reason {
|
||||
color: #e74c3c;
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.failure-info {
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
text-align: left;
|
||||
border: 2px solid;
|
||||
|
||||
&.insufficient-funds {
|
||||
background: linear-gradient(135deg, #fff5f5, #fef5e7);
|
||||
border-color: #ff6b35;
|
||||
|
||||
h3 { color: #ff6b35; }
|
||||
}
|
||||
|
||||
&.timeout {
|
||||
background: linear-gradient(135deg, #fff8e1, #fef9c3);
|
||||
border-color: #ff9800;
|
||||
|
||||
h3 { color: #ff9800; }
|
||||
}
|
||||
|
||||
&.cancelled {
|
||||
background: linear-gradient(135deg, #fce4ec, #f3e5f5);
|
||||
border-color: #9c27b0;
|
||||
|
||||
h3 { color: #9c27b0; }
|
||||
}
|
||||
|
||||
&.network {
|
||||
background: linear-gradient(135deg, #eceff1, #cfd8dc);
|
||||
border-color: #607d8b;
|
||||
|
||||
h3 { color: #607d8b; }
|
||||
}
|
||||
|
||||
&.authentication {
|
||||
background: linear-gradient(135deg, #fce4ec, #f8bbd9);
|
||||
border-color: #e91e63;
|
||||
|
||||
h3 { color: #e91e63; }
|
||||
}
|
||||
|
||||
&.general-error {
|
||||
background: linear-gradient(135deg, #ffebee, #ffcdd2);
|
||||
border-color: #f44336;
|
||||
|
||||
h3 { color: #f44336; }
|
||||
}
|
||||
}
|
||||
|
||||
.failure-explanation {
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
color: #2c3e50;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
color: #34495e;
|
||||
padding-left: 1.5rem;
|
||||
line-height: 1.6;
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-tips {
|
||||
background: linear-gradient(135deg, #e8f5e8, #f0f8ff);
|
||||
border: 2px solid #4CAF50;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tips-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
font-size: 0.95rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.tip-icon {
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary, .btn-tertiary {
|
||||
padding: 14px 28px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #4CAF50, #45a049);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
||||
color: #495057;
|
||||
border: 2px solid #dee2e6;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
transform: translateY(-2px);
|
||||
background: linear-gradient(135deg, #e9ecef, #dee2e6);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn-tertiary {
|
||||
background: linear-gradient(135deg, #2196F3, #1976D2);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
|
||||
.btn-tertiary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(33, 150, 243, 0.4);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.loading-card {
|
||||
padding: 3rem;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #ff6b35;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 768px) {
|
||||
.payment-result-container {
|
||||
margin: 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.failure-card, .loading-card, .error-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary, .btn-tertiary {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
text-align: left;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.tips-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.failure-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.action-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.payment-result-container {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.failure-card {
|
||||
box-shadow: none;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PaymentFailureComponent } from './payment-failure.component';
|
||||
|
||||
describe('PaymentFailureComponent', () => {
|
||||
let component: PaymentFailureComponent;
|
||||
let fixture: ComponentFixture<PaymentFailureComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PaymentFailureComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PaymentFailureComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,173 @@
|
||||
// payment-failure.component.ts - Updated version
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
interface FailedTransactionDetails {
|
||||
transactionId: string;
|
||||
status: string;
|
||||
amount: string;
|
||||
customerName: string;
|
||||
customerEmail: string;
|
||||
customerPhone: string;
|
||||
products: string;
|
||||
failureReason: string;
|
||||
transactionDate: string;
|
||||
errorCode?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-payment-failure',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './payment-failure.component.html',
|
||||
styleUrls: ['./payment-failure.component.scss']
|
||||
})
|
||||
export class PaymentFailureComponent implements OnInit {
|
||||
transactionDetails: FailedTransactionDetails | null = null;
|
||||
loading = true;
|
||||
error = '';
|
||||
originalParams: any;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private http: HttpClient
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
// Get all query parameters from PayU response
|
||||
this.route.queryParams.subscribe(params => {
|
||||
console.log('PayU Failure Response Parameters:', params);
|
||||
this.originalParams = params;
|
||||
this.processFailureResponse(params);
|
||||
});
|
||||
}
|
||||
|
||||
processFailureResponse(params: any) {
|
||||
if (!params.txnid) {
|
||||
this.error = 'Invalid payment response received';
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Process the failure data directly (no backend logging needed for now)
|
||||
this.transactionDetails = {
|
||||
transactionId: params.txnid,
|
||||
status: params.status || 'Failed',
|
||||
amount: params.amount || '0.00',
|
||||
customerName: `${params.firstname || ''} ${params.lastname || ''}`.trim(),
|
||||
customerEmail: params.email || '',
|
||||
customerPhone: params.phone || '',
|
||||
products: params.productinfo || '',
|
||||
failureReason: params.error_Message || params.error || 'Payment processing failed',
|
||||
transactionDate: new Date().toLocaleString(),
|
||||
errorCode: params.error_code || params.error
|
||||
};
|
||||
|
||||
this.loading = false;
|
||||
|
||||
// Optional: Log to backend in the background (non-blocking)
|
||||
this.logFailureInBackground(params);
|
||||
}
|
||||
|
||||
logFailureInBackground(params: any) {
|
||||
// Optional background logging - won't block the UI if it fails
|
||||
this.http.post(`${environment.apiUrl}/log-payment-failure`, params).subscribe({
|
||||
next: (response) => {
|
||||
console.log('Payment failure logged successfully');
|
||||
},
|
||||
error: (error) => {
|
||||
console.log('Failed to log payment failure (continuing anyway):', error);
|
||||
// Don't show error to user - this is just background logging
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
retryPayment() {
|
||||
// Check if we have pending cart data in session
|
||||
const pendingCart = sessionStorage.getItem('pendingCart');
|
||||
const pendingMachineId = sessionStorage.getItem('pendingMachineId');
|
||||
|
||||
if (pendingMachineId) {
|
||||
// Redirect back to the machine with cart intact
|
||||
this.router.navigate(['/machine-slots', pendingMachineId]);
|
||||
} else if (this.originalParams.udf1) {
|
||||
// Use machine ID from UDF1 if available
|
||||
this.router.navigate(['/machine-slots', this.originalParams.udf1]);
|
||||
} else {
|
||||
// Fallback to login page
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
||||
|
||||
goToMachine() {
|
||||
// Clear cart and redirect to machine selection
|
||||
sessionStorage.removeItem('pendingCart');
|
||||
sessionStorage.removeItem('pendingMachineId');
|
||||
|
||||
if (this.originalParams.udf1) {
|
||||
this.router.navigate(['/machine-slots', this.originalParams.udf1]);
|
||||
} else {
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
||||
|
||||
contactSupport() {
|
||||
// Create support email with transaction details
|
||||
const subject = `Payment Failure - Transaction ID: ${this.transactionDetails?.transactionId}`;
|
||||
const body = `Dear Support Team,
|
||||
|
||||
I encountered a payment failure with the following details:
|
||||
|
||||
Transaction ID: ${this.transactionDetails?.transactionId}
|
||||
Amount: ₹${this.transactionDetails?.amount}
|
||||
Status: ${this.transactionDetails?.status}
|
||||
Error: ${this.transactionDetails?.failureReason}
|
||||
Date: ${this.transactionDetails?.transactionDate}
|
||||
|
||||
Customer Details:
|
||||
Name: ${this.transactionDetails?.customerName}
|
||||
Email: ${this.transactionDetails?.customerEmail}
|
||||
Phone: ${this.transactionDetails?.customerPhone}
|
||||
|
||||
Products: ${this.transactionDetails?.products}
|
||||
|
||||
Please assist me with this issue.
|
||||
|
||||
Thank you.`;
|
||||
|
||||
const mailtoUrl = `mailto:support@yourdomain.com?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
||||
window.location.href = mailtoUrl;
|
||||
}
|
||||
|
||||
getFailureReasonClass(): string {
|
||||
if (!this.transactionDetails?.failureReason) return '';
|
||||
|
||||
const reason = this.transactionDetails.failureReason.toLowerCase();
|
||||
|
||||
if (reason.includes('insufficient') || reason.includes('balance')) return 'insufficient-funds';
|
||||
if (reason.includes('timeout') || reason.includes('expired')) return 'timeout';
|
||||
if (reason.includes('cancelled') || reason.includes('abort')) return 'cancelled';
|
||||
if (reason.includes('network') || reason.includes('connection')) return 'network';
|
||||
if (reason.includes('authentication') || reason.includes('invalid')) return 'authentication';
|
||||
|
||||
return 'general-error';
|
||||
}
|
||||
|
||||
getFailureIcon(): string {
|
||||
const reasonClass = this.getFailureReasonClass();
|
||||
|
||||
switch (reasonClass) {
|
||||
case 'insufficient-funds': return '💳';
|
||||
case 'timeout': return '⏱️';
|
||||
case 'cancelled': return '❌';
|
||||
case 'network': return '🌐';
|
||||
case 'authentication': return '🔒';
|
||||
default: return '⚠️';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
<!-- payment-success.component.html -->
|
||||
<div class="payment-result-container">
|
||||
<div class="success-card" *ngIf="transactionDetails">
|
||||
<div class="success-header">
|
||||
<div class="success-icon">✓</div>
|
||||
<h1>Payment Successful!</h1>
|
||||
<p class="success-message">Thank you for your purchase. Your transaction has been completed successfully.</p>
|
||||
</div>
|
||||
|
||||
<div class="transaction-details">
|
||||
<h2>Transaction Details</h2>
|
||||
<div class="details-grid">
|
||||
<div class="detail-item">
|
||||
<span class="label">Transaction ID:</span>
|
||||
<span class="value">{{ transactionDetails.transactionId }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Payment ID:</span>
|
||||
<span class="value">{{ transactionDetails.paymentId }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Amount:</span>
|
||||
<span class="value amount">₹{{ transactionDetails.amount }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Status:</span>
|
||||
<span class="value status-success">{{ transactionDetails.status }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Customer Name:</span>
|
||||
<span class="value">{{ transactionDetails.customerName }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Email:</span>
|
||||
<span class="value">{{ transactionDetails.customerEmail }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Phone:</span>
|
||||
<span class="value">{{ transactionDetails.customerPhone }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Products:</span>
|
||||
<span class="value">{{ transactionDetails.products }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Payment Mode:</span>
|
||||
<span class="value">{{ transactionDetails.paymentMode }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Transaction Date:</span>
|
||||
<span class="value">{{ transactionDetails.transactionDate }}</span>
|
||||
</div>
|
||||
<div class="detail-item" *ngIf="transactionDetails.bankRefNumber">
|
||||
<span class="label">Bank Reference:</span>
|
||||
<span class="value">{{ transactionDetails.bankRefNumber }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dispensing-status">
|
||||
<div class="dispensing-icon">🎁</div>
|
||||
<p>Your products are being dispensed from the vending machine!</p>
|
||||
<p class="dispensing-note">Please collect your items from the machine.</p>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="btn-primary" (click)="goToMachine()">Continue Shopping</button>
|
||||
<button class="btn-secondary" (click)="printReceipt()">Print Receipt</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loading-card" *ngIf="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Processing transaction details...</p>
|
||||
</div>
|
||||
|
||||
<div class="error-card" *ngIf="error">
|
||||
<div class="error-icon">⚠</div>
|
||||
<h2>Error Loading Transaction Details</h2>
|
||||
<p>{{ error }}</p>
|
||||
<button class="btn-primary" (click)="goToMachine()">Return to Machine</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,285 @@
|
||||
/* payment-success.component.scss */
|
||||
.payment-result-container {
|
||||
max-width: 800px;
|
||||
margin: 2rem auto;
|
||||
padding: 1rem;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.success-card, .loading-card, .error-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
padding: 2.5rem;
|
||||
text-align: center;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.success-header {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: linear-gradient(135deg, #4CAF50, #45a049);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3.5rem;
|
||||
margin: 0 auto 1.5rem;
|
||||
box-shadow: 0 4px 20px rgba(76, 175, 80, 0.3);
|
||||
animation: successPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes successPulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, #f44336, #d32f2f);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #34495e;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
color: #7f8c8d;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.transaction-details {
|
||||
text-align: left;
|
||||
margin-bottom: 2.5rem;
|
||||
background: #f8f9fa;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #4CAF50;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.detail-item:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #34495e;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
word-break: break-word;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.amount {
|
||||
color: #27ae60;
|
||||
font-weight: 700;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: #27ae60;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
background: #d4edda;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.dispensing-status {
|
||||
background: linear-gradient(135deg, #e3f2fd, #f3e5f5);
|
||||
border: 2px solid #2196F3;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dispensing-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
|
||||
40% { transform: translateY(-10px); }
|
||||
60% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
.dispensing-note {
|
||||
color: #1976D2;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
margin: 0.5rem 0 0 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
padding: 14px 28px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 160px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #4CAF50, #45a049);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
||||
color: #495057;
|
||||
border: 2px solid #dee2e6;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
transform: translateY(-2px);
|
||||
background: linear-gradient(135deg, #e9ecef, #dee2e6);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.loading-card {
|
||||
padding: 3rem;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #4CAF50;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 768px) {
|
||||
.payment-result-container {
|
||||
margin: 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.success-card, .loading-card, .error-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
text-align: left;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.action-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.payment-result-container {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.success-card {
|
||||
box-shadow: none;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PaymentSuccessComponent } from './payment-success.component';
|
||||
|
||||
describe('PaymentSuccessComponent', () => {
|
||||
let component: PaymentSuccessComponent;
|
||||
let fixture: ComponentFixture<PaymentSuccessComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PaymentSuccessComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PaymentSuccessComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,142 @@
|
||||
// payment-success.component.ts - Updated version
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
interface TransactionDetails {
|
||||
transactionId: string;
|
||||
paymentId: string;
|
||||
status: string;
|
||||
amount: string;
|
||||
customerName: string;
|
||||
customerEmail: string;
|
||||
customerPhone: string;
|
||||
products: string;
|
||||
paymentMode: string;
|
||||
transactionDate: string;
|
||||
bankRefNumber: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-payment-success',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './payment-success.component.html',
|
||||
styleUrls: ['./payment-success.component.scss']
|
||||
})
|
||||
export class PaymentSuccessComponent implements OnInit {
|
||||
transactionDetails: TransactionDetails | null = null;
|
||||
loading = true;
|
||||
error = '';
|
||||
dispensing = false;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private http: HttpClient
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
// Get all query parameters from PayU response
|
||||
this.route.queryParams.subscribe(params => {
|
||||
console.log('PayU Response Parameters:', params);
|
||||
this.processPaymentResponse(params);
|
||||
});
|
||||
}
|
||||
|
||||
processPaymentResponse(params: any) {
|
||||
if (!params.txnid || !params.status) {
|
||||
this.error = 'Invalid payment response received';
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Process the payment data directly (no backend verification needed)
|
||||
this.transactionDetails = {
|
||||
transactionId: params.txnid,
|
||||
paymentId: params.mihpayid || params.payuMoneyId || 'N/A',
|
||||
status: params.status,
|
||||
amount: params.amount,
|
||||
customerName: `${params.firstname || ''} ${params.lastname || ''}`.trim(),
|
||||
customerEmail: params.email || '',
|
||||
customerPhone: params.phone || '',
|
||||
products: params.productinfo || '',
|
||||
paymentMode: params.mode || 'Online',
|
||||
transactionDate: new Date().toLocaleString(),
|
||||
bankRefNumber: params.bank_ref_num || ''
|
||||
};
|
||||
|
||||
this.loading = false;
|
||||
|
||||
// Process dispensing if payment successful
|
||||
if (params.status === 'success') {
|
||||
this.processMachineDispensing(params);
|
||||
}
|
||||
}
|
||||
|
||||
processMachineDispensing(params: any) {
|
||||
// Extract machine dispensing instructions from UDF fields
|
||||
const machineId = params.udf1; // Machine ID stored in UDF1
|
||||
const cartData = params.udf2; // Cart data stored in UDF2
|
||||
|
||||
if (machineId && cartData && !this.dispensing) {
|
||||
this.dispensing = true;
|
||||
|
||||
try {
|
||||
const parsedCartData = JSON.parse(cartData);
|
||||
|
||||
// Create dispensing instructions
|
||||
const dispensingInstructions = parsedCartData.map((item: any) => ({
|
||||
slotId: item.slotId,
|
||||
rowId: item.slotId ? item.slotId[0] : '',
|
||||
column: item.slotId ? parseInt(item.slotId.substring(1)) : 0,
|
||||
quantity: item.quantity,
|
||||
productName: item.productName
|
||||
}));
|
||||
|
||||
this.http.post(`${environment.apiUrl}/dispense-products`, {
|
||||
transactionId: params.txnid,
|
||||
machineId: machineId,
|
||||
paymentStatus: params.status,
|
||||
dispensingInstructions: dispensingInstructions
|
||||
}).subscribe({
|
||||
next: (response) => {
|
||||
console.log('Dispensing initiated:', response);
|
||||
// Clear cart from session storage
|
||||
sessionStorage.removeItem('pendingCart');
|
||||
sessionStorage.removeItem('pendingMachineId');
|
||||
|
||||
setTimeout(() => {
|
||||
this.dispensing = false;
|
||||
}, 3000);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Dispensing error:', error);
|
||||
this.dispensing = false;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error parsing cart data:', e);
|
||||
this.dispensing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
goToMachine() {
|
||||
// Get machine ID from transaction details
|
||||
const machineId = this.route.snapshot.queryParams['udf1'];
|
||||
|
||||
if (machineId) {
|
||||
this.router.navigate(['/machine-slots', machineId]);
|
||||
} else {
|
||||
// Fallback - you might need to adjust this route based on your app structure
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
||||
|
||||
printReceipt() {
|
||||
window.print();
|
||||
}
|
||||
}
|
||||
26
machine-operations/src/app/payment/payment.component.html
Normal file
26
machine-operations/src/app/payment/payment.component.html
Normal file
@ -0,0 +1,26 @@
|
||||
<form #paymentForm action="https://test.payu.in/_payment" method="post" ngNoForm>
|
||||
<input type="hidden" name="key" [(ngModel)]="payUParams.key" />
|
||||
<input type="hidden" name="txnid" [(ngModel)]="payUParams.txnid" />
|
||||
<input type="hidden" name="amount" [(ngModel)]="payUParams.amount" />
|
||||
<input type="hidden" name="productinfo" [(ngModel)]="payUParams.productinfo" />
|
||||
<input type="hidden" name="firstname" [(ngModel)]="payUParams.firstname" />
|
||||
<input type="hidden" name="email" [(ngModel)]="payUParams.email" />
|
||||
<input type="hidden" name="phone" [(ngModel)]="payUParams.phone" />
|
||||
<input type="hidden" name="surl" [(ngModel)]="payUParams.surl" />
|
||||
<input type="hidden" name="furl" [(ngModel)]="payUParams.furl" />
|
||||
<input type="hidden" name="hash" [(ngModel)]="payUParams.hash" />
|
||||
|
||||
<div>
|
||||
<label>First Name:</label>
|
||||
<input type="text" [(ngModel)]="payUParams.firstname" name="firstname" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Email:</label>
|
||||
<input type="email" [(ngModel)]="payUParams.email" name="email" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Phone:</label>
|
||||
<input type="tel" [(ngModel)]="payUParams.phone" name="phone" />
|
||||
</div>
|
||||
<button type="submit" (click)="submitPayUForm()">Pay Now</button>
|
||||
</form>
|
||||
23
machine-operations/src/app/payment/payment.component.spec.ts
Normal file
23
machine-operations/src/app/payment/payment.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PaymentComponent } from './payment.component';
|
||||
|
||||
describe('PaymentComponent', () => {
|
||||
let component: PaymentComponent;
|
||||
let fixture: ComponentFixture<PaymentComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PaymentComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PaymentComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
91
machine-operations/src/app/payment/payment.component.ts
Normal file
91
machine-operations/src/app/payment/payment.component.ts
Normal file
@ -0,0 +1,91 @@
|
||||
// payment.component.ts - Fixed version
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-payment',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
templateUrl: './payment.component.html',
|
||||
styleUrls: ['./payment.component.scss']
|
||||
})
|
||||
export class PaymentComponent implements OnInit {
|
||||
payUParams: any = {
|
||||
key: 'VqvG3m',
|
||||
txnid: this.generateTxnId(),
|
||||
amount: '0.00',
|
||||
productinfo: '',
|
||||
firstname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
// Use PayU's test success/failure URLs for now
|
||||
surl: 'https://apiplayground-response.herokuapp.com/', // Test success URL
|
||||
furl: 'https://apiplayground-response.herokuapp.com/', // Test failure URL
|
||||
hash: ''
|
||||
};
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
generateTxnId(): string {
|
||||
return 'txn_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
submitPayUForm() {
|
||||
// Validate inputs
|
||||
if (!this.payUParams.firstname || !this.payUParams.email || !this.payUParams.phone || !this.payUParams.amount) {
|
||||
alert('Please fill all required fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send to backend to generate hash
|
||||
this.http
|
||||
.post(`${environment.apiUrl}/create-payu-order`, this.payUParams)
|
||||
.subscribe(
|
||||
(response: any) => {
|
||||
console.log('PayU response:', response);
|
||||
|
||||
// Update payUParams with response data
|
||||
this.payUParams = {
|
||||
...this.payUParams,
|
||||
...response
|
||||
};
|
||||
|
||||
// Auto-submit form
|
||||
setTimeout(() => {
|
||||
this.submitFormToPayU();
|
||||
}, 100);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error generating hash:', error);
|
||||
alert('Failed to initiate payment. Please try again.');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private submitFormToPayU() {
|
||||
// Create form dynamically
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = 'https://test.payu.in/_payment';
|
||||
|
||||
// Add all PayU parameters as hidden inputs
|
||||
Object.keys(this.payUParams).forEach(key => {
|
||||
if (this.payUParams[key]) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = key;
|
||||
input.value = this.payUParams[key];
|
||||
form.appendChild(input);
|
||||
}
|
||||
});
|
||||
|
||||
// Submit form
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
39
machine-operations/src/app/services/auth.service.ts
Normal file
39
machine-operations/src/app/services/auth.service.ts
Normal file
@ -0,0 +1,39 @@
|
||||
// src/app/services/auth.service.ts
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private apiUrl = environment.apiUrl;
|
||||
|
||||
constructor(private http: HttpClient, private router: Router) {}
|
||||
|
||||
login(machineId: string, password: string): Observable<any> {
|
||||
return this.http.post(`${this.apiUrl}/login`, { machine_id: machineId, password });
|
||||
}
|
||||
|
||||
setLoggedInMachine(machineId: string, machineIdNum: number) {
|
||||
localStorage.setItem('loggedInMachineId', machineId);
|
||||
localStorage.setItem('loggedInMachineIdNum', machineIdNum.toString());
|
||||
}
|
||||
|
||||
logout() {
|
||||
localStorage.removeItem('loggedInMachineId');
|
||||
localStorage.removeItem('loggedInMachineIdNum');
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
|
||||
getLoggedInMachineId(): string | null {
|
||||
return localStorage.getItem('loggedInMachineId');
|
||||
}
|
||||
|
||||
getLoggedInMachineIdNum(): number | null {
|
||||
const idNum = localStorage.getItem('loggedInMachineIdNum');
|
||||
return idNum ? +idNum : null;
|
||||
}
|
||||
}
|
||||
6
machine-operations/src/environments/environment.ts
Normal file
6
machine-operations/src/environments/environment.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// src/environments/environment.ts
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiUrl: 'http://localhost:5001', // Points to existing app's backend
|
||||
payuUrl: 'https://test.payu.in/_payment'
|
||||
};
|
||||
13
machine-operations/src/index.html
Normal file
13
machine-operations/src/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>MachineOperations</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>
|
||||
7
machine-operations/src/main.server.ts
Normal file
7
machine-operations/src/main.server.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { config } from './app/app.config.server';
|
||||
|
||||
const bootstrap = () => bootstrapApplication(AppComponent, config);
|
||||
|
||||
export default bootstrap;
|
||||
17
machine-operations/src/main.ts
Normal file
17
machine-operations/src/main.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { routes } from './app/app.routes';
|
||||
import { importProvidersFrom } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
bootstrapApplication(AppComponent,{
|
||||
providers: [
|
||||
provideHttpClient(), // Add this to provide HttpClient
|
||||
provideRouter(routes),
|
||||
importProvidersFrom(ReactiveFormsModule)
|
||||
]
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
67
machine-operations/src/server.ts
Normal file
67
machine-operations/src/server.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { CommonEngine, isMainModule } from '@angular/ssr/node';
|
||||
import express from 'express';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import bootstrap from './main.server';
|
||||
|
||||
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
|
||||
const browserDistFolder = resolve(serverDistFolder, '../browser');
|
||||
const indexHtml = join(serverDistFolder, 'index.server.html');
|
||||
|
||||
const app = express();
|
||||
const commonEngine = new CommonEngine();
|
||||
|
||||
/**
|
||||
* Example Express Rest API endpoints can be defined here.
|
||||
* Uncomment and define endpoints as necessary.
|
||||
*
|
||||
* Example:
|
||||
* ```ts
|
||||
* app.get('/api/**', (req, res) => {
|
||||
* // Handle API request
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
/**
|
||||
* Serve static files from /browser
|
||||
*/
|
||||
app.get(
|
||||
'**',
|
||||
express.static(browserDistFolder, {
|
||||
maxAge: '1y',
|
||||
index: 'index.html'
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle all other requests by rendering the Angular application.
|
||||
*/
|
||||
app.get('**', (req, res, next) => {
|
||||
const { protocol, originalUrl, baseUrl, headers } = req;
|
||||
|
||||
commonEngine
|
||||
.render({
|
||||
bootstrap,
|
||||
documentFilePath: indexHtml,
|
||||
url: `${protocol}://${headers.host}${originalUrl}`,
|
||||
publicPath: browserDistFolder,
|
||||
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
|
||||
})
|
||||
.then((html) => res.send(html))
|
||||
.catch((err) => next(err));
|
||||
});
|
||||
|
||||
/**
|
||||
* Start the server if this module is the main entry point.
|
||||
* The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
|
||||
*/
|
||||
if (isMainModule(import.meta.url)) {
|
||||
const port = process.env['PORT'] || 4000;
|
||||
app.listen(port, () => {
|
||||
console.log(`Node Express server listening on http://localhost:${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
export default app;
|
||||
1
machine-operations/src/styles.scss
Normal file
1
machine-operations/src/styles.scss
Normal file
@ -0,0 +1 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
Reference in New Issue
Block a user