Improve client portal
This commit is contained in:
parent
e568b342ad
commit
cc37bf2a78
2 changed files with 30 additions and 159 deletions
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue