82 lines
5.9 KiB
TypeScript
82 lines
5.9 KiB
TypeScript
import { SearchableSelect } from '../components/SearchableSelect';
|
||
import { useEffect, useState } from 'react';
|
||
import type { FormEvent } from 'react';
|
||
import { Link, useSearchParams } from 'react-router';
|
||
import { PermissionGate } from '../auth/PermissionGate';
|
||
import { Alert, EmptyState, LoadingBlock, errorMessage } from '../components/Feedback';
|
||
import { Icon } from '../components/Icon';
|
||
import {
|
||
SURVEY_CAMPAIGN_STATUSES,
|
||
surveyCampaignStatusLabel,
|
||
surveyStatusClass,
|
||
} from '../features/surveys/surveyPresentation';
|
||
import { listSurveyCampaigns } from '../lib/api';
|
||
import type {
|
||
PageMeta,
|
||
SurveyCampaignListItem,
|
||
SurveyCampaignStatus,
|
||
} from '../lib/api';
|
||
import { formatDate } from '../lib/format';
|
||
|
||
export function SurveyCampaignsPage() {
|
||
const [urlParams, setUrlParams] = useSearchParams();
|
||
const [campaigns, setCampaigns] = useState<SurveyCampaignListItem[]>([]);
|
||
const [meta, setMeta] = useState<PageMeta>({ page: 1, pageSize: 25, total: 0, totalPages: 0 });
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState('');
|
||
const [draftSearch, setDraftSearch] = useState(urlParams.get('search') ?? '');
|
||
const search = urlParams.get('search') ?? '';
|
||
const rawStatus = urlParams.get('status') ?? '';
|
||
const status = SURVEY_CAMPAIGN_STATUSES.some((item) => item.value === rawStatus)
|
||
? rawStatus as SurveyCampaignStatus
|
||
: '';
|
||
const page = Math.max(1, Number(urlParams.get('page') ?? 1) || 1);
|
||
|
||
useEffect(() => {
|
||
setLoading(true);
|
||
setError('');
|
||
listSurveyCampaigns({ page, pageSize: 25, search, status })
|
||
.then((response) => {
|
||
setCampaigns(response.data);
|
||
setMeta(response.meta);
|
||
})
|
||
.catch((requestError) => setError(errorMessage(requestError)))
|
||
.finally(() => setLoading(false));
|
||
}, [page, search, status]);
|
||
|
||
const updateFilter = (key: string, value: string) => {
|
||
const next = new URLSearchParams(urlParams);
|
||
value ? next.set(key, value) : next.delete(key);
|
||
next.delete('page');
|
||
setUrlParams(next);
|
||
};
|
||
|
||
const applySearch = (event: FormEvent) => {
|
||
event.preventDefault();
|
||
updateFilter('search', draftSearch.trim());
|
||
};
|
||
|
||
const setPage = (value: number) => {
|
||
const next = new URLSearchParams(urlParams);
|
||
value > 1 ? next.set('page', String(value)) : next.delete('page');
|
||
setUrlParams(next);
|
||
};
|
||
|
||
return <section>
|
||
<div className="page-heading">
|
||
<div><span className="eyebrow">OPERACIÓN DE CAMPO</span><h1>Relevamientos</h1><p>Campañas planificadas sobre registros existentes de los inventarios.</p></div>
|
||
<PermissionGate permission="surveys.manage"><Link className="button primary" to="/relevamiento/nuevo"><Icon name="plus" />Nueva campaña</Link></PermissionGate>
|
||
</div>
|
||
|
||
<div className="temporal-notice"><Icon name="clipboard" /><p><strong>Inventarios organizados por empresa.</strong> Las campañas organizan qué registros relevar, quién los atiende y su avance; no crean un inventario paralelo.</p></div>
|
||
|
||
<form className="toolbar survey-toolbar" onSubmit={applySearch}>
|
||
<label className="search-field"><Icon name="search" /><input value={draftSearch} onChange={(event) => setDraftSearch(event.target.value)} placeholder="Buscar por código o nombre" /><button>Buscar</button></label>
|
||
<label className="select-field"><span>Estado</span><SearchableSelect value={status} onChange={(event) => updateFilter('status', event.target.value)}><option value="">Todos</option>{SURVEY_CAMPAIGN_STATUSES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</SearchableSelect></label>
|
||
</form>
|
||
|
||
{error && <Alert>{error}</Alert>}
|
||
{loading ? <LoadingBlock label="Cargando campañas…" /> : campaigns.length === 0 ? <EmptyState title="Sin campañas" text="Creá una campaña para planificar el próximo relevamiento de los inventarios." /> : <div className="table-panel"><div className="table-summary"><strong>{meta.total} campaña{meta.total === 1 ? '' : 's'}</strong><span>Página {page} de {Math.max(meta.totalPages, 1)}</span></div><div className="table-scroll"><table><thead><tr><th>Campaña</th><th>Estado</th><th>Alcance</th><th>Coordinación</th><th>Fechas previstas</th><th>Avance</th><th /></tr></thead><tbody>{campaigns.map((campaign) => <tr key={campaign.id}><td><div className="asset-cell"><span className="asset-symbol"><Icon name="clipboard" size={17} /></span><div><strong>{campaign.name}</strong><small>{campaign.code}</small></div></div></td><td><span className={`status-badge ${surveyStatusClass(campaign.status)}`}>{surveyCampaignStatusLabel(campaign.status)}</span></td><td>{campaign.scopeAsset ? <Link className="text-link" to={`/inventarios/${campaign.scopeAsset.id}`}>{campaign.scopeAsset.name}</Link> : <span className="muted">Todos los inventarios</span>}</td><td>{campaign.coordinator ? `${campaign.coordinator.firstName} ${campaign.coordinator.lastName}` : 'Sin asignar'}</td><td><span className="survey-date-range">{formatDate(campaign.plannedStartAt)}<small>hasta {formatDate(campaign.plannedEndAt)}</small></span></td><td><div className="survey-progress"><strong>{campaign.completedCount + campaign.skippedCount}/{campaign.targetCount}</strong><span><i style={{ width: `${campaign.targetCount ? ((campaign.completedCount + campaign.skippedCount) / campaign.targetCount) * 100 : 0}%` }} /></span></div></td><td className="action-cell"><Link className="icon-button" to={`/relevamiento/${campaign.id}`} aria-label={`Abrir ${campaign.name}`}><Icon name="chevron" /></Link></td></tr>)}</tbody></table></div><div className="pagination"><button className="button secondary" disabled={page <= 1} onClick={() => setPage(page - 1)}>Anterior</button><span>{meta.total ? `${(page - 1) * meta.pageSize + 1}–${Math.min(page * meta.pageSize, meta.total)} de ${meta.total}` : '0 resultados'}</span><button className="button secondary" disabled={page >= meta.totalPages} onClick={() => setPage(page + 1)}>Siguiente</button></div></div>}
|
||
</section>;
|
||
}
|