ppt-tool/frontend/app/presentation-templates/modern/ChartOrTableWithMetricsDescription.tsx
Vadym Samoilenko cf21ba4516 Phase 1-2: Foundation + Admin Panel & Client Management
Phase 1 (Foundation):
- Project restructure (presenton-main → backend/ + frontend/)
- Database schema (8 new models, Alembic config, seed script)
- Auth (Azure AD SSO + dev bypass, JWT sessions, AuthMiddleware)
- RBAC (access_service, rbac_middleware, admin routers)
- Audit logging (fire-and-forget, AuditMiddleware, admin router)
- i18n (react-i18next with 5 namespace files)

Phase 2 (Admin Panel & Client Management):
- Admin panel shell (sidebar layout, role guard, 12 pages)
- Redux admin slice with 18 async thunks
- User management (role changes, deactivation)
- Client management (CRUD, brand config, team management)
- Brand config editor (colors, fonts, logos, voice rules)
- Master deck upload & parser (PPTX → HTML → React pipeline)
- Audit log viewer with filters and CSV/JSON export

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:37:17 +00:00

327 lines
13 KiB
TypeScript

import React from "react";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
import * as z from "zod";
export const layoutId = "chart-with-metrics";
export const layoutName = "Chart With Metrics Slide";
export const layoutDescription =
"A chart or table with metrics slide layout";
const growthStatsSchema = z
.object({
year: z.string(),
})
.catchall(z.number())
.meta({
description:
"Growth statistics for a specific year, with any number of metrics as key-value pairs where keys are metric names and values are numbers.",
});
// growthStats: list of dicts, each dict is { year: string, <metric1>: number, <metric2>: number, ... }
const tractionSchema = z.object({
title: z.string().default("Company Traction").meta({
description: "Main title of the slide",
}),
description: z
.string()
.min(3)
.max(200)
.default(
"Traction is a period where the company is feeling momentum during its development period. If traction momentum is not harnessed, sales figures can decline and the customer base can shrink. In general, companies will judge success by the amount of revenue and new customers they receive.",
)
.meta({
description:
"Main content text describing the company's traction and growth momentum.",
}),
tableMode: z.boolean().default(false),
tableColumns: z.array(z.string().min(1).max(40)).min(2).max(10).default(["Metric", "Value"]),
tableRows: z.array(z.array(z.string().min(0).max(200)).min(2).max(10)).min(1).max(30).default([["Users", "10K+"], ["Revenue", "$1.2M"], ["Satisfaction", "95%"]]),
// growthStats is a list of objects, each with a 'year' and any number of metric keys (all numbers)
growthStats: z
.array(growthStatsSchema)
.min(1)
.max(20)
.default([
growthStatsSchema.parse({
year: "2020",
artificialIntelligence: 5,
internetOfThings: 10,
others: 8,
}),
growthStatsSchema.parse({
year: "2021",
artificialIntelligence: 10,
internetOfThings: 20,
others: 15,
}),
growthStatsSchema.parse({
year: "2022",
artificialIntelligence: 20,
internetOfThings: 30,
others: 22,
}),
growthStatsSchema.parse({
year: "2023",
artificialIntelligence: 28,
internetOfThings: 38,
others: 29,
}),
growthStatsSchema.parse({
year: "2024",
artificialIntelligence: 35,
internetOfThings: 45,
others: 34,
}),
growthStatsSchema.parse({
year: "2025",
artificialIntelligence: 45,
internetOfThings: 53,
others: 42,
}),
growthStatsSchema.parse({
year: "2026",
artificialIntelligence: 55,
internetOfThings: 65,
others: 52,
}),
growthStatsSchema.parse({
year: "2029",
artificialIntelligence: 55,
internetOfThings: 65,
others: 52,
}),
])
.meta({
description:
"Growth statistics for the company, used for chart visualization. Each entry is an object representing a specific year, with the 'year' key as a string (e.g., '2020'), and additional keys for each metric (such as 'artificialIntelligence', 'internetOfThings', 'others'), where the values are numbers representing the metric's value for that year. Example:\n\n[\n { year: '2020', artificialIntelligence: 5, internetOfThings: 10, others: 8 },\n { year: '2021', artificialIntelligence: 10, internetOfThings: 20, others: 15 },\n ...\n]\nThis structure allows the chart to dynamically render multiple series over time, with each metric visualized as a separate line.",
}),
});
export const Schema = tractionSchema;
export type CompanyTractionData = z.infer<typeof tractionSchema>;
interface Props {
data?: Partial<CompanyTractionData>;
}
// Helper: assign colors to series
const defaultColors = [
"#1E4CD9",
"#3b82f6",
"#f59e0b",
"#10b981",
"#ef4444",
"#a21caf",
"#6366f1",
"#f43f5e",
"#fbbf24",
"#14b8a6",
];
function getSeriesKeys(
growthStats: Array<Record<string, string | number>>,
): string[] {
if (!growthStats.length) return [];
// Exclude 'year' or any non-numeric keys
const first = growthStats[0];
return Object.keys(first).filter(
(key) => key !== "year" && typeof first[key] === "number",
);
}
// Compute stats for right column, generic for all series
function computeStats(
growthStats: Array<Record<string, string | number>>,
seriesKeys: string[],
) {
if (!growthStats.length) return [];
const first = growthStats[0];
const last = growthStats[growthStats.length - 1];
return seriesKeys.map((key) => {
const start = typeof first[key] === "number" ? (first[key] as number) : 0;
const end = typeof last[key] === "number" ? (last[key] as number) : 0;
const growth = start === 0 ? 0 : ((end - start) / Math.abs(start)) * 100;
return {
label: key
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase()),
value: `${growth >= 0 ? "+" : ""}${Math.round(growth)}% growth`,
description: `${key
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase())} growth over the period.`,
};
});
}
const CompanyTractionSlideLayout: React.FC<Props> = ({ data }) => {
const growthStats = data?.growthStats || [];
// Dynamically determine series keys
const seriesKeys = getSeriesKeys(growthStats);
// Prepare stats for the right column, generic for all series
const stats = computeStats(growthStats, seriesKeys);
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div
className="w-full max-w-[1280px] max-h-[720px] aspect-video mx-auto rounded shadow-lg overflow-hidden relative z-20"
style={{
fontFamily: "var(--heading-font-family,Montserrat)",
backgroundColor: "var(--background-color, #FFFFFF)",
}}
>
{/* Header */}
{((data as any)?.__companyName__ || (data as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
{(data as any)?._logo_url__ && <img src={(data as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
{(data as any)?.__companyName__ && <span className="text-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(data as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="px-16 py-16 flex h-full gap-8">
{/* Left Column - Chart with Title Below */}
<div className="flex-1 pr-12 flex flex-col justify-center">
<h1 className="text-5xl font-bold mb-4 leading-tight text-left" style={{ color: 'var(--background-text, #234CD9)' }}>
{data?.title}
</h1>
<div className=" rounded-lg shadow p-4 mb-8"
style={{ backgroundColor: 'var(--card-color, #ffffff)' }}
>
<div className="w-full h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={growthStats} margin={{ top: 10, right: 20, left: 0, bottom: 10 }}>
<CartesianGrid strokeDasharray="3 3" stroke={`var(--background-text, #E5E7EB)`} />
<XAxis
dataKey="year"
stroke="var(--background-text, #234CD9)"
tick={{ fill: "var(--background-text, #234CD9)", fontSize: 12, fontWeight: 600 }}
/>
<YAxis
stroke="var(--background-text, #234CD9)"
tick={{ fill: "var(--background-text, #234CD9)", fontSize: 12, fontWeight: 600 }}
/>
<Tooltip
contentStyle={{
backgroundColor: "var(--card-color, #234CD9)",
border: "none",
color: "var(--background-text, #ffffff)",
}}
/>
<Legend
wrapperStyle={{ color: "var(--background-text, #234CD9)", fontSize: 12, fontWeight: 600 }}
iconType="circle"
/>
{seriesKeys.map((key, idx) => (
<Line
key={key}
type="monotone"
dataKey={key}
stroke={`var(--graph-${idx}, ${defaultColors[idx % defaultColors.length]})`}
strokeWidth={3}
name={key
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase())}
dot={{
r: 4,
fill: `var(--graph-${idx}, ${defaultColors[idx % defaultColors.length]})`,
}}
activeDot={{
r: 6,
fill: `var(--graph-${idx}, ${defaultColors[idx % defaultColors.length]})`,
}}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
</div>
</div>
{/* Right Column - Description and Stats or Table */}
<div className="flex flex-col items-start justify-center w-[52%] gap-8">
<p className="text-base leading-relaxed font-normal mb-6 max-w-xl text-left" style={{ color: 'var(--background-text, #234CD9)' }}>
{data?.description ||
"Traction is a period where the company is feeling momentum during its development period. If traction momentum is not harnessed, sales figures can decline and the customer base can shrink. In general, companies will judge success by the amount of revenue and new customers they receive."}
</p>
{data?.tableMode ? (
<div className="w-full">
<div className="rounded-lg ring-1" style={{ borderColor: 'var(--secondary-accent-color, rgba(0,0,0,0.08))' }}>
<table className="w-full border-separate border-spacing-0">
<thead>
<tr>
{data.tableColumns?.map((col, idx) => (
<th key={idx} className="text-left text-sm font-semibold px-4 py-3 border-b" style={{ borderColor: 'var(--stroke, rgba(0,0,0,0.12))', color: 'var(--primary-color, #1E4CD9)' }}>
{col}
</th>
))}
</tr>
</thead>
<tbody>
{data.tableRows?.map((row, rIdx) => (
<tr key={rIdx} className="align-top">
{row.map((cell, cIdx) => (
<td key={cIdx} className="text-sm px-4 py-3 border-t" style={{ borderColor: 'var(--stroke, rgba(0,0,0,0.08))', color: 'var(--background-text, #334155)' }}>
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
<div className="flex flex-row w-full gap-6">
{stats.map((stat, index) => (
<div
key={index}
className="flex-1 rounded-lg shadow-sm px-5 py-4 flex flex-col items-start"
style={{ backgroundColor: 'var(--primary-color, #F5F8FE)' }}
>
<div className="text-white text-xs font-semibold px-3 py-1 rounded-sm mb-2" style={{ backgroundColor: 'var(--card-color, #234CD9)', color: 'var(--background-text, #ffffff)' }}>
{stat.label}
</div>
<div className="text-2xl font-bold mb-1" style={{ color: 'var(--primary-text, #234CD9)' }}>
{stat.value}
</div>
<p className="text-sm leading-snug" style={{ color: 'var(--primary-text, #234CD9)' }}>
{stat.description}
</p>
</div>
))}
</div>
)}
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 h-1" style={{ backgroundColor: 'var(--primary-color, #1E4CD9)' }} />
</div>
</>
);
};
export default CompanyTractionSlideLayout;