Finalize CRM module: bookings, pricing, survey with full UI integration and validation
This commit is contained in:
@@ -33,62 +33,11 @@ import StatusChip from '@/components/StatusChip';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import TabLabelCount from '@/components/TabLabelCount';
|
||||
import { fetchPoints, deletePoint, COLLECTIONS } from '@/utils/qdrant';
|
||||
import { fetchClients, deleteClient } from '@/utils/apiClient';
|
||||
import { toClient } from '@/utils/mappers';
|
||||
import { titleCase } from '@/utils/format';
|
||||
import ClientFormDialog from './ClientFormDialog';
|
||||
|
||||
const generateLogicalId = (id) => {
|
||||
const str = String(id).replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
||||
return 'CLI-' + str.substring(0, 6).padStart(6, '0');
|
||||
};
|
||||
|
||||
// Map a raw Qdrant point from doormile_clients to a flat client row.
|
||||
function toClient(point) {
|
||||
const p = point.payload || {};
|
||||
|
||||
// Qdrant point.id is usually a UUID.
|
||||
// We generate a clean CLI-XXXXXX format based on it.
|
||||
const logicalId = generateLogicalId(point.id);
|
||||
|
||||
return {
|
||||
id: point.id,
|
||||
logicalId,
|
||||
// Force override the ugly payload client_timestamp string with the clean ID
|
||||
clientId: logicalId,
|
||||
name: p.name || '—',
|
||||
phone: p.phone || '',
|
||||
city: p.city || '',
|
||||
businessState: p.businessState || '',
|
||||
businessType: p.businessType || '',
|
||||
status: p.status || 'unknown',
|
||||
parcelVolume: p.parcelVolume ?? 0,
|
||||
activeContracts: p.activeContracts ?? 0,
|
||||
frequency: p.frequency || '',
|
||||
provider: p.provider || '',
|
||||
efficiency: p.efficiency || '',
|
||||
logisticsSegment: p.logisticsSegment || '',
|
||||
transitFrom: p.transitFrom || '',
|
||||
transitTo: p.transitTo || '',
|
||||
neighbourhood: p.neighbourhood || p.surveyZone || '',
|
||||
surveyAddress: p.surveyAddress || '',
|
||||
surveyLat: p.surveyLat ?? p.surveyGeo?.lat ?? '',
|
||||
surveyLng: p.surveyLng ?? p.surveyGeo?.lon ?? '',
|
||||
dataConsent: p.dataConsent || '',
|
||||
lastUpdated: p.lastUpdated || '',
|
||||
notes: p.notes || ''
|
||||
};
|
||||
}
|
||||
|
||||
// Humanize raw payload tokens like `basicOnly`, `newClient`, `first_mile` → "Basic Only".
|
||||
const humanize = (s) =>
|
||||
String(s || '')
|
||||
.replace(/[_-]+/g, ' ')
|
||||
.replace(/([a-z\d])([A-Z])/g, '$1 $2')
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
.trim();
|
||||
|
||||
const titleCase = humanize;
|
||||
|
||||
// Map categorical enum values to a semantic palette color.
|
||||
const consentTone = (v) => ({ full: 'success', basiconly: 'info', none: 'default' }[String(v || '').toLowerCase()] || 'default');
|
||||
const efficiencyTone = (v) => {
|
||||
@@ -421,16 +370,28 @@ export default function Tenants() {
|
||||
const [toDelete, setToDelete] = useState(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const load = () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
fetchPoints(COLLECTIONS.clients)
|
||||
const load = (silent = false) => {
|
||||
if (!silent) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
}
|
||||
fetchClients()
|
||||
.then((points) => setClients(points.map(toClient)))
|
||||
.catch((e) => setError(e.message || 'Failed to load clients'))
|
||||
.finally(() => setLoading(false));
|
||||
.catch((e) => {
|
||||
if (!silent) setError(e.message || 'Failed to load clients');
|
||||
})
|
||||
.finally(() => {
|
||||
if (!silent) setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
useEffect(() => {
|
||||
load();
|
||||
const intervalId = setInterval(() => {
|
||||
load(true); // Silent poll every 10 seconds
|
||||
}, 10000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
const stats = useMemo(() => ({
|
||||
total: clients.length,
|
||||
@@ -473,7 +434,7 @@ export default function Tenants() {
|
||||
const confirmDelete = async () => {
|
||||
setDeleting(true);
|
||||
try {
|
||||
await deletePoint(COLLECTIONS.clients, toDelete.id);
|
||||
await deleteClient(toDelete.id);
|
||||
setToDelete(null);
|
||||
load();
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user