Rapport_React/src/views/dashboard/widgets/MilestoneTeamLeaderboard.jsx

180 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from 'react';
import { Link } from 'react-router';
import { Spinner } from 'react-bootstrap';
import { signedGet, isSuccess } from '@/api/client';
import { asArray, rankRoleId, rankRoleName, rankRolePillLabel, userInitials } from '@/views/dashboard/helpers';
import EmptyChart from '@/views/dashboard/EmptyChart';
function podiumUsersFromRankRole(rankRole) {
return asArray(rankRole?.users)
.slice(0, 3)
.map((row) => {
const info = row.user || {};
const name = String(info.name || '').trim();
return {
name,
initials: userInitials(name),
avatar: info.avatar || '',
score: Number.isFinite(Number(row.score)) ? Number(row.score) : 0,
};
});
}
function Podium({ users }) {
const slots = [users[1] || null, users[0] || null, users[2] || null];
const ranks = [2, 1, 3];
return (
<div className="mlb-podium-row">
{slots.map((user, index) => {
const rank = ranks[index];
return (
<div key={rank} className={`mlb-col ${rank === 1 ? 'is-first' : ''} ${user ? '' : 'is-empty'}`}>
{user ? (
<>
<div className="mlb-avatar-wrap">
{rank === 1 ? (
<span className="mlb-crown" aria-hidden="true">
<svg width="26" height="18" viewBox="0 0 26 18" fill="none">
<path d="M3 15.5L1 5l6.2 4L13 1l5.8 8L25 5l-2 10.5H3z" fill="#F0C94A" />
<path d="M4 15.5h18v2H4v-2z" fill="#E3B341" />
</svg>
</span>
) : null}
<div className={`mlb-avatar rank-${rank}`}>
{user.avatar && !String(user.avatar).includes('ui-avatars.com') ? (
<img src={user.avatar} alt="" />
) : (
user.initials
)}
</div>
<span className={`mlb-badge rank-${rank}`}>{rank}</span>
</div>
<div className="mlb-name" title={user.name}>
{user.name}
</div>
<div className={`mlb-bar rank-${rank}`}>{user.score}</div>
</>
) : (
<>
<div className="mlb-name">-</div>
<div className={`mlb-bar rank-${rank}`}>-</div>
</>
)}
</div>
);
})}
</div>
);
}
function LeaderboardCard({ item }) {
const [activeIndex, setActiveIndex] = useState(0);
const pills = item.pills || [];
const active = pills[activeIndex] || pills[0];
const users = active?.podium_users || item.podium_users || [];
const detailsUrl = active?.url || item.details_url || '#';
return (
<div className="col-6">
<article className="mlb-card">
<div className="mlb-card-head">
<h3 className="mlb-card-title">{item.title}</h3>
<div className="mlb-card-meta">
<p className="mlb-card-date">
<span>{item.interval_label || '—'}</span>
</p>
<Link className="mlb-card-details" to={detailsUrl}>
View details
</Link>
</div>
</div>
<div className="mlb-podium">
<Podium users={users} />
</div>
{pills.length ? (
<div className="mlb-pills">
{pills.map((pill, index) => (
<button
key={pill.label}
type="button"
className={`mlb-pill ${index === activeIndex ? 'is-active' : ''}`}
onClick={() => setActiveIndex(index)}
>
{pill.label}
</button>
))}
</div>
) : null}
</article>
</div>
);
}
export default function MilestoneTeamLeaderboard({ widget }) {
const [items, setItems] = useState(null);
useEffect(() => {
const load = async () => {
const response = await signedGet('reports', 'roles_leaderboard/list', {}, { timeout: 120000 });
if (!isSuccess(response)) {
setItems([]);
return;
}
let leaderboards = response.data;
if (leaderboards?.items) leaderboards = leaderboards.items;
leaderboards = asArray(leaderboards);
const mapped = leaderboards.map((board) => {
const rankRoles = asArray(board.rank_roles);
const pills = rankRoles.map((rankRole, index) => {
const limit = Number(rankRole.limit) > 0 ? Number(rankRole.limit) : 10;
const roleId = rankRoleId(rankRole);
return {
label: rankRolePillLabel(rankRole.title, limit, rankRoleName(rankRole)),
url: `/leaderboard/dashboard?id=${encodeURIComponent(board.id || '')}&role=${encodeURIComponent(roleId)}`,
podium_users: podiumUsersFromRankRole(rankRole),
active: index === 0,
};
});
const start = board.validity_start || '';
const end = board.validity_end || '';
return {
id: board.id,
title: board.name || widget.title || 'Leaderboard',
interval_label: `${start}${start && end ? ' – ' : ''}${end}`.trim(),
details_url: pills[0]?.url || `/leaderboard/dashboard?id=${encodeURIComponent(board.id || '')}`,
podium_users: pills[0]?.podium_users || [],
pills,
};
});
setItems(mapped);
};
load();
}, [widget.title]);
return (
<div className="mb-3">
<div className="row">
<div className="col-12">
<div className="page-title-box">
<h4 className="page-title">{widget.title || 'Leaderboard'}</h4>
</div>
</div>
</div>
{items === null ? (
<div className="text-center py-4">
<Spinner animation="border" />
<p className="text-muted mt-2 mb-0">Loading leaderboards...</p>
</div>
) : items.length === 0 ? (
<EmptyChart type="radialBar" label="No data" />
) : (
<div className="row">
{items.map((item) => (
<LeaderboardCard key={item.id} item={item} />
))}
</div>
)}
</div>
);
}