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:
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "project-linkedcorp-backend",
|
||||
"version": "2.5.0",
|
||||
"version": "2.6.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/server.js",
|
||||
|
||||
@@ -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,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>
|
||||
);
|
||||
}
|
||||
@@ -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