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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) }] : []),
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user