Improve backend

This commit is contained in:
Urtzi Alfaro
2025-11-18 07:17:17 +01:00
parent d36f2ab9af
commit 5c45164c8e
61 changed files with 9846 additions and 495 deletions

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