1import { useState, useEffect } from 'react';
2import { apiFetch } from '../lib/api';
4export default function PurchaseLicenseModal({ onClose, onSuccess }) {
5 const [step, setStep] = useState(1);
6 const [loading, setLoading] = useState(false);
7 const [error, setError] = useState(null);
9 // Step 1: Customer selection
10 const [customers, setCustomers] = useState([]);
11 const [selectedCustomer, setSelectedCustomer] = useState(null);
12 const [syncing, setSyncing] = useState(false);
14 // Step 2: Product selection
15 const [products, setProducts] = useState([]);
16 const [selectedProduct, setSelectedProduct] = useState(null);
17 const [productSearch, setProductSearch] = useState('');
18 const [productFilter, setProductFilter] = useState('all'); // all, business, enterprise
20 // Step 3-4: Configuration
21 const [quantity, setQuantity] = useState(1);
22 const [billingTerm, setBillingTerm] = useState('monthly');
25 const [purchasing, setPurchasing] = useState(false);
30 } else if (step === 2 && !products.length) {
35 const fetchCustomers = async () => {
39 const token = localStorage.getItem('token');
40 const res = await apiFetch('/customers', {
41 headers: { Authorization: `Bearer ${token}` }
45 const data = await res.json();
46 setCustomers(data.customers || []);
48 throw new Error('Failed to fetch customers');
51 setError(err.message);
57 const fetchProducts = async () => {
61 const token = localStorage.getItem('token');
62 const res = await apiFetch('/office365/products', {
63 headers: { Authorization: `Bearer ${token}` }
67 const data = await res.json();
68 setProducts(data.products || []);
70 throw new Error('Failed to fetch product catalog');
73 setError('Unable to load product catalog. Please ensure Pax8 is configured in Settings.');
79 const syncCustomer = async (customerId) => {
83 const token = localStorage.getItem('token');
84 const res = await apiFetch(`/office365/customers/${customerId}/sync`, {
86 headers: { Authorization: `Bearer ${token}` }
90 // Refresh customer list to update sync status
91 await fetchCustomers();
92 const customer = customers.find(c => c.customer_id === customerId);
93 setSelectedCustomer({ ...customer, synced: true });
95 throw new Error('Failed to sync customer with Pax8');
98 setError(err.message);
104 const handleCustomerSelect = (customer) => {
105 setSelectedCustomer(customer);
108 const handleProductSelect = (product) => {
109 setSelectedProduct(product);
112 const handlePurchase = async () => {
117 const token = localStorage.getItem('token');
118 const res = await apiFetch('/office365/subscriptions', {
121 'Authorization': `Bearer ${token}`,
122 'Content-Type': 'application/json'
124 body: JSON.stringify({
125 customer_id: selectedCustomer.customer_id,
126 product_id: selectedProduct.pax8_product_id,
128 billing_term: billingTerm
133 onSuccess && onSuccess();
136 const data = await res.json();
137 throw new Error(data.error || 'Failed to create subscription');
140 setError(err.message);
142 setPurchasing(false);
146 const canProceedToStep2 = selectedCustomer && selectedCustomer.synced;
147 const canProceedToStep3 = selectedProduct;
148 const canProceedToStep4 = quantity > 0;
150 const formatCurrency = (amount) => {
151 return new Intl.NumberFormat('en-AU', {
154 }).format(amount || 0);
157 const calculateTotal = () => {
158 if (!selectedProduct) return 0;
159 const monthlyPrice = selectedProduct.unit_price || 0;
160 const total = monthlyPrice * quantity * (billingTerm === 'annual' ? 12 : 1);
164 const filteredProducts = products.filter(product => {
165 const matchesSearch = !productSearch ||
166 product.name.toLowerCase().includes(productSearch.toLowerCase()) ||
167 (product.sku && product.sku.toLowerCase().includes(productSearch.toLowerCase()));
169 const matchesFilter = productFilter === 'all' ||
170 (productFilter === 'business' && product.category?.toLowerCase().includes('business')) ||
171 (productFilter === 'enterprise' && product.category?.toLowerCase().includes('enterprise'));
173 return matchesSearch && matchesFilter;
177 <div className="modal-overlay" onClick={onClose}>
178 <div className="modal modal-lg" onClick={(e) => e.stopPropagation()}>
179 <div className="modal-header">
180 <h2>Purchase Microsoft 365 License</h2>
181 <button className="modal-close" onClick={onClose}>×</button>
184 {/* Progress Steps */}
185 <div style={{ padding: '20px', borderBottom: '1px solid var(--border)' }}>
186 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
187 {[1, 2, 3, 4, 5].map((s) => (
188 <div key={s} style={{ flex: 1, textAlign: 'center' }}>
193 background: step >= s ? 'var(--primary)' : '#ddd',
194 color: step >= s ? 'white' : '#888',
196 alignItems: 'center',
197 justifyContent: 'center',
198 margin: '0 auto 8px',
203 <div style={{ fontSize: '12px', color: step >= s ? 'var(--text)' : '#888' }}>
204 {['Customer', 'Product', 'Quantity', 'Term', 'Review'][s - 1]}
211 <div className="modal-body" style={{ minHeight: '400px', maxHeight: '500px', overflow: 'auto' }}>
216 border: '1px solid #fcc',
225 {/* Step 1: Select Customer */}
228 <h3 style={{ marginBottom: '16px' }}>Select Customer</h3>
229 <p style={{ color: '#888', marginBottom: '20px' }}>
230 Choose the customer who will receive the Microsoft 365 licenses
234 <div style={{ textAlign: 'center', padding: '40px' }}>
235 <div className="spinner"></div>
236 <p style={{ marginTop: '16px', color: '#888' }}>Loading customers...</p>
238 ) : customers.length === 0 ? (
239 <div style={{ textAlign: 'center', padding: '40px', color: '#888' }}>
240 <p>No customers found. Please create a customer first.</p>
243 <div style={{ display: 'grid', gap: '12px' }}>
244 {customers.map((customer) => (
246 key={customer.customer_id}
247 onClick={() => handleCustomerSelect(customer)}
250 border: `2px solid ${selectedCustomer?.customer_id === customer.customer_id ? 'var(--primary)' : '#ddd'}`,
253 transition: 'all 0.2s',
254 background: selectedCustomer?.customer_id === customer.customer_id ? '#f0f8ff' : 'white'
257 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
259 <strong>{customer.customer_name}</strong>
260 {customer.contact_email && (
261 <div style={{ fontSize: '14px', color: '#888', marginTop: '4px' }}>
262 {customer.contact_email}
269 background: '#d4edda',
279 className="btn btn-sm"
282 syncCustomer(customer.customer_id);
286 {syncing ? 'Syncing...' : 'Sync to Pax8'}
295 {selectedCustomer && !selectedCustomer.synced && (
299 background: '#fff3cd',
300 border: '1px solid #ffc107',
304 <strong>⚠️ Customer not synced</strong>
305 <p style={{ margin: '4px 0 0 0', fontSize: '14px' }}>
306 Please sync this customer with Pax8 before purchasing licenses.
313 {/* Step 2: Select Product */}
316 <h3 style={{ marginBottom: '16px' }}>Select Product</h3>
317 <p style={{ color: '#888', marginBottom: '20px' }}>
318 Choose the Microsoft 365 license type
321 <div style={{ marginBottom: '20px' }}>
324 className="form-input"
325 placeholder="Search products..."
326 value={productSearch}
327 onChange={(e) => setProductSearch(e.target.value)}
328 style={{ marginBottom: '12px' }}
330 <div style={{ display: 'flex', gap: '8px' }}>
332 className={`btn ${productFilter === 'all' ? 'btn-primary' : ''}`}
333 onClick={() => setProductFilter('all')}
338 className={`btn ${productFilter === 'business' ? 'btn-primary' : ''}`}
339 onClick={() => setProductFilter('business')}
344 className={`btn ${productFilter === 'enterprise' ? 'btn-primary' : ''}`}
345 onClick={() => setProductFilter('enterprise')}
353 <div style={{ textAlign: 'center', padding: '40px' }}>
354 <div className="spinner"></div>
355 <p style={{ marginTop: '16px', color: '#888' }}>Loading products...</p>
357 ) : filteredProducts.length === 0 ? (
358 <div style={{ textAlign: 'center', padding: '40px', color: '#888' }}>
359 <p>No products found matching your criteria.</p>
362 <div style={{ display: 'grid', gap: '12px' }}>
363 {filteredProducts.map((product) => (
365 key={product.pax8_product_id}
366 onClick={() => handleProductSelect(product)}
369 border: `2px solid ${selectedProduct?.pax8_product_id === product.pax8_product_id ? 'var(--primary)' : '#ddd'}`,
372 transition: 'all 0.2s',
373 background: selectedProduct?.pax8_product_id === product.pax8_product_id ? '#f0f8ff' : 'white'
376 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
377 <div style={{ flex: 1 }}>
378 <strong>{product.name}</strong>
379 {product.description && (
380 <div style={{ fontSize: '14px', color: '#666', marginTop: '4px' }}>
381 {product.description}
385 <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
390 <div style={{ textAlign: 'right', marginLeft: '16px' }}>
391 <div style={{ fontSize: '18px', fontWeight: 'bold', color: 'var(--primary)' }}>
392 {formatCurrency(product.unit_price)}
394 <div style={{ fontSize: '12px', color: '#888' }}>
406 {/* Step 3: Select Quantity */}
409 <h3 style={{ marginBottom: '16px' }}>Select Quantity</h3>
410 <p style={{ color: '#888', marginBottom: '20px' }}>
411 How many licenses do you need?
414 <div style={{ marginBottom: '24px' }}>
415 <label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
420 className="form-input"
423 onChange={(e) => setQuantity(parseInt(e.target.value) || 1)}
424 style={{ maxWidth: '200px', fontSize: '18px', textAlign: 'center' }}
428 {selectedProduct && (
431 background: '#f8f9fa',
433 border: '1px solid #ddd'
435 <div style={{ marginBottom: '12px' }}>
436 <strong>{selectedProduct.name}</strong>
438 <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
439 <span>Unit Price:</span>
440 <span>{formatCurrency(selectedProduct.unit_price)}/month</span>
442 <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
443 <span>Quantity:</span>
444 <span>{quantity}</span>
448 justifyContent: 'space-between',
450 borderTop: '2px solid #ddd',
454 <span>Monthly Total:</span>
455 <span style={{ color: 'var(--primary)' }}>
456 {formatCurrency(selectedProduct.unit_price * quantity)}
464 {/* Step 4: Select Billing Term */}
467 <h3 style={{ marginBottom: '16px' }}>Select Billing Term</h3>
468 <p style={{ color: '#888', marginBottom: '20px' }}>
469 Choose your billing frequency
472 <div style={{ display: 'grid', gap: '12px', marginBottom: '24px' }}>
474 onClick={() => setBillingTerm('monthly')}
477 border: `2px solid ${billingTerm === 'monthly' ? 'var(--primary)' : '#ddd'}`,
480 background: billingTerm === 'monthly' ? '#f0f8ff' : 'white'
483 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
485 <strong>Monthly Billing</strong>
486 <div style={{ fontSize: '14px', color: '#888', marginTop: '4px' }}>
487 Pay month-to-month with flexibility to cancel anytime
490 <div style={{ fontSize: '18px', fontWeight: 'bold', color: 'var(--primary)' }}>
491 {selectedProduct && formatCurrency(selectedProduct.unit_price * quantity)}
497 onClick={() => setBillingTerm('annual')}
500 border: `2px solid ${billingTerm === 'annual' ? 'var(--primary)' : '#ddd'}`,
503 background: billingTerm === 'annual' ? '#f0f8ff' : 'white'
506 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
508 <strong>Annual Billing</strong>
509 <div style={{ fontSize: '14px', color: '#888', marginTop: '4px' }}>
510 Pay annually for the full year upfront
513 <div style={{ fontSize: '18px', fontWeight: 'bold', color: 'var(--primary)' }}>
514 {selectedProduct && formatCurrency(selectedProduct.unit_price * quantity * 12)}
522 {/* Step 5: Review & Confirm */}
525 <h3 style={{ marginBottom: '16px' }}>Review and Confirm</h3>
526 <p style={{ color: '#888', marginBottom: '20px' }}>
527 Please review your order before confirming
532 background: '#f8f9fa',
534 border: '1px solid #ddd',
537 <div style={{ marginBottom: '16px' }}>
538 <div style={{ color: '#888', fontSize: '14px', marginBottom: '4px' }}>Customer</div>
539 <strong>{selectedCustomer?.customer_name}</strong>
542 <div style={{ marginBottom: '16px' }}>
543 <div style={{ color: '#888', fontSize: '14px', marginBottom: '4px' }}>Product</div>
544 <strong>{selectedProduct?.name}</strong>
545 {selectedProduct?.sku && (
546 <div style={{ fontSize: '12px', color: '#888' }}>SKU: {selectedProduct.sku}</div>
550 <div style={{ marginBottom: '16px' }}>
551 <div style={{ color: '#888', fontSize: '14px', marginBottom: '4px' }}>Quantity</div>
552 <strong>{quantity} licenses</strong>
555 <div style={{ marginBottom: '16px' }}>
556 <div style={{ color: '#888', fontSize: '14px', marginBottom: '4px' }}>Billing Term</div>
557 <strong>{billingTerm === 'monthly' ? 'Monthly' : 'Annual'}</strong>
562 borderTop: '2px solid #ddd',
565 <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '18px', fontWeight: 'bold' }}>
566 <span>Total {billingTerm === 'annual' ? '(Annual)' : '(Monthly)'}:</span>
567 <span style={{ color: 'var(--primary)' }}>
568 {formatCurrency(calculateTotal())}
571 {billingTerm === 'annual' && (
572 <div style={{ fontSize: '14px', color: '#888', textAlign: 'right', marginTop: '4px' }}>
573 {formatCurrency(calculateTotal() / 12)}/month
581 background: '#d1ecf1',
582 border: '1px solid #bee5eb',
587 <strong>ⓘ Note:</strong> This subscription will be created in Pax8 and linked to the selected customer.
593 <div className="modal-footer" style={{ display: 'flex', justifyContent: 'space-between' }}>
596 onClick={() => step === 1 ? onClose() : setStep(step - 1)}
597 disabled={purchasing}
599 {step === 1 ? 'Cancel' : 'Back'}
604 className="btn btn-primary"
605 onClick={() => setStep(step + 1)}
607 (step === 1 && !canProceedToStep2) ||
608 (step === 2 && !canProceedToStep3) ||
609 (step === 3 && !canProceedToStep4) ||
617 className="btn btn-primary"
618 onClick={handlePurchase}
619 disabled={purchasing}
621 {purchasing ? 'Processing...' : 'Confirm Purchase'}