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