fix the customChartBar in dashboard and process-innvation, also fix the style in dashboard and ecosystem's popup

This commit is contained in:
Saeed AB 2025-09-23 15:41:27 +03:30
parent 1a0cf20319
commit 585e66570d
9 changed files with 175 additions and 75 deletions

View File

@ -130,18 +130,18 @@ export function DashboardHome() {
let incCapacityTotal = 0;
const chartRows = rows.map((r) => {
const rel = r?.related_company ?? "-";
const preFee = Number(r?.pre_innovation_fee_sum ?? 0) > 0 ? r?.pre_innovation_fee_sum : 0;
const costRed = Number(r?.innovation_cost_reduction_sum ?? 0) > 0 ? r?.innovation_cost_reduction_sum : 0;
const preCap = Number(r?.pre_project_production_capacity_sum ?? 0) > 0 ? r?.pre_project_production_capacity_sum : 0;
const incCap = Number(r?.increased_capacity_after_innovation_sum ?? 0) > 0 ? r?.increased_capacity_after_innovation_sum : 0;
const preInc = Number(r?.pre_project_income_sum ?? 0) > 0 ? r?.pre_project_income_sum : 0;
const incInc = Number(r?.increased_income_after_innovation_sum ?? 0) > 0 ? r?.increased_income_after_innovation_sum : 0;
const preFee = Number(r?.pre_innovation_fee_sum ?? 0) >= 0 ? r?.pre_innovation_fee_sum : 0;
const costRed = Number(r?.innovation_cost_reduction_sum ?? 0) >= 0 ? r?.innovation_cost_reduction_sum : 0;
const preCap = Number(r?.pre_project_production_capacity_sum ?? 0) >= 0 ? r?.pre_project_production_capacity_sum : 0;
const incCap = Number(r?.increased_capacity_after_innovation_sum ?? 0) >= 0 ? r?.increased_capacity_after_innovation_sum : 0;
const preInc = Number(r?.pre_project_income_sum ?? 0) >= 0 ? r?.pre_project_income_sum : 0;
const incInc = Number(r?.increased_income_after_innovation_sum ?? 0) >= 0 ? r?.increased_income_after_innovation_sum : 0;
incCapacityTotal += incCap;
const capacityPct = preCap > 0 ? (incCap / preCap) * 100 : 0;
const revenuePct = preInc > 0 ? (incInc / preInc) * 100 : 0;
const costPct = preFee > 0 ? (costRed / preFee) * 100 : 0;
const capacityPct = preCap >= 0 ? (incCap / preCap) * 100 : 0;
const revenuePct = preInc >= 0 ? (incInc / preInc) * 100 : 0;
const costPct = preFee >= 0 ? (costRed / preFee) * 100 : 0;
return {
category: rel,
capacity: isFinite(capacityPct) ? capacityPct : 0,
@ -178,7 +178,7 @@ export function DashboardHome() {
dashboardData.topData.ongoing_innovation_technology_ideas || "0",
);
const percentage =
registered > 0 ? Math.round((ongoing / registered) * 100) : 0;
registered > 0 ? (ongoing / registered) * 100 : 0;
return [
{ browser: "safari", visitors: percentage, fill: "var(--color-safari)" },
@ -461,7 +461,7 @@ export function DashboardHome() {
<MetricCard
title="افزایش درآمد مبتنی بر فناوری و نوآوری"
value={dashboardData.topData?.technology_innovation_based_revenue_growth || "0"}
percentValue={Math.round(dashboardData.topData?.technology_innovation_based_revenue_growth_percent) || "0"}
percentValue={dashboardData.topData?.technology_innovation_based_revenue_growth_percent}
percentLabel="درصد به کل درآمد"
/>
@ -469,7 +469,7 @@ export function DashboardHome() {
<MetricCard
title="کاهش هزینه ها مبتنی بر فناوری و نوآوری"
value={Math.round(parseFloat(dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(/,/g, "") || "0") / 1000000)}
percentValue={Math.round(dashboardData.topData?.technology_innovation_based_cost_reduction_percent) || "0"}
percentValue={dashboardData.topData?.technology_innovation_based_cost_reduction_percent || "0"}
percentLabel="درصد به کل هزینه"
/>
@ -646,17 +646,10 @@ export function DashboardHome() {
<CardTitle className="text-white text-sm min-w-[100px]">
شدت فناوری
</CardTitle>
<p className="text-base text-left">
%
{formatNumber(
Math.round(
dashboardData.leftData?.technology_intensity || 0,
),
)}
</p>
<Progress
value={parseFloat(
dashboardData.leftData?.technology_intensity || "0",
dashboardData.leftData?.technology_intensity,
)}
className="h-4 flex-1"
/>

View File

@ -260,7 +260,6 @@ export function ProcessInnovationPage() {
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
});
console.log(JSON.parse(response.data));
if (response.state === 0) {
const dataString = response.data;
if (dataString && typeof dataString === "string") {
@ -656,6 +655,12 @@ export function ProcessInnovationPage() {
</div>
{/* Process Impacts Chart */}
{/* نمودار با الگوریتم Nice Numbers:
مثلاً اگر دادهها [10, 35, 63, 18] باشند:
- حداکثر: 63، با حاشیه 5% = 66.15
- Nice Max: 75 (گرد و خوانا)
- Ticks: [0, 20, 40, 60, 75]
این باعث میشود نمودار زیباتر و خواناتر باشد */}
<BaseCard className="rounded-2xl w-full overflow-hidden">
<CustomBarChart
title="تاثیرات فرآیندی به صورت درصد مقایسه ای"

View File

@ -61,6 +61,7 @@ interface ProjectData {
project_id: string;
title: string;
project_status: string;
current_status?: string;
project_rating: string;
project_description: string;
developed_technology_type: string;
@ -94,6 +95,7 @@ interface ProductInnovationData {
title: string;
project_status: projectStatus;
project_rating: string;
current_status?: string;
project_description: string;
developed_technology_type: string;
obtained_standard_title: string;
@ -138,10 +140,14 @@ const columns = [
];
export default function Timeline() {
const stages = ["تجاری سازی", "توسعه", "تحلیل بازار", "ثبت ایده"];
const currentStage = 1; // index of current stage
export default function Timeline( valueTimeLine : string) {
const stages = ["تجاری سازی", "توسعه", "تحلیل بازار", "ثبت ایده"];
const currentStage = stages?.toReversed()?.findIndex((x : string) => x == valueTimeLine)
const per = () => {
const main = stages?.findIndex((x) => x == "ثبت ایده")
console.log( 'yay ' , 25 * main + 12.5);
return 25 * main + 12.5
}
return (
<div className="w-full p-4">
{/* Year labels */}
@ -151,7 +157,6 @@ export default function Timeline() {
<span>۱۴۰۵</span>
<span>۱۴۰۴</span>
</div>
{/* Timeline bar */}
<div className="relative rounded-lg flex mb-4 items-center">
{stages.map((stage, index) => (
@ -171,15 +176,17 @@ export default function Timeline() {
))}
{/* Vertical line showing current position */}
<div
className="absolute left-[37%] top-0 h-[150%] bottom-0 w-[2px] bg-white rounded-full"
{ valueTimeLine?.length > 0 && ( <> <div
className={`absolute top-0 h-[150%] bottom-0 w-[2px] bg-white rounded-full`}
style={{ left: `${(currentStage + 0.5) * (100 / stages.length)}%` }}
/>
<div
className="absolute top-15 h-[max-content] translate-x-[-50%] text-xs text-gray-300 border-gray-400 rounded-md border px-2 bottom-0"
style={{ left: `${(currentStage + 0.5) * (100 / stages.length)}%` }}
>وضعیت فعلی</div>
</div>
</> ) }
</div>
</div>
);
}
@ -343,6 +350,7 @@ export function ProductInnovationPage() {
"project_no",
"title",
"project_status",
"current_status",
"project_rating",
"project_description",
"developed_technology_type",
@ -350,7 +358,7 @@ export function ProductInnovationPage() {
"knowledge_based_certificate_obtained",
"knowledge_based_certificate_number",
"certificate_obtain_date",
"issuing_authority",
"issuing_authority"
],
Sorts: [["start_date", "asc"]],
Conditions: [["type_of_innovation", "=", "نوآوری در محصول"]],
@ -458,8 +466,8 @@ export function ProductInnovationPage() {
...prev,
revenueNewProducts: {
...prev.revenueNewProducts,
value: formatNumber(normalized.new_products_revenue_share),
percent: formatNumber(normalized.new_products_revenue_share_percent),
value: formatNumber(normalized?.new_products_revenue_share),
percent: formatNumber(normalized?.new_products_revenue_share_percent),
},
impactOnImports: {
...prev.impactOnImports,
@ -708,8 +716,8 @@ export function ProductInnovationPage() {
<div className="col-span-2">
<MetricCard
title={stateCard.revenueNewProducts.title}
value={stateCard.revenueNewProducts.value}
percentValue={stateCard.revenueNewProducts.percent}
value={formatNumber(stateCard.revenueNewProducts.value)}
percentValue={formatNumber(stateCard.revenueNewProducts.percent)}
valueLabel={stateCard.revenueNewProducts.description}
percentLabel={stateCard.revenueNewProducts.descriptionPercent}
/>
@ -936,7 +944,7 @@ export function ProductInnovationPage() {
<h3 className="font-bold text-base">{selectedProjectDetails?.title}</h3>
<p className="py-2">{selectedProjectDetails?.project_description}</p>
</div>
<Timeline />
<Timeline valueTimeLine={selectedProjectDetails?.current_status} />
{/* Technical Knowledge */}
<div className=" rounded-lg py-2 mb-0">

View File

@ -289,7 +289,7 @@ export function ProjectManagementPage() {
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
// Trigger load more when scrolled to 90% of the container
if (scrollPercentage == 1) {
if (scrollPercentage == 1 || scrollPercentage == .9) {
loadMore();
}
};

View File

@ -41,11 +41,11 @@ const chartConfig = {
},
};
const maxHeight = 150;
const maxHeight = 150;
const barHeights = () => Math.floor(Math.random() * maxHeight);
const ChartSkeleton = () => (
<div className="flex justify-center h-96 w-full p-4">
{/* Chart bars */}
<div className=" w-full flex items-end gap-10">
@ -131,7 +131,7 @@ export function StrategicAlignmentPopup({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full max-w-4xl bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] text-white border-none">
<DialogHeader className="border-b-3 mb-10 py-2 w-full pb-4 border-b-2 border-gray-500/20">
<DialogHeader className="mb-10 py-2 w-full pb-4 border-b-2 border-gray-500/20">
<DialogTitle className="ml-auto text-sm text-white ">میزان انطباق راهبردی</DialogTitle>
</DialogHeader>
@ -161,7 +161,7 @@ export function StrategicAlignmentPopup({
return (
<g transform={`translate(${x},${y})`}>
<foreignObject width={80} height={20} x={-45} y={0}>
<TruncatedText
<TruncatedText
maxWords={2}
text={payload.value}
/>
@ -182,14 +182,14 @@ export function StrategicAlignmentPopup({
label={{
value: "تعداد برنامه ها" ,
angle: -90,
position: "insideLeft",
value: "تعداد برنامه ها" ,
angle: -90,
position: "insideLeft",
fill: "#94a3b8",
fontSize: 11,
offset: 0,
dy: 0,
style: { textAnchor: "middle" },
dy: 0,
style: { textAnchor: "middle" },
}}
/>
@ -209,7 +209,7 @@ export function StrategicAlignmentPopup({
}}
formatter={(v: number) => `${formatNumber(Math.round(v))}`}
/>
</Bar>
</BarChart>
</ChartContainer>

View File

@ -1,4 +1,4 @@
import { formatNumber } from "~/lib/utils";
import { formatNumber, calculateNiceRange } from "~/lib/utils";
export interface BarChartData {
label: string;
@ -29,10 +29,10 @@ export function CustomBarChart({
className = "",
loading = false,
}: CustomBarChartProps) {
// Calculate the maximum value across all data points for consistent scaling
const globalMaxValue = Math.max(
...data.map((item) => item.maxValue || item.value)
);
// استفاده از nice numbers برای محاسبه دامنه مناسب
const values = data.map((item) => item.maxValue || item.value);
const { niceMax, ticks } = calculateNiceRange(values, 0, 5);
const globalMaxValue = niceMax;
// Loading skeleton
if (loading) {
@ -77,6 +77,7 @@ export function CustomBarChart({
<div className="space-y-4 px-4 pb-4">
{data.map((item, index) => {
// محاسبه درصد بر اساس nice max value
const percentage =
globalMaxValue > 0 ? (item.value / globalMaxValue) * 100 : 0;
const displayValue: any = item.value;
@ -106,7 +107,7 @@ export function CustomBarChart({
<span className={`text-base font-normal text-left text-white`}>
{item.valuePrefix || ""}
{formatNumber(parseFloat(displayValue))}
{formatNumber(parseFloat(displayValue))}%
{item.valueSuffix || ""}
</span>
</div>
@ -114,24 +115,16 @@ export function CustomBarChart({
);
})}
{/* Axis Labels */}
{/* Axis Labels با استفاده از nice numbers */}
{showAxisLabels && globalMaxValue > 0 && (
<div className="flex w-full items-center gap-3 mt-6">
<span className="min-w-[120px]"></span>
<div className="flex-1 flex justify-between pt-2 border-t border-gray-700">
<span className="text-gray-400 text-xs">{formatNumber(0)}</span>
<span className="text-gray-400 text-xs">
{formatNumber(Math.round(globalMaxValue / 4))}
</span>
<span className="text-gray-400 text-xs">
{formatNumber(Math.round(globalMaxValue / 2))}
</span>
<span className="text-gray-400 text-xs">
{formatNumber(Math.round((globalMaxValue * 3) / 4))}
</span>
<span className="text-gray-400 text-xs">
{formatNumber(Math.round(globalMaxValue))}
</span>
{ticks.map((tick, index) => (
<span key={index} className="text-gray-400 text-xs">
{formatNumber(tick)}%
</span>
))}
</div>
<span className="min-w-[0px]"></span>
</div>

View File

@ -1,7 +1,7 @@
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "~/lib/utils"
import { cn, formatNumber } from "~/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
@ -10,14 +10,19 @@ const Progress = React.forwardRef<
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
"relative h-4 w-full overflow-hidden rounded-full bg-pr-gray",
className
)}
{...props}
>
<span className="left-0 text-sm absolute z-10 px-2 text-[#5F6284]">۰%</span>
<span className="w-full text-sm absolute z-10 px-2 text-[#5F6284]"
style={{ transform: `translateX(-${10 - (value || 0)}%)` }}
>{formatNumber(Math.ceil(value || 0 * 10) / 10)}%</span>
<span className="right-0 text-sm absolute z-10 px-2 text-[#5F6284]">{formatNumber(.2)}%</span>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
style={{ transform: `translateX(-${20 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))

View File

@ -24,6 +24,98 @@ export const formatCurrency = (amount: string | number) => {
/**
* محاسبه دامنه nice numbers برای محور Y نمودارها
* @param values آرایه از مقادیر دادهها
* @param minValue حداقل مقدار (پیش‌فرض: 0 برای دادههای درصدی)
* @param marginPercent درصد حاشیه اضافی (پیش‌فرض: 5%)
* @returns شیء شامل حداکثر nice، فاصله tick ها، و آرایه tick ها
*/
export function calculateNiceRange(
values: number[],
minValue: number = 0,
marginPercent: number = 5
): {
niceMax: number;
tickInterval: number;
ticks: number[];
} {
if (values.length === 0) {
return { niceMax: 100, tickInterval: 20, ticks: [0, 20, 40, 60, 80, 100] };
}
// پیدا کردن حداکثر مقدار در داده‌ها
const dataMax = Math.max(...values);
// اگر همه مقادیر صفر یا منفی هستند
if (dataMax <= 0) {
return { niceMax: 100, tickInterval: 20, ticks: [0, 20, 40, 60, 80, 100] };
}
// اضافه کردن حاشیه
const maxWithMargin = dataMax * (1 + marginPercent / 100);
// محاسبه nice upper limit
const niceMax = calculateNiceNumber(maxWithMargin, true);
// محاسبه فاصله مناسب tick ها بر اساس niceMax
const range = niceMax - minValue;
const targetTicks = 5; // هدف: 5 tick
const roughTickInterval = range / (targetTicks - 1);
const niceTickInterval = calculateNiceNumber(roughTickInterval, false);
// ایجاد آرایه tick ها
const ticks: number[] = [];
for (let i = minValue; i <= niceMax; i += niceTickInterval) {
ticks.push(Math.round(i));
}
// اطمینان از اینکه niceMax در آرایه tick ها باشد
if (ticks[ticks.length - 1] !== niceMax) {
ticks.push(niceMax);
}
return {
niceMax,
tickInterval: niceTickInterval,
ticks,
};
}
/**
* محاسبه عدد nice (گرد و خوانا) بر اساس الگوریتم nice numbers
* @param value مقدار ورودی
* @param round آیا به سمت بالا گرد شود یا نه
* @returns عدد nice
*/
function calculateNiceNumber(value: number, round: boolean): number {
if (value <= 0) return 0;
// پیدا کردن قدرت 10
const exponent = Math.floor(Math.log10(value));
const fraction = value / Math.pow(10, exponent);
let niceFraction: number;
if (round) {
// برای حداکثر: به سمت بالا گرد می‌کنیم با دقت بیشتر
if (fraction <= 1.0) niceFraction = 1;
else if (fraction <= 2.0) niceFraction = 2;
else if (fraction <= 2.5) niceFraction = 2.5;
else if (fraction <= 5.0) niceFraction = 5;
else if (fraction <= 7.5) niceFraction = 7.5;
else niceFraction = 10;
} else {
// برای فاصله tick ها: اعداد ساده‌تر
if (fraction <= 1.0) niceFraction = 1;
else if (fraction <= 2.0) niceFraction = 2;
else if (fraction <= 5.0) niceFraction = 5;
else niceFraction = 10;
}
return niceFraction * Math.pow(10, exponent);
}
export const handleDataValue = (val: any): any => {
moment.loadPersian({ usePersianDigits: true });
if (val == null) return val;

View File

@ -22,6 +22,7 @@ const API_BASE_URL =
// Import the CompanyDetails type
import type { CompanyDetails } from "~/components/ecosystem/network-graph";
import { formatNumber } from "~/lib/utils";
import { Hexagon } from "lucide-react";
export function meta({}: Route.MetaArgs) {
return [
@ -164,18 +165,21 @@ export default function EcosystemPage() {
</h3>
{selectedCompany?.fields &&
selectedCompany.fields.length > 0 ? (
<div className="space-y-3 px-4">
<div className="space-y-3 px-2">
{selectedCompany.fields.map((field, index) => (
<div
key={index}
className="flex justify-between items-center rounded-lg"
>
<span className="font-persian text-sm font-light">
<span className="font-persian flex items-center gap-1 text-sm font-light">
<Hexagon className="text-pr-green h-4 w-4" />
{field.N}:
</span>
<span className="font-persian text-sm font-normal text-right">
{handleValue(field.V)}
{field.U && <span className="mr-1">({field.U})</span>}
<span className="text-right min-w-1/3">
<span className="font-persian text-sm font-normal text-right">
{handleValue(field.V)}
{field.U && <span className="mr-1">({field.U})</span>}
</span>
</span>
</div>
))}