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

View File

@ -260,7 +260,6 @@ export function ProcessInnovationPage() {
Pagination: { PageNumber: pageToFetch, PageSize: pageSize }, Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
}); });
console.log(JSON.parse(response.data));
if (response.state === 0) { if (response.state === 0) {
const dataString = response.data; const dataString = response.data;
if (dataString && typeof dataString === "string") { if (dataString && typeof dataString === "string") {
@ -656,6 +655,12 @@ export function ProcessInnovationPage() {
</div> </div>
{/* Process Impacts Chart */} {/* 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"> <BaseCard className="rounded-2xl w-full overflow-hidden">
<CustomBarChart <CustomBarChart
title="تاثیرات فرآیندی به صورت درصد مقایسه ای" title="تاثیرات فرآیندی به صورت درصد مقایسه ای"

View File

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

View File

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

View File

@ -131,7 +131,7 @@ export function StrategicAlignmentPopup({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <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"> <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> <DialogTitle className="ml-auto text-sm text-white ">میزان انطباق راهبردی</DialogTitle>
</DialogHeader> </DialogHeader>

View File

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

View File

@ -1,7 +1,7 @@
import * as React from "react" import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress" import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "~/lib/utils" import { cn, formatNumber } from "~/lib/utils"
const Progress = React.forwardRef< const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>, React.ElementRef<typeof ProgressPrimitive.Root>,
@ -10,14 +10,19 @@ const Progress = React.forwardRef<
<ProgressPrimitive.Root <ProgressPrimitive.Root
ref={ref} ref={ref}
className={cn( 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 className
)} )}
{...props} {...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 <ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all" 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> </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 => { export const handleDataValue = (val: any): any => {
moment.loadPersian({ usePersianDigits: true }); moment.loadPersian({ usePersianDigits: true });
if (val == null) return val; if (val == null) return val;

View File

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