feat: version 2.6.0 -- barra de progreso rediseñada + icono de compartido

Porcentaje de avance dentro de la barra (antes aparte), fondo gris
visible en vez de casi invisible sobre el tema oscuro. Nuevo icono en
el listado de Proyectos que muestra con quien esta compartido cada
proyecto y su permiso, solo si aplica.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-30 08:04:50 -05:00
parent fda83e9dff
commit 82308faeb1
10 changed files with 82 additions and 12 deletions
+10
View File
@@ -1,5 +1,15 @@
# Changelog - project.linkedcorp.net
## [2.6.0] - 2026-09-30
### Changed
- Listado de Proyectos: el porcentaje de avance ahora va **dentro** de la barra de progreso
(antes iba aparte, a la derecha) y el fondo de la barra pasa a un gris visible sobre el fondo
oscuro (antes casi no se notaba). Libera espacio horizontal en la fila.
### Added
- Ícono de "compartido" en el listado de Proyectos: aparece solo si el proyecto tiene otros
usuarios además del dueño. Al hacer clic muestra con quién está compartido y su permiso
(Lectura/Escritura).
## [2.5.0] - 2026-09-25
### Fixed
- **Bug de seguridad real**: cualquier usuario autenticado veía TODOS los proyectos del sistema
+1 -1
View File
@@ -1 +1 @@
2.5.0
2.6.0
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "project-linkedcorp-backend",
"version": "2.5.0",
"version": "2.6.0",
"private": true,
"type": "module",
"main": "src/server.js",
+2 -1
View File
@@ -8,7 +8,8 @@ const LIST_SELECT = `
p.requesting_department_id, rd.name AS requesting_department_name,
p.responsible_department_id, sd.name AS responsible_department_name,
p.start_date, p.estimated_end_date, p.actual_end_date, p.progress_pct,
p.risk_level, p.notes, p.created_by, p.created_at, p.updated_at, p.deleted_at, p.deleted_by
p.risk_level, p.notes, p.created_by, p.created_at, p.updated_at, p.deleted_at, p.deleted_by,
(SELECT COUNT(*) FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id <> p.owner_user_id) AS shared_count
FROM projects p
JOIN priorities pr ON pr.id = p.priority_id
JOIN project_statuses ps ON ps.id = p.status_id
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "project-linkedcorp-frontend",
"private": true,
"version": "2.5.0",
"version": "2.6.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -0,0 +1,50 @@
import { useState } from 'react';
import { Users } from 'lucide-react';
import { Flyout } from './Flyout.jsx';
import { Badge } from './Badge.jsx';
import { useUi } from '../context/ui-context.jsx';
import { getProject } from '../services/projects.js';
// Icono que solo aparece si el proyecto esta compartido (shared_count > 0, ver
// projects/repository.js#findAll) -- al abrir, carga los miembros (menos el dueno) una sola vez
// por click y muestra su permiso (Lectura/Escritura).
export function SharedWithIcon({ projectId, count }) {
const { t } = useUi();
const [members, setMembers] = useState(null);
const [loading, setLoading] = useState(false);
if (!count) return null;
async function loadMembers() {
if (members) return;
setLoading(true);
try {
const project = await getProject(projectId);
setMembers(project.members.filter((m) => m.role_in_project !== 'owner'));
} finally {
setLoading(false);
}
}
return (
<Flyout trigger={({ onClick }) => (
<button
className="icon-btn" title={t('project_shared_with')}
onClick={(e) => { e.stopPropagation(); loadMembers(); onClick(); }}
>
<Users size={16} />
</button>
)}>
<div style={{ padding: '4px 8px 8px', fontWeight: 700, fontSize: '.78rem' }}>{t('project_shared_with')}</div>
{loading && <div style={{ padding: '6px 10px', fontSize: '.78rem' }} className="text-muted">{t('common_loading')}</div>}
{!loading && members?.map((m) => (
<div key={m.user_id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px' }}>
<span style={{ flex: 1, fontSize: '.78rem' }}>{m.name}</span>
<Badge color={m.role_in_project === 'write' ? 'success' : 'muted'}>
{t(m.role_in_project === 'write' ? 'project_role_write' : 'project_role_read')}
</Badge>
</div>
))}
</Flyout>
);
}
+3 -5
View File
@@ -16,11 +16,9 @@ export function DueBadge({ status }) {
export function ProgressBar({ value }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div className="progress-bar" style={{ flex: 1 }}>
<div className="progress-bar-fill" style={{ width: `${value}%` }} />
</div>
<span style={{ fontSize: '.72rem', width: 36, textAlign: 'right' }}>{Math.round(value)}%</span>
<div className="progress-bar">
<div className="progress-bar-fill" style={{ width: `${value}%` }} />
<span className="progress-bar-label">{Math.round(value)}%</span>
</div>
);
}
+2
View File
@@ -163,6 +163,7 @@ export const messages = {
project_estimated_end_date: 'Fin estimado',
project_actual_end_date: 'Fin real',
project_progress: 'Avance',
project_shared_with: 'Compartido con',
project_requesting_department: 'Departamento solicitante',
project_responsible_department: 'Departamento responsable',
project_risk: 'Riesgo',
@@ -461,6 +462,7 @@ export const messages = {
project_estimated_end_date: 'Estimated end',
project_actual_end_date: 'Actual end',
project_progress: 'Progress',
project_shared_with: 'Shared with',
project_requesting_department: 'Requesting department',
project_responsible_department: 'Responsible department',
project_risk: 'Risk',
@@ -19,6 +19,7 @@ import { ComboBox } from '../../components/ComboBox.jsx';
import { StatusBadge, ProgressBar } from '../../components/StatusBadge.jsx';
import { Badge } from '../../components/Badge.jsx';
import { DetailPopup } from '../../components/DetailPopup.jsx';
import { SharedWithIcon } from '../../components/SharedWithIcon.jsx';
const CAN_DELETE_ROLES = ['superadmin', 'admin', 'it_manager', 'project_lead'];
@@ -168,7 +169,8 @@ export default function ProyectosPage() {
{ key: 'status_name', label: t('common_status'), render: (r) => <StatusBadge name={r.status_name} color={r.status_color} /> },
{ key: 'priority_name', label: t('common_priority'), sortValue: (r) => r.priority_level, render: (r) => <StatusBadge name={r.priority_name} color={r.priority_color} /> },
{ key: 'owner_name', label: t('project_owner') },
{ key: 'progress_pct', label: t('project_progress'), render: (r) => <div style={{ width: 100 }}><ProgressBar value={Number(r.progress_pct)} /></div> },
{ key: 'progress_pct', label: t('project_progress'), render: (r) => <div style={{ width: 90 }}><ProgressBar value={Number(r.progress_pct)} /></div> },
{ key: 'shared', label: '', sortable: false, render: (r) => <SharedWithIcon projectId={r.id} count={r.shared_count} /> },
{ key: 'risk_level', label: t('project_risk'), render: (r) => <Badge color={RISK_COLOR[r.risk_level]}>{t(`risk_${r.risk_level}`)}</Badge> },
{ key: 'updated_at', label: t('project_updated_at'), render: (r) => String(r.updated_at).slice(0, 16) },
...(showTrash ? [{ key: 'deleted_at', label: t('project_deleted_at'), render: (r) => String(r.deleted_at).slice(0, 10) }] : []),
+9 -2
View File
@@ -378,8 +378,15 @@ table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; }
.expander-desc { font-size: .72rem; color: rgb(var(--muted)); }
.expander-body { padding: 14px; border-top: 1px solid rgb(var(--line)); }
.progress-bar { height: 6px; border-radius: 999px; background: rgb(var(--field)); overflow: hidden; }
.progress-bar-fill { height: 100%; background: rgb(var(--accent)); }
.progress-bar {
position: relative; height: 18px; border-radius: 999px; overflow: hidden;
background: rgb(var(--muted) / .32);
}
.progress-bar-fill { height: 100%; background: rgb(var(--accent)); transition: width .2s ease; }
.progress-bar-label {
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
font-size: .66rem; font-weight: 700; color: #fff; text-shadow: 0 1px 2px rgb(0 0 0 / .55);
}
.text-muted { color: rgb(var(--muted)); }
.mt-0 { margin-top: 0; }