diff --git a/app/components/dashboard/project-management/process-innovation-page.tsx b/app/components/dashboard/project-management/process-innovation-page.tsx index 8870352..1a1008b 100644 --- a/app/components/dashboard/project-management/process-innovation-page.tsx +++ b/app/components/dashboard/project-management/process-innovation-page.tsx @@ -22,10 +22,6 @@ import { ChevronUp, ChevronDown, RefreshCw, - TrendingUp, - TrendingDown, - Target, - BarChart3, ExternalLink, } from "lucide-react"; import apiService from "~/lib/api"; @@ -57,6 +53,19 @@ interface StatsCard { color: string; } +interface InnovationStats { + totalProjects: number; + averageScore: number; + productionStopsPreventionSum: number; // مجموع جلوگیری از توقفات تولید + bottleneckRemovalCount: number; // تعداد رفع گلوگاه + currencyReductionSum: number; // مجموع کاهش ارز بری (میلیون ریال) + frequentFailuresReductionSum: number; // مجموع کاهش خرابی های پرتکرار + percentProductionStops: number; // درصد مقایسه‌ای جلوگیری از توقفات تولید + percentBottleneckRemoval: number; // درصد مقایسه‌ای رفع گلوگاه + percentCurrencyReduction: number; // درصد مقایسه‌ای کاهش ارز بری + percentFailuresReduction: number; // درصد مقایسه‌ای کاهش خرابی‌های پرتکرار +} + const columns = [ { key: "select", label: "", sortable: false, width: "50px" }, { key: "project_no", label: "شماره پروژه", sortable: true, width: "140px" }, @@ -75,6 +84,19 @@ export function ProcessInnovationPage() { const [hasMore, setHasMore] = useState(true); const [totalCount, setTotalCount] = useState(0); const [actualTotalCount, setActualTotalCount] = useState(0); + const [statsLoading, setStatsLoading] = useState(false); + const [stats, setStats] = useState({ + totalProjects: 0, + averageScore: 0, + productionStopsPreventionSum: 0, + bottleneckRemovalCount: 0, + currencyReductionSum: 0, + frequentFailuresReductionSum: 0, + percentProductionStops: 0, + percentBottleneckRemoval: 0, + percentCurrencyReduction: 0, + percentFailuresReduction: 0, + }); const [sortConfig, setSortConfig] = useState({ field: "start_date", direction: "asc", @@ -121,10 +143,7 @@ export function ProcessInnovationPage() { { id: "production-stops-prevention", title: "جلوگیری از توقفات تولید", - value: formatNumber(projects - .filter(p => p.reduce_prevention_production_stops && parseFloat(p.reduce_prevention_production_stops) > 0) - .reduce((sum, p) => sum + parseFloat(p.reduce_prevention_production_stops), 0) - .toFixed(1)), + value: formatNumber(stats.productionStopsPreventionSum.toFixed?.(1) ?? stats.productionStopsPreventionSum), description: "ظرفیت افزایش یافته", icon: , color: "text-emerald-400", @@ -132,7 +151,7 @@ export function ProcessInnovationPage() { { id: "bottleneck-removal", title: "رفع گلوگاه", - value: formatNumber(projects.filter(p => p.throat_removal === "بله").length), + value: formatNumber(stats.bottleneckRemovalCount), description: "تعداد رفع گلوگاه", icon: , color: "text-emerald-400" @@ -141,10 +160,7 @@ export function ProcessInnovationPage() { { id: "currency-reduction", title: "کاهش ارز بری", - value: formatNumber(projects - .filter(p => p.amount_currency_reduction && parseFloat(p.amount_currency_reduction) > 0) - .reduce((sum, p) => sum + parseFloat(p.amount_currency_reduction), 0) - .toFixed(0)), + value: formatNumber(stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum), description: "میلیون ریال کاهش یافته", icon: , color: "text-emerald-400", @@ -152,10 +168,7 @@ export function ProcessInnovationPage() { { id: "frequent-failures-reduction", title: "کاهش خرابیهای پرتکرار", - value: formatNumber(projects - .filter(p => p.Reduce_rate_failure && parseFloat(p.Reduce_rate_failure) > 0) - .reduce((sum, p) => sum + parseFloat(p.Reduce_rate_failure), 0) - .toFixed(1)), + value: formatNumber(stats.frequentFailuresReductionSum.toFixed?.(1) ?? stats.frequentFailuresReductionSum), description: "مجموع درصد کاهش خرابی", icon: , color: "text-emerald-400", @@ -191,6 +204,7 @@ export function ProcessInnovationPage() { "amount_currency_reduction", "Reduce_rate_failure", ], + Pagination: { PageNumber: pageToFetch, PageSize: pageSize }, Sorts: [[sortConfig.field, sortConfig.direction]], Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]], }); @@ -263,6 +277,7 @@ export function ProcessInnovationPage() { useEffect(() => { fetchProjects(true); fetchTotalCount(); + fetchStats(); }, [sortConfig]); useEffect(() => { @@ -322,7 +337,10 @@ export function ProcessInnovationPage() { try { const parsedData = JSON.parse(dataString); if (Array.isArray(parsedData) && parsedData[0]) { - setActualTotalCount(parsedData[0].project_no_count || 0); + const count = parsedData[0].project_no_count || 0; + setActualTotalCount(count); + // Keep stats in sync if backend stats not yet loaded + setStats((prev) => ({ ...prev, totalProjects: count })); } } catch (parseError) { console.error("Error parsing count data:", parseError); @@ -334,6 +352,52 @@ export function ProcessInnovationPage() { } }; + // Fetch aggregated stats from backend call API (innovation_process_function) + const fetchStats = async () => { + try { + setStatsLoading(true); + const raw = await apiService.callInnovationProcess({ + innovation_process_function: { + }, + }); + + let payload: any = raw?.data; + if (typeof payload === "string") { + try { payload = JSON.parse(payload); } catch {} + } + + const parseNum = (v: unknown): number => { + if (v == null) return 0; + if (typeof v === "number") return v; + if (typeof v === "string") { + const cleaned = v.replace(/,/g, "").trim(); + const n = parseFloat(cleaned); + return isNaN(n) ? 0 : n; + } + return 0; + }; + + const normalized: InnovationStats = { + totalProjects: parseNum(payload?.count_innovation_process_projects), + averageScore: parseNum(payload?.average_project_score), + productionStopsPreventionSum: parseNum(payload?.sum_stopping_production), + bottleneckRemovalCount: parseNum(payload?.count_throat_removal), + currencyReductionSum: parseNum(payload?.sum_reduction_value_currency), + frequentFailuresReductionSum: parseNum(payload?.sum_reducing_breakdowns), + percentProductionStops: parseNum(payload?.percent_sum_stopping_production), + percentBottleneckRemoval: parseNum(payload?.percent_throat_removal), + percentCurrencyReduction: parseNum(payload?.percent_reduction_value_currency), + percentFailuresReduction: parseNum(payload?.percent_reducing_breakdowns), + }; + + setStats(normalized); + } catch (error) { + console.error("Error fetching stats:", error); + } finally { + setStatsLoading(false); + } + }; + const handleRefresh = () => { fetchingRef.current = false; setCurrentPage(1); @@ -341,6 +405,7 @@ export function ProcessInnovationPage() { setHasMore(true); fetchProjects(true); fetchTotalCount(); + fetchStats(); }; const formatCurrency = (amount: string | number) => { @@ -468,13 +533,13 @@ export function ProcessInnovationPage() {
{/* Stats Grid */}
- {loading ? ( + {loading || statsLoading ? ( // Loading skeleton for stats cards - matching new design Array.from({ length: 4 }).map((_, index) => (
-
+
@@ -493,16 +558,16 @@ export function ProcessInnovationPage() {
-
-

+
+

{card.title}

-
+
{card.icon}
-

+

{card.value}

@@ -520,8 +585,8 @@ export function ProcessInnovationPage() { {/* Process Impacts Chart */} - -

+ +

تاثیرات فرآیندی به صورت درصد مقایسه ای

@@ -536,21 +601,13 @@ export function ProcessInnovationPage() {
p.reduce_prevention_production_stops && parseFloat(p.reduce_prevention_production_stops) > 0) - .reduce((sum, p) => sum + parseFloat(p.reduce_prevention_production_stops), 0) / 10, - 100 - )}%` + width: `${Math.min((stats.percentProductionStops || 0), 100)}%` }} >
- {formatNumber(projects - .filter(p => p.reduce_prevention_production_stops && parseFloat(p.reduce_prevention_production_stops) > 0) - .reduce((sum, p) => sum + parseFloat(p.reduce_prevention_production_stops), 0) - .toFixed(1))}% + {formatNumber(((stats.percentProductionStops ?? 0) as number).toFixed?.(1) ?? (stats.percentProductionStops ?? 0))}%
@@ -561,16 +618,13 @@ export function ProcessInnovationPage() {
p.throat_removal === "بله").length * 10, - 100 - )}%` + width: `${Math.min((stats.percentBottleneckRemoval || 0), 100)}%` }} >
- {formatNumber(projects.filter(p => p.throat_removal === "بله").length)}% + {formatNumber(((stats.percentBottleneckRemoval ?? 0) as number).toFixed?.(1) ?? (stats.percentBottleneckRemoval ?? 0))}%
@@ -581,21 +635,13 @@ export function ProcessInnovationPage() {
p.amount_currency_reduction && parseFloat(p.amount_currency_reduction) > 0) - .reduce((sum, p) => sum + parseFloat(p.amount_currency_reduction), 0) / 100, - 100 - )}%` + width: `${Math.min((stats.percentCurrencyReduction || 0), 100)}%` }} >

- {formatNumber(projects - .filter(p => p.amount_currency_reduction && parseFloat(p.amount_currency_reduction) > 0) - .reduce((sum, p) => sum + parseFloat(p.amount_currency_reduction), 0) - .toFixed(0))}% + {formatNumber(((stats.percentCurrencyReduction ?? 0) as number).toFixed?.(0) ?? (stats.percentCurrencyReduction ?? 0))}%
@@ -606,21 +652,13 @@ export function ProcessInnovationPage() {
p.Reduce_rate_failure && parseFloat(p.Reduce_rate_failure) > 0) - .reduce((sum, p) => sum + parseFloat(p.Reduce_rate_failure), 0) / 10, - 100 - )}%` + width: `${Math.min((stats.percentFailuresReduction || 0), 100)}%` }} >
- {formatNumber(projects - .filter(p => p.Reduce_rate_failure && parseFloat(p.Reduce_rate_failure) > 0) - .reduce((sum, p) => sum + parseFloat(p.Reduce_rate_failure), 0) - .toFixed(1))}% + {formatNumber(((stats.percentFailuresReduction ?? 0) as number).toFixed?.(1) ?? (stats.percentFailuresReduction ?? 0))}%
@@ -628,62 +666,23 @@ export function ProcessInnovationPage() { {/* Percentage Scale */}
۰٪ - - {formatNumber(Math.round(Math.max( - projects - .filter(p => p.reduce_prevention_production_stops && parseFloat(p.reduce_prevention_production_stops) > 0) - .reduce((sum, p) => sum + parseFloat(p.reduce_prevention_production_stops), 0) / 10, - projects.filter(p => p.throat_removal === "بله").length * 10, - projects - .filter(p => p.amount_currency_reduction && parseFloat(p.amount_currency_reduction) > 0) - .reduce((sum, p) => sum + parseFloat(p.amount_currency_reduction), 0) / 100, - projects - .filter(p => p.Reduce_rate_failure && parseFloat(p.Reduce_rate_failure) > 0) - .reduce((sum, p) => sum + parseFloat(p.Reduce_rate_failure), 0) / 10 - ) / 4))}٪ - - - {formatNumber(Math.round(Math.max( - projects - .filter(p => p.reduce_prevention_production_stops && parseFloat(p.reduce_prevention_production_stops) > 0) - .reduce((sum, p) => sum + parseFloat(p.reduce_prevention_production_stops), 0) / 10, - projects.filter(p => p.throat_removal === "بله").length * 10, - projects - .filter(p => p.amount_currency_reduction && parseFloat(p.amount_currency_reduction) > 0) - .reduce((sum, p) => sum + parseFloat(p.amount_currency_reduction), 0) / 100, - projects - .filter(p => p.Reduce_rate_failure && parseFloat(p.Reduce_rate_failure) > 0) - .reduce((sum, p) => sum + parseFloat(p.Reduce_rate_failure), 0) / 10 - ) / 2))}٪ - - - {formatNumber(Math.round(Math.max( - projects - .filter(p => p.reduce_prevention_production_stops && parseFloat(p.reduce_prevention_production_stops) > 0) - .reduce((sum, p) => sum + parseFloat(p.reduce_prevention_production_stops), 0) / 10, - projects.filter(p => p.throat_removal === "بله").length * 10, - projects - .filter(p => p.amount_currency_reduction && parseFloat(p.amount_currency_reduction) > 0) - .reduce((sum, p) => sum + parseFloat(p.amount_currency_reduction), 0) / 100, - projects - .filter(p => p.Reduce_rate_failure && parseFloat(p.Reduce_rate_failure) > 0) - .reduce((sum, p) => sum + parseFloat(p.Reduce_rate_failure), 0) / 10 - ) * 3 / 4))}٪ - - - {formatNumber(Math.round(Math.max( - projects - .filter(p => p.reduce_prevention_production_stops && parseFloat(p.reduce_prevention_production_stops) > 0) - .reduce((sum, p) => sum + parseFloat(p.reduce_prevention_production_stops), 0) / 10, - projects.filter(p => p.throat_removal === "بله").length * 10, - projects - .filter(p => p.amount_currency_reduction && parseFloat(p.amount_currency_reduction) > 0) - .reduce((sum, p) => sum + parseFloat(p.amount_currency_reduction), 0) / 100, - projects - .filter(p => p.Reduce_rate_failure && parseFloat(p.Reduce_rate_failure) > 0) - .reduce((sum, p) => sum + parseFloat(p.Reduce_rate_failure), 0) / 10 - )))}٪ - + { + (() => { + const p1 = (stats.percentProductionStops || 0); + const p2 = (stats.percentBottleneckRemoval || 0); + const p3 = (stats.percentCurrencyReduction || 0); + const p4 = (stats.percentFailuresReduction || 0); + const maxVal = Math.max(p1, p2, p3, p4); + return ( + <> + {formatNumber(Math.round(maxVal / 4))}٪ + {formatNumber(Math.round(maxVal / 2))}٪ + {formatNumber(Math.round((maxVal * 3) / 4))}٪ + {formatNumber(Math.round(maxVal))}٪ + + ); + })() + }
@@ -691,7 +690,7 @@ export function ProcessInnovationPage() { {/* Data Table */} - +
@@ -785,7 +784,7 @@ export function ProcessInnovationPage() { {/* Infinite scroll trigger */}
{loadingMore && ( -
+
@@ -823,7 +822,7 @@ export function ProcessInnovationPage() {
-
کل پروژه ها : {formatNumber(actualTotalCount)}
+
کل پروژه ها : {formatNumber(stats.totalProjects || actualTotalCount)}
{/* Project number column - empty */}
@@ -835,13 +834,7 @@ export function ProcessInnovationPage() {
میانگین امتیاز :‌
- {(() => { - if (projects.length === 0) return formatNumber(0); - const validRatings = projects.filter(p => p.project_rating && !isNaN(parseFloat(p.project_rating))); - if (validRatings.length === 0) return formatNumber(0); - const average = validRatings.reduce((sum, p) => sum + parseFloat(p.project_rating), 0) / validRatings.length; - return formatNumber(average.toFixed(1)); - })()} + {formatNumber(((stats.averageScore ?? 0) as number).toFixed?.(1) ?? (stats.averageScore ?? 0))}
diff --git a/app/lib/api.ts b/app/lib/api.ts index 7c65fdc..88700f4 100644 --- a/app/lib/api.ts +++ b/app/lib/api.ts @@ -103,6 +103,50 @@ class ApiService { } } + // POST to external absolute endpoint while reusing token handling + public async postAbsolute( + fullUrl: string, + data?: any, + ): Promise> { + const defaultHeaders: HeadersInit = { + "Content-Type": "application/json;charset=UTF-8", + }; + + if (this.token) { + defaultHeaders.Authorization = `Bearer ${this.token}`; + } + + const config: RequestInit = { + method: "POST", + headers: defaultHeaders, + body: data ? JSON.stringify(data) : undefined, + }; + + try { + const response = await fetch(fullUrl, config); + const apiData: ApiResponse = await response.json(); + + if (!response.ok) { + throw new Error(apiData?.message || `HTTP error! status: ${response.status}`); + } + + if (typeof apiData?.state !== "undefined" && apiData.state !== 0) { + throw new Error(apiData.message || "API error occurred"); + } + + return apiData; + } catch (error) { + console.error("API absolute POST failed:", error); + throw error as Error; + } + } + + // Innovation process function call wrapper + public async callInnovationProcess(payload: any) { + const url = "https://inogen-back.pelekan.org/api/call"; + return this.postAbsolute(url, payload); + } + // GET request public async get(endpoint: string): Promise> { return this.request(endpoint, {