Improve client portal

This commit is contained in:
Arthur Belleville 2026-04-18 12:04:55 +02:00
parent e568b342ad
commit cc37bf2a78
No known key found for this signature in database
2 changed files with 30 additions and 159 deletions

View file

@ -102,9 +102,24 @@ describe("ClientTabloPage parity shell", () => {
expect(screen.getByText("Client Project")).toBeInTheDocument();
expect(screen.getAllByRole("button", { name: "Discussion" })).toHaveLength(2);
expect(screen.getByText("Rôle :")).toBeInTheDocument();
expect(screen.getByText("Créé le :")).toBeInTheDocument();
expect(screen.getByText("Progression :")).toBeInTheDocument();
expect(screen.getAllByText("Rôle").length).toBeGreaterThan(0);
expect(screen.getAllByText("Créé le").length).toBeGreaterThan(0);
expect(screen.getAllByText("Progression").length).toBeGreaterThan(0);
});
it("keeps the shared main-app header labels even when the client locale is english", () => {
renderWithProviders(<ClientTabloPage />, {
route: "/tablo/tablo-1",
path: "/tablo/:tabloId",
language: "en",
});
expect(screen.getAllByText("Rôle").length).toBeGreaterThan(0);
expect(screen.getAllByText("Créé le").length).toBeGreaterThan(0);
expect(screen.getAllByText("Progression").length).toBeGreaterThan(0);
expect(screen.queryByText("Role")).not.toBeInTheDocument();
expect(screen.queryByText("Created on")).not.toBeInTheDocument();
expect(screen.queryByText("Progress")).not.toBeInTheDocument();
});
it("keeps client restrictions by hiding invite and layout-edit controls", () => {

View file

@ -4,31 +4,15 @@ import { buildApi } from "@xtablo/shared";
import { useSession } from "@xtablo/shared/contexts/SessionContext";
import type { Etape, KanbanTask, TabloFolder, UserTablo } from "@xtablo/shared-types";
import {
CalendarIcon,
Compass,
Flame,
FolderIcon,
Gem,
Heart,
KanbanIcon,
LayoutDashboardIcon,
Leaf,
ListChecksIcon,
MapIcon,
MessageCircleIcon,
Sparkles,
Star,
Sun,
Waves,
Zap,
} from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import {
EtapesSection,
RoadmapSection,
TabloDetailsShell,
SingleTabloView,
type SingleTabloTabId,
TabloDiscussionSection,
TabloEventsSection,
TabloFilesSection,
@ -158,43 +142,6 @@ function useClientTabloFolders(tabloId: string, accessToken: string | undefined)
});
}
function getTabloIcon(color: string | null | undefined) {
switch (color) {
case "bg-blue-500":
return Zap;
case "bg-green-500":
return Leaf;
case "bg-purple-500":
return Gem;
case "bg-red-500":
return Flame;
case "bg-yellow-500":
return Star;
case "bg-indigo-500":
return Compass;
case "bg-pink-500":
return Heart;
case "bg-teal-500":
return Waves;
case "bg-orange-500":
return Sun;
case "bg-cyan-500":
return Sparkles;
default:
return FolderIcon;
}
}
function getTabloIconColor(color: string | null | undefined): string {
switch (color) {
case "bg-yellow-500":
case "bg-cyan-500":
return "text-gray-700";
default:
return "text-white";
}
}
function getStatusConfig(status: string) {
switch (status) {
case "in_progress":
@ -238,27 +185,12 @@ function getEtapeProgressStats(etapes: Etape[]) {
};
}
// ─── Tabs ─────────────────────────────────────────────────────────────────────
type TabId = "overview" | "etapes" | "tasks" | "files" | "discussion" | "events" | "roadmap";
const TABS: { id: TabId; label: string; icon: React.ElementType }[] = [
{ id: "overview", label: "Aperçu", icon: LayoutDashboardIcon },
{ id: "etapes", label: "Étapes", icon: ListChecksIcon },
{ id: "tasks", label: "Tâches", icon: KanbanIcon },
{ id: "files", label: "Fichiers", icon: FolderIcon },
{ id: "discussion", label: "Discussion", icon: MessageCircleIcon },
{ id: "events", label: "Événements", icon: CalendarIcon },
{ id: "roadmap", label: "Roadmap", icon: MapIcon },
];
// ─── Page ─────────────────────────────────────────────────────────────────────
export function ClientTabloPage() {
const { t } = useTranslation(["pages", "chat"]);
const { tabloId } = useParams<{ tabloId: string }>();
const { session } = useSession();
const [activeTab, setActiveTab] = useState<TabId>("overview");
const [activeTab, setActiveTab] = useState<SingleTabloTabId>("overview");
const accessToken = session?.access_token;
const currentUserId = session?.user.id ?? "";
@ -293,93 +225,17 @@ export function ClientTabloPage() {
const { label: statusLabel, badgeClass } = getStatusConfig(tablo.status);
const progress = getEtapeProgressStats(etapes);
const isDiscussionView = activeTab === "discussion";
const TabloIcon = getTabloIcon(tablo.color);
const iconColor = getTabloIconColor(tablo.color);
const metadata = [
{
key: "role",
label: t("pages:tablo.details.roleLabel"),
value: (
<span className="text-foreground font-medium">{t("pages:tablo.role.guest")}</span>
),
},
{
key: "created-at",
label: t("pages:tablo.details.createdAtLabel"),
value: (
<span className="text-foreground">
{new Intl.DateTimeFormat("fr-FR", {
year: "numeric",
month: "short",
day: "2-digit",
}).format(new Date(tablo.created_at))}
</span>
),
},
{
key: "status",
label: t("pages:tablo.details.statusLabel"),
value: <span className={cn("px-3 py-1 rounded-full text-xs font-medium", badgeClass)}>{statusLabel}</span>,
},
{
key: "progress",
label: t("pages:tablo.details.progressLabel"),
value: (
<>
<div className="relative w-24 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className="absolute inset-y-0 left-0 bg-blue-500/40"
style={{ width: `${progress.startedPercentage}%` }}
/>
<div
className="absolute inset-y-0 left-0 bg-green-500"
style={{ width: `${progress.donePercentage}%` }}
/>
</div>
<span className="text-foreground font-medium">{progress.donePercentage}%</span>
</>
),
},
];
const headerVisual = (
<div
className={cn(
"w-12 h-12 rounded-lg flex items-center justify-center shrink-0 overflow-hidden",
!tablo.image && (tablo.color || "bg-gray-400")
)}
>
{tablo.image ? (
<img src={tablo.image} alt={tablo.name} className="w-full h-full object-cover" />
) : (
<TabloIcon className={cn("w-6 h-6", iconColor)} />
)}
</div>
);
const headerActions = (
<button
type="button"
onClick={() => setActiveTab("discussion")}
className="bg-[#804EEC] hover:bg-[#6f3fd4] text-white font-medium py-2.5 px-4 rounded-lg flex items-center justify-center gap-2 transition-colors flex-1 sm:flex-none min-h-[44px]"
>
<MessageCircleIcon className="w-5 h-5" />
{t("chat:discussionTitle")}
</button>
);
return (
<TabloDetailsShell
<SingleTabloView
tablo={tablo}
headerVisual={headerVisual}
headerActions={headerActions}
metadata={metadata}
tabs={TABS}
roleLabel="Invité"
statusLabel={statusLabel}
statusBadgeClass={badgeClass}
progress={progress}
activeTab={activeTab}
onTabChange={(tabId) => setActiveTab(tabId as TabId)}
isDiscussionView={isDiscussionView}
onTabChange={setActiveTab}
discussionAction={{ kind: "button", onClick: () => setActiveTab("discussion") }}
>
{activeTab === "overview" && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
@ -464,7 +320,7 @@ export function ClientTabloPage() {
</div>
<div className="flex justify-between">
<dt className="text-muted-foreground">Rôle</dt>
<dd className="font-medium text-foreground">{t("pages:tablo.role.guest")}</dd>
<dd className="font-medium text-foreground">Invité</dd>
</div>
</dl>
</div>
@ -540,6 +396,6 @@ export function ClientTabloPage() {
onTaskStatusChange={() => {}}
/>
)}
</TabloDetailsShell>
</SingleTabloView>
);
}