feat: filtros de Estado, Prioridad y proximos a vencer en Proyectos
El backend ya soportaba filtrar por statusId/priorityId; solo faltaba exponerlo en la UI. "Proximos a vencer" es client-side, ventana de 7 dias (misma definicion que el KPI del dashboard). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Changelog - project.linkedcorp.net
|
# Changelog - project.linkedcorp.net
|
||||||
|
|
||||||
|
## [1.11.0] - 2026-09-11
|
||||||
|
### Added
|
||||||
|
- Filtros en el listado de Proyectos: Estado, Prioridad (ambos ya soportados por el backend, solo
|
||||||
|
faltaba exponerlos en la UI) y "Solo próximos a vencer" (checkbox, ventana de 7 días — misma
|
||||||
|
definición que el KPI del dashboard).
|
||||||
|
|
||||||
## [1.10.0] - 2026-09-11
|
## [1.10.0] - 2026-09-11
|
||||||
### Added
|
### Added
|
||||||
- Ordenar proyectos por Prioridad ahora usa la severidad real (Baja→Media→Alta→Crítica), no el
|
- Ordenar proyectos por Prioridad ahora usa la severidad real (Baja→Media→Alta→Crítica), no el
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-backend",
|
"name": "project-linkedcorp-backend",
|
||||||
"version": "1.10.0",
|
"version": "1.11.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-frontend",
|
"name": "project-linkedcorp-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.10.0",
|
"version": "1.11.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ export const messages = {
|
|||||||
project_invite_sent: 'Invitacion enviada por correo',
|
project_invite_sent: 'Invitacion enviada por correo',
|
||||||
project_created_at: 'Creado',
|
project_created_at: 'Creado',
|
||||||
project_deleted_at: 'Eliminado',
|
project_deleted_at: 'Eliminado',
|
||||||
|
project_filter_due_soon: 'Solo próximos a vencer (7 días)',
|
||||||
project_delete_confirm_msg: '¿Seguro que deseas eliminar este proyecto? Se movera a la papelera y dejara de verse en el listado; un superadministrador puede restaurarlo o borrarlo definitivamente.',
|
project_delete_confirm_msg: '¿Seguro que deseas eliminar este proyecto? Se movera a la papelera y dejara de verse en el listado; un superadministrador puede restaurarlo o borrarlo definitivamente.',
|
||||||
project_trash: 'Papelera',
|
project_trash: 'Papelera',
|
||||||
project_trash_title: 'Papelera de proyectos',
|
project_trash_title: 'Papelera de proyectos',
|
||||||
@@ -346,6 +347,7 @@ export const messages = {
|
|||||||
project_invite_sent: 'Invite sent by email',
|
project_invite_sent: 'Invite sent by email',
|
||||||
project_created_at: 'Created',
|
project_created_at: 'Created',
|
||||||
project_deleted_at: 'Deleted',
|
project_deleted_at: 'Deleted',
|
||||||
|
project_filter_due_soon: 'Due soon only (7 days)',
|
||||||
project_delete_confirm_msg: 'Are you sure you want to delete this project? It will move to the trash and disappear from the list; a superadmin can restore it or delete it permanently.',
|
project_delete_confirm_msg: 'Are you sure you want to delete this project? It will move to the trash and disappear from the list; a superadmin can restore it or delete it permanently.',
|
||||||
project_trash: 'Trash',
|
project_trash: 'Trash',
|
||||||
project_trash_title: 'Project trash',
|
project_trash_title: 'Project trash',
|
||||||
|
|||||||
@@ -40,7 +40,14 @@ export default function ProyectosPage() {
|
|||||||
const isSuperadmin = currentUser?.roleCode === 'superadmin';
|
const isSuperadmin = currentUser?.roleCode === 'superadmin';
|
||||||
const canDelete = CAN_DELETE_ROLES.includes(currentUser?.roleCode);
|
const canDelete = CAN_DELETE_ROLES.includes(currentUser?.roleCode);
|
||||||
|
|
||||||
const { data: projects, reload } = useAsync(() => listProjects(showTrash ? { trash: true } : {}), [showTrash]);
|
const [filterStatusId, setFilterStatusId] = useState('');
|
||||||
|
const [filterPriorityId, setFilterPriorityId] = useState('');
|
||||||
|
const [filterDueSoon, setFilterDueSoon] = useState(false);
|
||||||
|
|
||||||
|
const { data: projects, reload } = useAsync(
|
||||||
|
() => listProjects(showTrash ? { trash: true } : { statusId: filterStatusId, priorityId: filterPriorityId }),
|
||||||
|
[showTrash, filterStatusId, filterPriorityId]
|
||||||
|
);
|
||||||
const { data: priorities } = useAsync(() => listCatalog('priorities'), []);
|
const { data: priorities } = useAsync(() => listCatalog('priorities'), []);
|
||||||
const { data: statuses } = useAsync(() => listCatalog('project-statuses'), []);
|
const { data: statuses } = useAsync(() => listCatalog('project-statuses'), []);
|
||||||
const { data: types } = useAsync(() => listCatalog('project-types'), []);
|
const { data: types } = useAsync(() => listCatalog('project-types'), []);
|
||||||
@@ -133,6 +140,14 @@ export default function ProyectosPage() {
|
|||||||
|
|
||||||
if (!projects) return <div className="text-muted">{t('common_loading')}</div>;
|
if (!projects) return <div className="text-muted">{t('common_loading')}</div>;
|
||||||
|
|
||||||
|
const visibleProjects = filterDueSoon && !showTrash
|
||||||
|
? projects.filter((p) => {
|
||||||
|
if (p.status_is_closed || !p.estimated_end_date) return false;
|
||||||
|
const diffDays = Math.round((new Date(p.estimated_end_date) - new Date()) / 86400000);
|
||||||
|
return diffDays >= 0 && diffDays <= 7;
|
||||||
|
})
|
||||||
|
: projects;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
@@ -155,11 +170,25 @@ export default function ProyectosPage() {
|
|||||||
|
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
<SearchInput value={search} onChange={setSearch} />
|
<SearchInput value={search} onChange={setSearch} />
|
||||||
|
{!showTrash && (
|
||||||
|
<>
|
||||||
|
<div style={{ width: 160 }}>
|
||||||
|
<ComboBox options={[{ value: '', label: t('common_all') }, ...statusOptions]} value={filterStatusId} onChange={setFilterStatusId} placeholder={t('common_status')} />
|
||||||
|
</div>
|
||||||
|
<div style={{ width: 160 }}>
|
||||||
|
<ComboBox options={[{ value: '', label: t('common_all') }, ...priorityOptions]} value={filterPriorityId} onChange={setFilterPriorityId} placeholder={t('common_priority')} />
|
||||||
|
</div>
|
||||||
|
<label className="checkbox-row">
|
||||||
|
<input type="checkbox" checked={filterDueSoon} onChange={(e) => setFilterDueSoon(e.target.checked)} />
|
||||||
|
{t('project_filter_due_soon')}
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rows={projects}
|
rows={visibleProjects}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
getSearchHaystack={(p) => `${p.code} ${p.name} ${p.owner_name} ${p.status_name}`}
|
getSearchHaystack={(p) => `${p.code} ${p.name} ${p.owner_name} ${p.status_name}`}
|
||||||
getRowKey={(p) => p.id}
|
getRowKey={(p) => p.id}
|
||||||
|
|||||||
Reference in New Issue
Block a user