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
@@ -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; }