Replace submodules with full folder contents

This commit is contained in:
2025-10-14 17:07:03 +05:30
parent 8805b6146e
commit c24f610178
909 changed files with 116738 additions and 3 deletions

View File

@ -0,0 +1 @@
<router-outlet></router-outlet>

View 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');
});
});

View 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';
}

View 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);

View 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()]
};

View 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 }
];

View 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>

View 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);
}
}

View 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();
});
});

View 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.'
};
}
);
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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);
}
}
}
======================================== */
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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 '⚠️';
}
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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();
}
}

View 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>

View 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();
});
});

View 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();
}
}

View 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;
}
}

View 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'
};

View 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>

View 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;

View 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));

View 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;

View File

@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */