Improve backend
This commit is contained in:
296
frontend/src/pages/app/admin/WhatsAppAdminPage.tsx
Normal file
296
frontend/src/pages/app/admin/WhatsAppAdminPage.tsx
Normal file
@@ -0,0 +1,296 @@
|
||||
// frontend/src/pages/app/admin/WhatsAppAdminPage.tsx
|
||||
/**
|
||||
* WhatsApp Admin Management Page
|
||||
* Admin-only interface for assigning WhatsApp phone numbers to tenants
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { MessageSquare, Phone, CheckCircle, AlertCircle, Loader2, Users } from 'lucide-react';
|
||||
import axios from 'axios';
|
||||
|
||||
interface PhoneNumberInfo {
|
||||
id: string;
|
||||
display_phone_number: string;
|
||||
verified_name: string;
|
||||
quality_rating: string;
|
||||
}
|
||||
|
||||
interface TenantWhatsAppStatus {
|
||||
tenant_id: string;
|
||||
tenant_name: string;
|
||||
whatsapp_enabled: boolean;
|
||||
phone_number_id: string | null;
|
||||
display_phone_number: string | null;
|
||||
}
|
||||
|
||||
const WhatsAppAdminPage: React.FC = () => {
|
||||
const [availablePhones, setAvailablePhones] = useState<PhoneNumberInfo[]>([]);
|
||||
const [tenants, setTenants] = useState<TenantWhatsAppStatus[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [assigningPhone, setAssigningPhone] = useState<string | null>(null);
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8001';
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Fetch available phone numbers
|
||||
const phonesResponse = await axios.get(`${API_BASE_URL}/api/v1/admin/whatsapp/phone-numbers`);
|
||||
setAvailablePhones(phonesResponse.data);
|
||||
|
||||
// Fetch tenant WhatsApp status
|
||||
const tenantsResponse = await axios.get(`${API_BASE_URL}/api/v1/admin/whatsapp/tenants`);
|
||||
setTenants(tenantsResponse.data);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || 'Failed to load WhatsApp data');
|
||||
console.error('Failed to fetch WhatsApp data:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const assignPhoneNumber = async (tenantId: string, phoneNumberId: string, displayPhone: string) => {
|
||||
setAssigningPhone(tenantId);
|
||||
|
||||
try {
|
||||
await axios.post(`${API_BASE_URL}/api/v1/admin/whatsapp/tenants/${tenantId}/assign-phone`, {
|
||||
phone_number_id: phoneNumberId,
|
||||
display_phone_number: displayPhone
|
||||
});
|
||||
|
||||
// Refresh data
|
||||
await fetchData();
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.detail || 'Failed to assign phone number');
|
||||
console.error('Failed to assign phone:', err);
|
||||
} finally {
|
||||
setAssigningPhone(null);
|
||||
}
|
||||
};
|
||||
|
||||
const unassignPhoneNumber = async (tenantId: string) => {
|
||||
if (!confirm('Are you sure you want to unassign this phone number?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAssigningPhone(tenantId);
|
||||
|
||||
try {
|
||||
await axios.delete(`${API_BASE_URL}/api/v1/admin/whatsapp/tenants/${tenantId}/unassign-phone`);
|
||||
|
||||
// Refresh data
|
||||
await fetchData();
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.detail || 'Failed to unassign phone number');
|
||||
console.error('Failed to unassign phone:', err);
|
||||
} finally {
|
||||
setAssigningPhone(null);
|
||||
}
|
||||
};
|
||||
|
||||
const getQualityRatingColor = (rating: string) => {
|
||||
switch (rating.toUpperCase()) {
|
||||
case 'GREEN':
|
||||
return 'text-green-600 bg-green-100';
|
||||
case 'YELLOW':
|
||||
return 'text-yellow-600 bg-yellow-100';
|
||||
case 'RED':
|
||||
return 'text-red-600 bg-red-100';
|
||||
default:
|
||||
return 'text-gray-600 bg-gray-100';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3">
|
||||
<MessageSquare className="w-8 h-8 text-blue-600" />
|
||||
WhatsApp Admin Management
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Assign WhatsApp phone numbers to bakery tenants
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-red-800">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Available Phone Numbers */}
|
||||
<div className="mb-8 bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div className="p-6 border-b border-gray-200 bg-gray-50">
|
||||
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Phone className="w-5 h-5" />
|
||||
Available Phone Numbers ({availablePhones.length})
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-gray-200">
|
||||
{availablePhones.length === 0 ? (
|
||||
<div className="p-6 text-center text-gray-500">
|
||||
<p>No phone numbers available. Please add phone numbers to your WhatsApp Business Account.</p>
|
||||
</div>
|
||||
) : (
|
||||
availablePhones.map((phone) => (
|
||||
<div key={phone.id} className="p-4 hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<Phone className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-mono font-semibold text-gray-900">{phone.display_phone_number}</p>
|
||||
<p className="text-sm text-gray-500">{phone.verified_name}</p>
|
||||
<p className="text-xs text-gray-400 mt-1">ID: {phone.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`px-3 py-1 rounded-full text-xs font-semibold ${getQualityRatingColor(phone.quality_rating)}`}>
|
||||
{phone.quality_rating}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tenants List */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div className="p-6 border-b border-gray-200 bg-gray-50">
|
||||
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
Bakery Tenants ({tenants.length})
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-gray-200">
|
||||
{tenants.length === 0 ? (
|
||||
<div className="p-6 text-center text-gray-500">
|
||||
<p>No tenants found.</p>
|
||||
</div>
|
||||
) : (
|
||||
tenants.map((tenant) => (
|
||||
<div key={tenant.tenant_id} className="p-4 hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="font-semibold text-gray-900">{tenant.tenant_name}</h3>
|
||||
{tenant.whatsapp_enabled && tenant.display_phone_number ? (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs font-medium">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-gray-100 text-gray-600 rounded-full text-xs font-medium">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
Not Configured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tenant.display_phone_number ? (
|
||||
<p className="text-sm text-gray-600 mt-1 font-mono">
|
||||
Phone: {tenant.display_phone_number}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
No phone number assigned
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{tenant.display_phone_number ? (
|
||||
<button
|
||||
onClick={() => unassignPhoneNumber(tenant.tenant_id)}
|
||||
disabled={assigningPhone === tenant.tenant_id}
|
||||
className="px-4 py-2 bg-red-100 text-red-700 rounded-lg hover:bg-red-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
{assigningPhone === tenant.tenant_id ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Unassigning...
|
||||
</>
|
||||
) : (
|
||||
'Unassign'
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
const phone = availablePhones.find(p => p.id === e.target.value);
|
||||
if (phone) {
|
||||
assignPhoneNumber(tenant.tenant_id, phone.id, phone.display_phone_number);
|
||||
}
|
||||
e.target.value = ''; // Reset select
|
||||
}
|
||||
}}
|
||||
disabled={assigningPhone === tenant.tenant_id || availablePhones.length === 0}
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value="">Assign phone number...</option>
|
||||
{availablePhones.map((phone) => (
|
||||
<option key={phone.id} value={phone.id}>
|
||||
{phone.display_phone_number} - {phone.verified_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{assigningPhone === tenant.tenant_id && (
|
||||
<Loader2 className="w-5 h-5 animate-spin text-blue-600" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Refresh Button */}
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
onClick={fetchData}
|
||||
disabled={loading}
|
||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 font-medium"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Refreshing...
|
||||
</>
|
||||
) : (
|
||||
'Refresh Data'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhatsAppAdminPage;
|
||||
Reference in New Issue
Block a user