feat: add shared single tablo view
This commit is contained in:
parent
fc82ea1949
commit
37ab73bced
7 changed files with 585 additions and 1 deletions
|
|
@ -1 +1 @@
|
|||
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/components/clientlayout.tsx","./src/lib/supabase.ts","./src/pages/authcallback.tsx","./src/pages/clienttablolistpage.tsx","./src/pages/clienttablopage.tsx"],"version":"5.9.3"}
|
||||
{"root":["./src/app.tsx","./src/i18n.test.ts","./src/i18n.ts","./src/main.tsx","./src/maincss.test.ts","./src/routes.tsx","./src/setuptests.ts","./src/components/clientlayout.test.tsx","./src/components/clientlayout.tsx","./src/lib/supabase.ts","./src/pages/authcallback.tsx","./src/pages/clienttablolistpage.tsx","./src/pages/clienttablopage.test.tsx","./src/pages/clienttablopage.tsx","./src/test/testhelpers.test.tsx","./src/test/testhelpers.tsx"],"version":"5.9.3"}
|
||||
|
|
@ -7,6 +7,16 @@ export { RoadmapSection } from "./RoadmapSection";
|
|||
export { TabloHeaderActions } from "./TabloHeaderActions";
|
||||
export { ChatMessages } from "./ChatMessages";
|
||||
export { TabloDetailsShell } from "./TabloDetailsShell";
|
||||
export { SingleTabloOverview } from "./single-tablo/SingleTabloOverview";
|
||||
export { SingleTabloView } from "./single-tablo/SingleTabloView";
|
||||
export {
|
||||
DEFAULT_OVERVIEW_LAYOUT,
|
||||
sanitizeOverviewLayout,
|
||||
} from "./single-tablo/overviewLayout";
|
||||
export type { OverviewBlockId, OverviewLayoutV1 } from "./single-tablo/overviewLayout";
|
||||
export { moveBetweenZones, moveWithinZone } from "./single-tablo/overviewReorder";
|
||||
export { getSingleTabloStatusConfig } from "./single-tablo/shared";
|
||||
export type { SingleTabloTabId } from "./single-tablo/shared";
|
||||
|
||||
// Sub-components
|
||||
export { GanttChart } from "./components/gantt/GanttChart";
|
||||
|
|
|
|||
262
packages/tablo-views/src/single-tablo/SingleTabloOverview.tsx
Normal file
262
packages/tablo-views/src/single-tablo/SingleTabloOverview.tsx
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
import { cn } from "@xtablo/shared";
|
||||
import type { KanbanTask } from "@xtablo/shared-types";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { FileIcon, GripVerticalIcon, LayoutPanelTopIcon, ListChecksIcon, RotateCcwIcon } from "lucide-react";
|
||||
import type { OverviewBlockId, OverviewLayoutV1 } from "./overviewLayout";
|
||||
|
||||
interface DraggedBlock {
|
||||
zone: "left" | "right";
|
||||
index: number;
|
||||
}
|
||||
|
||||
interface SingleTabloOverviewProps {
|
||||
roleLabel?: string;
|
||||
statusLabel?: string;
|
||||
statusBadgeClass?: string;
|
||||
description: string;
|
||||
tasks: KanbanTask[];
|
||||
projectTaskCount: number;
|
||||
personalTaskCount: number;
|
||||
fileNames: string[];
|
||||
showFileMenu?: boolean;
|
||||
onOpenTasks: () => void;
|
||||
onOpenFiles: () => void;
|
||||
onCreateTask: () => void;
|
||||
onToggleTaskDone: (taskId: string) => void;
|
||||
showAllTasks: boolean;
|
||||
onToggleShowAllTasks: () => void;
|
||||
layout: OverviewLayoutV1;
|
||||
canEditLayout: boolean;
|
||||
isLayoutEditMode: boolean;
|
||||
draggedBlock: DraggedBlock | null;
|
||||
onToggleLayoutEditMode: () => void;
|
||||
onResetLayout: () => void;
|
||||
onBlockDragStart: (
|
||||
event: React.DragEvent<HTMLDivElement>,
|
||||
zone: "left" | "right",
|
||||
index: number
|
||||
) => void;
|
||||
onBlockDragOver: (event: React.DragEvent<HTMLDivElement>) => void;
|
||||
onBlockDrop: (zone: "left" | "right", index: number) => void;
|
||||
onBlockDragEnd: () => void;
|
||||
}
|
||||
|
||||
function OverviewCard({
|
||||
title,
|
||||
actions,
|
||||
children,
|
||||
draggable = false,
|
||||
onDragStart,
|
||||
onDragOver,
|
||||
onDrop,
|
||||
onDragEnd,
|
||||
isDragged = false,
|
||||
}: {
|
||||
title: string;
|
||||
actions?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
draggable?: boolean;
|
||||
onDragStart?: (event: React.DragEvent<HTMLDivElement>) => void;
|
||||
onDragOver?: (event: React.DragEvent<HTMLDivElement>) => void;
|
||||
onDrop?: (event: React.DragEvent<HTMLDivElement>) => void;
|
||||
onDragEnd?: () => void;
|
||||
isDragged?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
draggable={draggable}
|
||||
onDragStart={onDragStart}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onDragEnd={onDragEnd}
|
||||
className={cn(
|
||||
"bg-white dark:bg-card rounded-xl border border-border p-6 shadow-sm",
|
||||
draggable && "cursor-grab active:cursor-grabbing",
|
||||
isDragged && "opacity-60"
|
||||
)}
|
||||
>
|
||||
<div className="mb-4 flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{draggable && <GripVerticalIcon className="h-4 w-4 text-muted-foreground" />}
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">{title}</h2>
|
||||
</div>
|
||||
{actions}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SingleTabloOverview({
|
||||
description,
|
||||
tasks,
|
||||
projectTaskCount,
|
||||
personalTaskCount,
|
||||
fileNames,
|
||||
onOpenTasks,
|
||||
onOpenFiles,
|
||||
onCreateTask,
|
||||
onToggleTaskDone,
|
||||
showAllTasks,
|
||||
onToggleShowAllTasks,
|
||||
layout,
|
||||
canEditLayout,
|
||||
isLayoutEditMode,
|
||||
draggedBlock,
|
||||
onToggleLayoutEditMode,
|
||||
onResetLayout,
|
||||
onBlockDragStart,
|
||||
onBlockDragOver,
|
||||
onBlockDrop,
|
||||
onBlockDragEnd,
|
||||
}: SingleTabloOverviewProps) {
|
||||
const renderBlockContent = (blockId: OverviewBlockId) => {
|
||||
switch (blockId) {
|
||||
case "description":
|
||||
return {
|
||||
title: "Description du projet",
|
||||
children: (
|
||||
<p className="text-muted-foreground leading-relaxed text-sm sm:text-base">
|
||||
{description}
|
||||
</p>
|
||||
),
|
||||
};
|
||||
case "myTasks":
|
||||
return {
|
||||
title: "Mes tâches",
|
||||
actions: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="button" variant="ghost" size="sm" onClick={onOpenTasks}>
|
||||
Voir tout
|
||||
</Button>
|
||||
<Button type="button" size="sm" onClick={onCreateTask}>
|
||||
Ajouter
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
children: (
|
||||
<div className="space-y-3">
|
||||
{tasks.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Aucune tâche.</p>
|
||||
) : (
|
||||
tasks.map((task) => (
|
||||
<button
|
||||
key={task.id}
|
||||
type="button"
|
||||
onClick={() => onToggleTaskDone(task.id)}
|
||||
className="flex w-full items-center gap-3 rounded-lg border border-border px-3 py-2 text-left"
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"h-4 w-4 rounded-full border border-border",
|
||||
task.status === "done" && "bg-green-500 border-green-500"
|
||||
)}
|
||||
/>
|
||||
<span className="truncate text-sm font-medium text-foreground">
|
||||
{task.title}
|
||||
</span>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
{personalTaskCount > 5 && (
|
||||
<Button type="button" variant="link" className="px-0" onClick={onToggleShowAllTasks}>
|
||||
{showAllTasks ? "Afficher moins" : "Afficher plus"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
case "files":
|
||||
return {
|
||||
title: "Fichiers",
|
||||
actions: (
|
||||
<Button type="button" variant="ghost" size="sm" onClick={onOpenFiles}>
|
||||
Ouvrir
|
||||
</Button>
|
||||
),
|
||||
children: (
|
||||
<div className="space-y-3">
|
||||
{fileNames.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Aucun fichier.</p>
|
||||
) : (
|
||||
fileNames.slice(0, 5).map((fileName) => (
|
||||
<div key={fileName} className="flex items-center gap-2 text-sm text-foreground">
|
||||
<FileIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="truncate">{fileName}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
case "info":
|
||||
return {
|
||||
title: "Informations",
|
||||
children: (
|
||||
<div className="space-y-3 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<ListChecksIcon className="h-4 w-4" />
|
||||
<span>{projectTaskCount} tâches sur le projet</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutPanelTopIcon className="h-4 w-4" />
|
||||
<span>{personalTaskCount} tâches pour vous</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const renderZone = (zone: "left" | "right", blocks: OverviewBlockId[]) => (
|
||||
<div className="space-y-6">
|
||||
{blocks.map((blockId, index) => {
|
||||
const block = renderBlockContent(blockId);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={blockId}
|
||||
onDragOver={onBlockDragOver}
|
||||
onDrop={() => onBlockDrop(zone, index)}
|
||||
>
|
||||
<OverviewCard
|
||||
title={block.title}
|
||||
actions={block.actions}
|
||||
draggable={isLayoutEditMode}
|
||||
onDragStart={(event) => onBlockDragStart(event, zone, index)}
|
||||
onDragOver={onBlockDragOver}
|
||||
onDrop={() => onBlockDrop(zone, index)}
|
||||
onDragEnd={onBlockDragEnd}
|
||||
isDragged={draggedBlock?.zone === zone && draggedBlock.index === index}
|
||||
>
|
||||
{block.children}
|
||||
</OverviewCard>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{canEditLayout && (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button type="button" variant={isLayoutEditMode ? "secondary" : "outline"} onClick={onToggleLayoutEditMode}>
|
||||
Modifier la mise en page
|
||||
</Button>
|
||||
{isLayoutEditMode && (
|
||||
<Button type="button" variant="ghost" onClick={onResetLayout}>
|
||||
<RotateCcwIcon className="h-4 w-4" />
|
||||
Réinitialiser
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<div className="lg:col-span-2">{renderZone("left", layout.leftZone)}</div>
|
||||
<div>{renderZone("right", layout.rightZone)}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
169
packages/tablo-views/src/single-tablo/SingleTabloView.tsx
Normal file
169
packages/tablo-views/src/single-tablo/SingleTabloView.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { cn } from "@xtablo/shared";
|
||||
import type { UserTablo } from "@xtablo/shared-types";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import {
|
||||
CalendarIcon,
|
||||
FolderIcon,
|
||||
KanbanIcon,
|
||||
LayoutDashboardIcon,
|
||||
ListChecksIcon,
|
||||
MapIcon,
|
||||
MessageCircleIcon,
|
||||
} from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
TabloDetailsShell,
|
||||
type TabloDetailsShellMetadataItem,
|
||||
type TabloDetailsShellTab,
|
||||
} from "../TabloDetailsShell";
|
||||
import type { SingleTabloTabId } from "./shared";
|
||||
|
||||
const TABS: TabloDetailsShellTab[] = [
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
type DiscussionAction =
|
||||
| {
|
||||
kind: "link";
|
||||
to: string;
|
||||
}
|
||||
| {
|
||||
kind: "button";
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
interface SingleTabloViewProps {
|
||||
tablo: Pick<UserTablo, "name" | "created_at" | "status">;
|
||||
roleLabel: string;
|
||||
statusLabel: string;
|
||||
statusBadgeClass: string;
|
||||
progress: {
|
||||
startedPercentage: number;
|
||||
donePercentage: number;
|
||||
};
|
||||
activeTab: SingleTabloTabId;
|
||||
onTabChange: (tabId: SingleTabloTabId) => void;
|
||||
hasUnreadDiscussion?: boolean;
|
||||
discussionAction?: DiscussionAction;
|
||||
canInviteMembers?: boolean;
|
||||
onOpenInviteDialog?: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SingleTabloView({
|
||||
tablo,
|
||||
roleLabel,
|
||||
statusLabel,
|
||||
statusBadgeClass,
|
||||
progress,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
hasUnreadDiscussion = false,
|
||||
discussionAction,
|
||||
canInviteMembers = false,
|
||||
onOpenInviteDialog,
|
||||
children,
|
||||
}: SingleTabloViewProps) {
|
||||
const metadata: TabloDetailsShellMetadataItem[] = [
|
||||
{
|
||||
key: "role",
|
||||
label: "Rôle",
|
||||
value: <span className="text-foreground font-medium">{roleLabel}</span>,
|
||||
},
|
||||
{
|
||||
key: "created-at",
|
||||
label: "Créé le",
|
||||
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: "Statut",
|
||||
value: (
|
||||
<span className={cn("px-3 py-1 rounded-full text-xs font-medium", statusBadgeClass)}>
|
||||
{statusLabel}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "progress",
|
||||
label: "Progression",
|
||||
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 tabs = TABS.map((tab) =>
|
||||
tab.id === "discussion" ? { ...tab, hasUnread: hasUnreadDiscussion } : tab
|
||||
);
|
||||
|
||||
const discussionButton =
|
||||
discussionAction?.kind === "link" ? (
|
||||
<Button asChild>
|
||||
<Link to={discussionAction.to}>
|
||||
<MessageCircleIcon className="w-4 h-4" />
|
||||
Discussion
|
||||
</Link>
|
||||
</Button>
|
||||
) : discussionAction?.kind === "button" ? (
|
||||
<Button type="button" onClick={discussionAction.onClick}>
|
||||
<MessageCircleIcon className="w-4 h-4" />
|
||||
Discussion
|
||||
</Button>
|
||||
) : null;
|
||||
|
||||
const inviteButton =
|
||||
canInviteMembers && onOpenInviteDialog ? (
|
||||
<Button type="button" variant="outline" onClick={onOpenInviteDialog}>
|
||||
Inviter
|
||||
</Button>
|
||||
) : null;
|
||||
|
||||
const headerActions =
|
||||
discussionButton || inviteButton ? (
|
||||
<>
|
||||
{discussionButton}
|
||||
{inviteButton}
|
||||
</>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<TabloDetailsShell
|
||||
tablo={tablo}
|
||||
metadata={metadata}
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={(tabId) => onTabChange(tabId as SingleTabloTabId)}
|
||||
isDiscussionView={activeTab === "discussion"}
|
||||
headerActions={headerActions}
|
||||
>
|
||||
{children}
|
||||
</TabloDetailsShell>
|
||||
);
|
||||
}
|
||||
66
packages/tablo-views/src/single-tablo/overviewLayout.ts
Normal file
66
packages/tablo-views/src/single-tablo/overviewLayout.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
type OverviewLayoutVersion = 1;
|
||||
|
||||
export type OverviewBlockId = "description" | "myTasks" | "files" | "info";
|
||||
|
||||
export type OverviewLayoutV1 = {
|
||||
version: OverviewLayoutVersion;
|
||||
leftZone: OverviewBlockId[];
|
||||
rightZone: OverviewBlockId[];
|
||||
updatedAt?: string;
|
||||
updatedBy?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_LEFT_ZONE: OverviewBlockId[] = ["description", "myTasks"];
|
||||
const DEFAULT_RIGHT_ZONE: OverviewBlockId[] = ["files", "info"];
|
||||
const ALL_BLOCK_IDS: OverviewBlockId[] = ["description", "myTasks", "files", "info"];
|
||||
|
||||
export const DEFAULT_OVERVIEW_LAYOUT: OverviewLayoutV1 = {
|
||||
version: 1,
|
||||
leftZone: DEFAULT_LEFT_ZONE,
|
||||
rightZone: DEFAULT_RIGHT_ZONE,
|
||||
};
|
||||
|
||||
function unique<T>(items: T[]): T[] {
|
||||
return [...new Set(items)];
|
||||
}
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function parseZone(value: unknown): OverviewBlockId[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const valid = value.filter(
|
||||
(item): item is OverviewBlockId =>
|
||||
item === "description" || item === "myTasks" || item === "files" || item === "info"
|
||||
);
|
||||
return unique(valid);
|
||||
}
|
||||
|
||||
export function sanitizeOverviewLayout(input: unknown): OverviewLayoutV1 {
|
||||
if (!isObject(input)) {
|
||||
return DEFAULT_OVERVIEW_LAYOUT;
|
||||
}
|
||||
|
||||
const leftZoneInput = parseZone(input.leftZone);
|
||||
const rightZoneInput = parseZone(input.rightZone);
|
||||
|
||||
const leftZoneSize = leftZoneInput.length;
|
||||
const allBlocks = unique([...leftZoneInput, ...rightZoneInput]);
|
||||
|
||||
for (const blockId of ALL_BLOCK_IDS) {
|
||||
if (!allBlocks.includes(blockId)) {
|
||||
allBlocks.push(blockId);
|
||||
}
|
||||
}
|
||||
|
||||
const safeLeftZoneSize = Math.min(leftZoneSize, allBlocks.length);
|
||||
const leftZone = allBlocks.slice(0, safeLeftZoneSize);
|
||||
const rightZone = allBlocks.slice(safeLeftZoneSize);
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
leftZone,
|
||||
rightZone,
|
||||
};
|
||||
}
|
||||
46
packages/tablo-views/src/single-tablo/overviewReorder.ts
Normal file
46
packages/tablo-views/src/single-tablo/overviewReorder.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
export function moveWithinZone<T>(items: T[], fromIndex: number, toIndex: number): T[] {
|
||||
if (fromIndex < 0 || fromIndex >= items.length || toIndex < 0 || toIndex > items.length) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const next = [...items];
|
||||
const [moved] = next.splice(fromIndex, 1);
|
||||
const insertionIndex = Math.min(toIndex, next.length);
|
||||
next.splice(insertionIndex, 0, moved);
|
||||
|
||||
if (next.every((item, index) => item === items[index])) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
export function moveBetweenZones<T>(
|
||||
sourceItems: T[],
|
||||
targetItems: T[],
|
||||
sourceIndex: number,
|
||||
targetIndex: number
|
||||
): {
|
||||
sourceItems: T[];
|
||||
targetItems: T[];
|
||||
} {
|
||||
if (
|
||||
sourceIndex < 0 ||
|
||||
sourceIndex >= sourceItems.length ||
|
||||
targetIndex < 0 ||
|
||||
targetIndex > targetItems.length
|
||||
) {
|
||||
return { sourceItems, targetItems };
|
||||
}
|
||||
|
||||
const nextSourceItems = [...sourceItems];
|
||||
const [moved] = nextSourceItems.splice(sourceIndex, 1);
|
||||
const nextTargetItems = [...targetItems];
|
||||
const insertionIndex = Math.min(targetIndex, nextTargetItems.length);
|
||||
nextTargetItems.splice(insertionIndex, 0, moved);
|
||||
|
||||
return {
|
||||
sourceItems: nextSourceItems,
|
||||
targetItems: nextTargetItems,
|
||||
};
|
||||
}
|
||||
31
packages/tablo-views/src/single-tablo/shared.tsx
Normal file
31
packages/tablo-views/src/single-tablo/shared.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
export type SingleTabloTabId =
|
||||
| "overview"
|
||||
| "etapes"
|
||||
| "tasks"
|
||||
| "files"
|
||||
| "discussion"
|
||||
| "events"
|
||||
| "roadmap";
|
||||
|
||||
export function getSingleTabloStatusConfig(status: string) {
|
||||
switch (status) {
|
||||
case "in_progress":
|
||||
return {
|
||||
label: "En cours",
|
||||
badgeClass:
|
||||
"bg-yellow-50 text-yellow-700 border border-yellow-200 dark:bg-yellow-950/30 dark:text-yellow-400 dark:border-yellow-800",
|
||||
};
|
||||
case "done":
|
||||
return {
|
||||
label: "Terminé",
|
||||
badgeClass:
|
||||
"bg-green-50 text-green-600 border border-green-200 dark:bg-green-950/30 dark:text-green-400 dark:border-green-800",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: "À faire",
|
||||
badgeClass:
|
||||
"bg-blue-50 text-blue-600 border border-blue-200 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-800",
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue