UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: уплотнение workspace home и интерактивный Гант

This commit is contained in:
DCCONSTRUCTIONS
2026-04-24 00:21:19 +03:00
parent 4c436a949e
commit e5036fc95b
6 changed files with 1328 additions and 437 deletions
@@ -4,37 +4,105 @@
* See the LICENSE file for details.
*/
import { SlidersHorizontal } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { useHome } from "@/hooks/store/use-home";
import { useWorkspace } from "@/hooks/store/use-workspace";
import type { IUser, TActivityEntityData, TProjectAnalyticsCount } from "@plane/types";
import { useCurrentTime } from "@/hooks/use-current-time";
import { getActivityProjectId, getCompletionRate, type THomeProjectData } from "@/components/home/home.utils";
export function HomePageHeader() {
const { t } = useTranslation();
const { toggleWidgetSettings } = useHome();
const { currentWorkspace } = useWorkspace();
type HomePageHeaderProps = {
currentUser?: IUser;
selectedProject?: THomeProjectData;
selectedProjectAnalytics?: TProjectAnalyticsCount;
recents?: TActivityEntityData[];
};
export function HomePageHeader(props: HomePageHeaderProps) {
const { currentUser, selectedProject, selectedProjectAnalytics, recents } = props;
const { currentLocale } = useTranslation();
const { currentTime } = useCurrentTime();
const timeString = new Intl.DateTimeFormat(currentLocale, {
timeZone: currentUser?.user_timezone,
hour12: false,
hour: "2-digit",
minute: "2-digit",
}).format(currentTime);
const dateString = new Intl.DateTimeFormat(currentLocale, {
weekday: "long",
day: "numeric",
month: "long",
}).format(currentTime);
const heroDateLabel = dateString.toLocaleUpperCase(currentLocale || "ru-RU");
const totalIssues = selectedProjectAnalytics?.total_issues ?? 0;
const completedIssues = selectedProjectAnalytics?.completed_issues ?? 0;
const openIssues = Math.max(totalIssues - completedIssues, 0);
const completionRate = getCompletionRate(selectedProjectAnalytics);
const recentTouchpoints = (recents ?? []).filter((activity) => {
if (!selectedProject) return true;
return getActivityProjectId(activity) === selectedProject.id;
}).length;
const marketMetrics = [
{ label: "Готовность", value: `${completionRate}%`, caption: `${completedIssues}/${totalIssues || 0}` },
{ label: "Открытые задачи", value: openIssues.toString(), caption: "в работе" },
{ label: "Касания 7 дней", value: recentTouchpoints.toString(), caption: "recent" },
];
return (
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex min-w-0 flex-col gap-1">
<div className="inline-flex w-fit items-center gap-2 rounded-full bg-white/6 px-3 py-1.5 text-[11px] font-semibold tracking-[0.22em] text-placeholder uppercase">
<span>Workspace Home</span>
</div>
<div className="text-13 text-secondary">
{currentWorkspace?.name ? `Стартовый экран для ${currentWorkspace.name}` : "Главная страница workspace"}
</div>
<section className="nodedc-home-hero">
<div className="nodedc-home-hero-time">
<div className="text-[11px] font-semibold tracking-[0.16em] text-placeholder uppercase">{heroDateLabel}</div>
<div className="text-13 font-semibold text-primary">{timeString}</div>
</div>
<Button
variant="secondary"
size="lg"
className="nodedc-toolbar-pill"
prependIcon={<SlidersHorizontal className="size-4" />}
onClick={() => toggleWidgetSettings(true)}
>
{t("home.manage_widgets")}
</Button>
</div>
<div className="nodedc-home-hero-grid">
<div className="nodedc-home-hero-title-cell">
<h1>WORKSPACE HOME</h1>
<p>
{selectedProject
? `${selectedProject.identifier} в фокусе домашней сводки.`
: "Выберите проект для фокуса, Ганта и рабочей аналитики."}
</p>
</div>
<div className="nodedc-home-market-band">
<div className="min-w-0">
<div className="text-12 font-semibold text-black/[0.58]">Фокус</div>
<div className="mt-1 flex min-w-0 items-center gap-3">
<div className="min-w-0">
<div className="truncate text-24 leading-none font-semibold text-black">
{selectedProject?.name ?? "Workspace"}
</div>
<div className="mt-1 truncate text-12 font-medium text-black/[0.54]">
{selectedProject?.description || selectedProject?.identifier || "Координационный обзор"}
</div>
</div>
</div>
</div>
<div className="grid min-w-0 flex-1 gap-3 sm:grid-cols-3">
{marketMetrics.map((metric) => (
<div key={metric.label} className="min-w-0">
<div className="truncate text-12 font-medium text-black/[0.58]">{metric.label}</div>
<div className="mt-1 text-[26px] leading-none font-semibold text-black">{metric.value}</div>
<div className="mt-2 h-2 overflow-hidden rounded-full bg-black/[0.18]">
<div
className="h-full rounded-full bg-black"
style={{
width:
metric.label === "Готовность"
? `${Math.min(completionRate, 100)}%`
: `${Math.min(Number(metric.value) * 8 + 18, 100)}%`,
}}
/>
</div>
<div className="mt-1 text-11 text-black/[0.48]">{metric.caption}</div>
</div>
))}
</div>
</div>
</div>
</section>
);
}