import { useState, useEffect } from 'react';
import { fetchUsers, deleteCompetitorBranch } from '@/utils/apiClient';
import {
Search,
Edit,
Trash2,
Phone,
MapPin,
FileText,
ChevronDown,
ChevronUp,
Store,
Truck,
Map,
Plus,
X,
CheckCircle2,
AlertCircle
} from 'lucide-react';
import { Button } from '@astryxdesign/core/Button';
import { Card } from '@astryxdesign/core/Card';
import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog';
import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout';
import { FormLayout } from '@astryxdesign/core/FormLayout';
import { TextInput } from '@astryxdesign/core/TextInput';
import { TextArea } from '@astryxdesign/core/TextArea';
import { Selector } from '@astryxdesign/core/Selector';
import { AlertDialog } from '@astryxdesign/core/AlertDialog';
import { Text, Heading } from '@astryxdesign/core/Text';
import { Badge } from '@astryxdesign/core/Badge';
import { IconButton } from '@astryxdesign/core/IconButton';
import { Banner } from '@astryxdesign/core/Banner';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
import EmptyState from '@/components/EmptyState';
function isQuoted(row) {
return !!(row.rate_per_kg && !String(row.rate_per_kg).toLowerCase().includes('not answered'));
}
const API_BASE = import.meta.env.VITE_API_BASE || 'https://api.doormile.com/api/v1';
const getHeaders = () => {
const token = localStorage.getItem('auth_token');
return {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
};
};
function Pill({ label, color = 'neutral', truncate = false }) {
if (!label) return —;
const badgeStyles = {
success: { bg: '#E3F6EC', fg: '#00773B' },
warning: { bg: '#FFF7E0', fg: '#8A6500' },
info: { bg: '#E0F7F8', fg: '#00727B' },
error: { bg: '#FEEAE9', fg: '#A82216' },
primary: { bg: '#F8E0E3', fg: '#9E0E20' },
neutral: { bg: '#f1f5f9', fg: '#475569' }
};
const style = badgeStyles[color] || badgeStyles.neutral;
return (
{label}
);
}
function Field({ label, children }) {
return (
);
}
function SectionCard({ icon: Icon, title, children }) {
return (
);
}
async function apiFetchSurveys() {
const res = await fetch(`${API_BASE}/admin/competitor-branches?limit=1000`, { headers: getHeaders() });
if (!res.ok) throw new Error('Failed to fetch surveys');
const json = await res.json();
const data = Array.isArray(json) ? json : (json.data || []);
return { data, total: data.length };
}
async function apiSaveSurvey(data) {
const isUpdate = !!data.id;
const url = isUpdate ? `${API_BASE}/admin/competitor-branches/${data.id}` : `${API_BASE}/admin/competitor-branches`;
const method = isUpdate ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: getHeaders(),
body: JSON.stringify(data)
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || err.message || 'Failed to save survey');
}
return res.status === 204 ? {} : res.json().catch(() => ({}));
}
async function apiSavePricing(data) {
const url = `${API_BASE}/admin/carrier-pricing`;
const method = 'POST';
const res = await fetch(url, {
method,
headers: getHeaders(),
body: JSON.stringify({
company: data.company,
weight_slab: data.weight_slab,
zone: data.zone || '',
service_type: data.service_type || '',
rate: String(data.rate)
})
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || err.message || 'Failed to save pricing');
}
return res.status === 204 ? {} : res.json().catch(() => ({}));
}
const ZONES = ['Local / Same', 'Within Tamilnadu', 'Interstate India'];
const WEIGHT_SLABS = ['< 500g', '500g - 1kg', '1kg - 2kg', '2kg - 5kg', '> 5kg'];
function SurveyFormDialog({ open, onClose, onSave, initialData }) {
const [formData, setFormData] = useState({});
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (open) {
setFormData(initialData ? {
...initialData,
pincodes: initialData.pincodes ? initialData.pincodes.split(',').map(s => s.trim()).filter(Boolean) : []
} : {
company: '', area: '', phone: '', rate_per_kg: '',
offers_pickup: 'no', offers_drop: 'no', packing_charge: '',
time_in_days: '', plus_code: '', address: '', frequency: '',
pincodes: [],
slabs: []
});
setError(null);
}
}, [open, initialData]);
const set = (field) => (val) => {
setFormData(prev => ({ ...prev, [field]: val }));
};
const handleSlabChange = (index, field) => (val) => {
const newSlabs = [...(formData.slabs || [])];
newSlabs[index] = { ...newSlabs[index], [field]: val };
setFormData(prev => ({ ...prev, slabs: newSlabs }));
};
const addSlab = () => {
setFormData(prev => ({
...prev,
slabs: [...(prev.slabs || []), { weight_slab: '', zone: '', service_type: '', rate: '' }]
}));
};
const removeSlab = (index) => {
const newSlabs = [...(formData.slabs || [])];
newSlabs.splice(index, 1);
setFormData(prev => ({ ...prev, slabs: newSlabs }));
};
const handlePincodeChange = (index) => (val) => {
const newPincodes = [...(formData.pincodes || [])];
newPincodes[index] = val;
setFormData(prev => ({ ...prev, pincodes: newPincodes }));
};
const addPincode = () => {
setFormData(prev => ({
...prev,
pincodes: [...(prev.pincodes || []), '']
}));
};
const removePincode = (index) => {
const newPincodes = [...(formData.pincodes || [])];
newPincodes.splice(index, 1);
setFormData(prev => ({ ...prev, pincodes: newPincodes }));
};
const handleSave = async () => {
setSaving(true);
setError(null);
try {
if (formData.pincodes && formData.pincodes.length > 0) {
const invalidPincodes = formData.pincodes.filter(p => p.trim() && !/^\d{6}$/.test(p.trim()));
if (invalidPincodes.length > 0) {
throw new Error(`Invalid pincodes: ${invalidPincodes.join(', ')}. Must be 6 digits.`);
}
}
const payload = {
...(formData.id ? { id: formData.id } : {}),
company: formData.company || '',
area: formData.area || '',
phone: formData.phone || '',
rate_per_kg: formData.rate_per_kg || '',
offers_pickup: formData.offers_pickup || 'no',
offers_drop: formData.offers_drop || 'no',
packing_charge: formData.packing_charge || '',
time_in_days: formData.time_in_days || '',
plus_code: formData.plus_code || '',
address: formData.address || '',
frequency: formData.frequency || '',
pincodes: formData.pincodes ? formData.pincodes.join(',') : '',
};
await apiSaveSurvey(payload);
if (formData.slabs && formData.slabs.length > 0 && formData.company) {
try {
await Promise.all(formData.slabs.map(slab => {
if (slab.weight_slab && slab.rate) {
return apiSavePricing({
company: formData.company,
weight_slab: slab.weight_slab,
zone: slab.zone || '',
service_type: slab.service_type || '',
rate: slab.rate
});
}
return Promise.resolve();
}));
} catch (err) {
console.error("Failed to save pricing slabs", err);
}
}
onSave();
onClose();
} catch (err) {
console.error(err);
setError(err.message || 'Failed to save. Please try again.');
} finally {
setSaving(false);
}
};
const YES_NO = [{ value: 'yes', label: 'Yes' }, { value: 'no', label: 'No' }];
const FREQUENCY_OPTIONS = ['Daily', 'Weekly', 'Bi-weekly', 'Monthly', 'On-demand'];
const WEIGHT_SLAB_OPTIONS = WEIGHT_SLABS.map((s) => ({ value: s, label: s }));
const ZONE_OPTIONS = ZONES.map((z) => ({ value: z, label: z }));
return (
);
}
function BranchCard({ row, onEdit, onDelete, users }) {
const [open, setOpen] = useState(false);
const creator = users.find(u => u.id === row.created_by);
const updater = users.find(u => u.id === row.updated_by);
const quoted = isQuoted(row);
return (
setOpen(!open)}>
{/* Left: Location & Contact */}
{row.area || 'Unknown Area'}
{quoted ? (
) : (
)}
{/* Middle: Rates */}
Logistics
{row.offers_pickup === 'yes' || row.offers_drop === 'yes' ? `${row.offers_pickup === 'yes' ? 'Pickup' : ''} ${row.offers_drop === 'yes' ? 'Drop' : ''}` : '—'}
{/* Right: Actions */}
e.stopPropagation()}>
{open && (
{row.offers_pickup || '—'}
{row.offers_drop || '—'}
{row.packing_charge || '—'}
{row.time_in_days || '—'}
{row.company || '—'}
{row.frequency || '—'}
{row.phone || '—'}
{row.area || '—'}
{row.pincodes ? (
{row.pincodes.split(',').map((p, i) => (
{p.trim()}
))}
) : (
—
)}
{row.plus_code || '—'}
{row.address || '—'}
{(row.address || row.plus_code) && (
View on map
)}
{creator ? creator.first_name : (row.created_by ? `User ID: ${row.created_by}` : 'System')}
{updater ? updater.first_name : (row.updated_by ? `User ID: ${row.updated_by}` : '—')}
)}
);
}
function CompanyGroup({ companyName, branches, onEdit, onDelete, users, isLast }) {
const [open, setOpen] = useState(false);
const quotedCount = branches.filter(isQuoted).length;
const totalCount = branches.length;
const allQuoted = quotedCount === totalCount;
const noneQuoted = quotedCount === 0;
const quotedVariant = allQuoted ? 'success' : noneQuoted ? 'error' : 'warning';
return (
setOpen(!open)}
>
{(companyName || 'A')[0].toUpperCase()}
{companyName || 'Unknown Company'}
{branches.length} location{branches.length !== 1 && 's'}
:
}
label={`${quotedCount}/${totalCount} quoted`}
/>
:
}
/>
{open && (
{branches.map((branch, idx) => )}
)}
);
}
export default function Survey() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const [formOpen, setFormOpen] = useState(false);
const [editingRecord, setEditingRecord] = useState(null);
const [toDelete, setToDelete] = useState(null);
const [deleting, setDeleting] = useState(false);
const [deleteError, setDeleteError] = useState(null);
const [users, setUsers] = useState([]);
const loadData = () => {
let cancelled = false;
setLoading(true);
Promise.all([
apiFetchSurveys(),
fetchUsers().catch(() => [])
])
.then(([res, userRes]) => {
if (!cancelled) {
setData(res.data || res || []);
setUsers(userRes || []);
}
})
.catch(err => console.error("Error fetching data:", err))
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; };
};
useEffect(() => {
return loadData();
}, []);
const confirmDelete = async () => {
setDeleting(true);
setDeleteError(null);
try {
await deleteCompetitorBranch(toDelete.id);
setToDelete(null);
loadData();
} catch (e) {
setDeleteError(e.message || 'Failed to delete. Please try again.');
} finally {
setDeleting(false);
}
};
const filtered = data.filter(r =>
(r.company || '').toLowerCase().includes(search.toLowerCase()) ||
(r.area || '').toLowerCase().includes(search.toLowerCase()) ||
(r.phone || '').toLowerCase().includes(search.toLowerCase())
);
const groupedData = filtered.reduce((acc, row) => {
const comp = row.company || 'Unknown Company';
if (!acc[comp]) acc[comp] = [];
acc[comp].push(row);
return acc;
}, {});
const companyKeys = Object.keys(groupedData).sort();
const paginatedKeys = companyKeys.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const stats = {
total: data.length,
quoted: data.filter(d => d.rate_per_kg && !String(d.rate_per_kg).toLowerCase().includes('not answered')).length,
missing: data.filter(d => !d.rate_per_kg || String(d.rate_per_kg).toLowerCase().includes('not answered')).length,
};
return (
}
hasClear
value={search}
onChange={(v) => { setSearch(v); setPage(0); }}
/>
}
onClick={() => { setEditingRecord(null); setFormOpen(true); }}
label="Add New Survey"
style={{ flexShrink: 0 }}
/>
}
/>
{/* List Section */}
{loading ? (
{[0, 1, 2].map((i) => (
))}
) : paginatedKeys.length > 0 ? (
{paginatedKeys.map((companyName, idx) => (
{ setEditingRecord(row); setFormOpen(true); }}
onDelete={(row) => setToDelete(row)}
users={users}
isLast={idx === paginatedKeys.length - 1}
/>
))}
) : search ? (
{ setSearch(''); setPage(0); }} />
) : (
)}
{/* Pagination Section */}
Companies per page:
{companyKeys.length === 0 ? '0-0' : `${page * rowsPerPage + 1}-${Math.min((page + 1) * rowsPerPage, companyKeys.length)}`} of {companyKeys.length}
setPage((p) => Math.max(0, p - 1))}
isDisabled={page === 0}
/>
setPage((p) => Math.min(Math.ceil(companyKeys.length / rowsPerPage) - 1, p + 1))}
isDisabled={page >= Math.ceil(companyKeys.length / rowsPerPage) - 1}
/>
setFormOpen(false)}
onSave={() => loadData()}
initialData={editingRecord}
/>
{ if (!o) { setToDelete(null); setDeleteError(null); } }}
title="Delete survey record?"
description={
toDelete
? (deleteError || `This will permanently remove the record for ${toDelete.area || toDelete.company}. This cannot be undone.`)
: ''
}
actionLabel="Delete"
onAction={confirmDelete}
isActionLoading={deleting}
/>
);
}