first commit
This commit is contained in:
199
app/src/components/ui/analysis/ProductionCapacity.tsx
Normal file
199
app/src/components/ui/analysis/ProductionCapacity.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
LineElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
} from "chart.js";
|
||||
import { PowerIcon, ProductionCapacityIcon } from "../../icons/analysis";
|
||||
import SkeletonUI from "../../templates/SkeletonUI";
|
||||
import { useInputValues, useMachineUptime, useProductionCapacityData, useThroughPutData } from "../../../store/builder/store";
|
||||
|
||||
ChartJS.register(LineElement, CategoryScale, LinearScale, PointElement);
|
||||
|
||||
const ThroughputSummary: React.FC = () => {
|
||||
// Define all data internally within the component
|
||||
const timeRange = {
|
||||
startTime: "08:00 AM",
|
||||
endTime: "06:00 PM",
|
||||
};
|
||||
|
||||
const { machineActiveTime } = useMachineUptime();
|
||||
const energyConsumption = {
|
||||
energyConsumed: 456,
|
||||
unit: "KWH",
|
||||
};
|
||||
|
||||
// Dynamic shift data
|
||||
const shiftUtilization = [
|
||||
{ shift: 1, percentage: 30, color: "#F3C64D" },
|
||||
{ shift: 2, percentage: 40, color: "#67B3F4" },
|
||||
{ shift: 3, percentage: 30, color: "#7981F5" },
|
||||
];
|
||||
|
||||
const { productionCapacityData } = useProductionCapacityData()
|
||||
const { throughputData: data } = useThroughPutData()
|
||||
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
display: false, // hide X axis completely
|
||||
},
|
||||
y: {
|
||||
display: false, // hide Y axis completely
|
||||
min: 0, // force Y axis to start at 0
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const assetUsage = 85;
|
||||
|
||||
// machineActiveTime => Throughput
|
||||
// shiftUtilization.length => Shifts per day
|
||||
// 8 => Shift length
|
||||
// assetUsage => Yield rate
|
||||
|
||||
const throughtputMachineData = machineActiveTime * shiftUtilization.length * 8
|
||||
|
||||
const throughputData = {
|
||||
labels: ["08:00", "08:10", "08:20", "08:30", "08:40", "08:50"],
|
||||
data: [5, 10, 8, 10, 12, 10],
|
||||
totalThroughput: (throughtputMachineData) * assetUsage / 100,
|
||||
assetUsage: assetUsage,
|
||||
};
|
||||
|
||||
const { inputValues } = useInputValues();
|
||||
// Chart data configuration
|
||||
const chartData = {
|
||||
labels: throughputData.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Units/hour",
|
||||
data: throughputData.data,
|
||||
borderColor: "#B392F0",
|
||||
tension: 0.4,
|
||||
pointRadius: 0, // Hide points
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
//
|
||||
if (productionCapacityData > 0) {
|
||||
setTimeout(() => {
|
||||
|
||||
setIsLoading(false);
|
||||
}, 3000)
|
||||
|
||||
} else {
|
||||
setIsLoading(true);
|
||||
}
|
||||
}, [productionCapacityData]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isLoading && <div className="production analysis-card">
|
||||
<div className="production-wrapper analysis-card-wrapper">
|
||||
<div className="card-header">
|
||||
<div className="header">
|
||||
<div className="main-header">Production Capacity</div>
|
||||
<div className="sub-header">
|
||||
{timeRange.startTime} - {timeRange.endTime}
|
||||
</div>
|
||||
</div>
|
||||
<div className="icon-wrapper">
|
||||
<ProductionCapacityIcon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isLoading ? (
|
||||
<>
|
||||
<div className="process-container">
|
||||
<div className="throughput-value">
|
||||
<span className="value">{productionCapacityData}</span>{" "}
|
||||
Units/hour
|
||||
</div>
|
||||
<div className="lineChart">
|
||||
<div className="assetUsage">
|
||||
<div className="key">Asset usage</div>
|
||||
<div className="value">{parseFloat(inputValues["Yield rate"])}%</div>
|
||||
</div>
|
||||
<Line data={chartData} options={chartOptions} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="footer">
|
||||
<div className="energyConsumption footer-card">
|
||||
<div className="header">Energy Consumption</div>
|
||||
<div className="value-container">
|
||||
<div className="energy-icon">
|
||||
<PowerIcon />
|
||||
</div>
|
||||
<div className="value-wrapper">
|
||||
<div className="value">
|
||||
{energyConsumption.energyConsumed}
|
||||
</div>
|
||||
<div className="unit">{energyConsumption.unit}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shiftUtilization footer-card">
|
||||
<div className="header">Shift Utilization</div>
|
||||
<div className="value-container">
|
||||
<div className="value">{throughputData.assetUsage}%</div>
|
||||
|
||||
<div className="progress-wrapper">
|
||||
{/* Dynamically create progress bars based on shiftUtilization array */}
|
||||
{shiftUtilization.map((shift, index) => (
|
||||
<div
|
||||
key={shift.shift}
|
||||
className={`progress shift-${shift.shift}`}
|
||||
style={{
|
||||
width: `${shift.percentage}%`,
|
||||
backgroundColor: shift.color,
|
||||
}}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="progress-indicator">
|
||||
{/* Dynamically create shift indicators with random colors */}
|
||||
{shiftUtilization.map((shift, index) => (
|
||||
<div className="shift-wrapper" key={shift.shift}>
|
||||
<span
|
||||
className={`indicator shift-${shift.shift}`}
|
||||
style={{ backgroundColor: shift.color }} // Random color for indicator
|
||||
></span>
|
||||
<label>Shift {shift.shift}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<SkeletonUI type={"default"} />
|
||||
)}
|
||||
</div>
|
||||
</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThroughputSummary;
|
||||
253
app/src/components/ui/analysis/ROISummary.tsx
Normal file
253
app/src/components/ui/analysis/ROISummary.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
CostBreakDownIcon,
|
||||
LightBulpIcon,
|
||||
ROISummaryIcon,
|
||||
ROISummaryProductName,
|
||||
SonarCrownIcon,
|
||||
} from "../../icons/analysis";
|
||||
import SemiCircleProgress from "./SemiCircleProgress";
|
||||
import { ArrowIcon } from "../../icons/ExportCommonIcons";
|
||||
import SkeletonUI from "../../templates/SkeletonUI";
|
||||
import { useROISummaryData } from "../../../store/builder/store";
|
||||
|
||||
const ROISummary = ({
|
||||
roiSummaryData = {
|
||||
productName: "Product 1",
|
||||
roiPercentage: 133,
|
||||
paybackPeriod: 53,
|
||||
totalCost: "1,20,000",
|
||||
revenueGenerated: "2,80,000",
|
||||
netProfit: "1,60,000",
|
||||
netLoss: null,
|
||||
costBreakdown: [
|
||||
{
|
||||
item: "Raw Material A",
|
||||
unitCost: "10/unit",
|
||||
laborCost: "0",
|
||||
totalCost: "1000",
|
||||
sellingPrice: "1500",
|
||||
},
|
||||
{
|
||||
item: "Labor",
|
||||
unitCost: "10/unit",
|
||||
laborCost: "500",
|
||||
totalCost: "500",
|
||||
sellingPrice: "N/A",
|
||||
},
|
||||
{
|
||||
item: "Product 1",
|
||||
unitCost: "10/unit",
|
||||
laborCost: "200",
|
||||
totalCost: "200",
|
||||
sellingPrice: "2000",
|
||||
},
|
||||
{
|
||||
item: "Machine",
|
||||
unitCost: "-",
|
||||
laborCost: "-",
|
||||
totalCost: "20,000",
|
||||
sellingPrice: "N/A",
|
||||
},
|
||||
{
|
||||
item: "Total",
|
||||
unitCost: "-",
|
||||
laborCost: "-",
|
||||
totalCost: "1,20,000",
|
||||
sellingPrice: "-",
|
||||
},
|
||||
{
|
||||
item: "Net Profit",
|
||||
unitCost: "-",
|
||||
laborCost: "-",
|
||||
totalCost: "1,60,000",
|
||||
sellingPrice: "-",
|
||||
},
|
||||
],
|
||||
},
|
||||
}) => {
|
||||
const [isTableOpen, setIsTableOpen] = useState(false); // State to handle the table open/close
|
||||
|
||||
// Function to toggle the breakdown table visibility
|
||||
const toggleTable = () => {
|
||||
setIsTableOpen(!isTableOpen);
|
||||
};
|
||||
|
||||
function getCurrentDate() {
|
||||
const now = new Date();
|
||||
const day = now.getDate().toString().padStart(2, "0");
|
||||
const month = now.toLocaleString("en-GB", { month: "long" });
|
||||
const year = now.getFullYear();
|
||||
return `${day} ${month}, ${year}`;
|
||||
}
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { roiSummary } = useROISummaryData();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (roiSummary.productName) {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 4500)
|
||||
} else {
|
||||
// If productName is empty, assume still loading
|
||||
setIsLoading(true);
|
||||
}
|
||||
}, [roiSummary]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{
|
||||
!isLoading && <div className="roiSummary-container analysis-card">
|
||||
<div className="roiSummary-wrapper analysis-card-wrapper">
|
||||
<div className="card-header">
|
||||
<div className="header">
|
||||
<div className="main-header">ROI Summary</div>
|
||||
<div className="sub-header">From {getCurrentDate()}</div>
|
||||
</div>
|
||||
<div className="icon-wrapper">
|
||||
<ROISummaryIcon />
|
||||
</div>
|
||||
</div>
|
||||
{!isLoading ? (
|
||||
<>
|
||||
<div className="product-info">
|
||||
<ROISummaryProductName />
|
||||
<div className="product-label">Product :</div>
|
||||
<div className="product-name">{roiSummary.productName}</div>
|
||||
</div>
|
||||
<div className="playBack">
|
||||
<SonarCrownIcon />
|
||||
<div className="icon"></div>
|
||||
<div className="info">
|
||||
<span> {roiSummary.roiPercentage}%</span> ROI with payback
|
||||
in just <span>{roiSummary.paybackPeriod}</span> months
|
||||
</div>
|
||||
</div>
|
||||
<div className="roi-details">
|
||||
<div className="progress-wrapper">
|
||||
<SemiCircleProgress />
|
||||
<div className="content">
|
||||
you're on track to hit it by
|
||||
<div className="key">July 2029</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="metrics">
|
||||
<div className="metric-wrapper">
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Total Cost Incurred</span>
|
||||
<span className="metric-value">
|
||||
<span>₹</span>
|
||||
{roiSummary.totalCost}
|
||||
</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Revenue Generated</span>
|
||||
<span className="metric-value">
|
||||
<span>₹</span>
|
||||
|
||||
{roiSummary.revenueGenerated}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`metric-item net-profit ${roiSummary.netProfit > 0 ? "profit" : "loss"}`}
|
||||
>
|
||||
<div className="metric-label">
|
||||
<span>↑</span>
|
||||
Net Profit
|
||||
</div>
|
||||
<div className="metric-value">
|
||||
<span>₹</span>
|
||||
{roiSummary.netProfit > 0
|
||||
? roiSummary.netProfit
|
||||
: roiSummary.netLoss}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="cost-breakdown">
|
||||
<div className="breakdown-header" onClick={toggleTable}>
|
||||
<div className="section-wrapper">
|
||||
<CostBreakDownIcon />
|
||||
<span className="section-title">Cost Breakdown</span>
|
||||
</div>
|
||||
|
||||
<span className={`expand-icon ${isTableOpen ? "open" : ""}`}>
|
||||
<ArrowIcon />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`breakdown-table-wrapper ${isTableOpen ? "open" : "closed"
|
||||
}`}
|
||||
style={{
|
||||
transition: "max-height 0.3s ease-in-out",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<table className="breakdown-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Unit Cost</th>
|
||||
<th>Labor Cost</th>
|
||||
<th>Total Cost</th>
|
||||
<th>Selling Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{roiSummaryData.costBreakdown.map((row, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={
|
||||
row.item === "Total"
|
||||
? "total-row"
|
||||
: row.item === "Net Profit"
|
||||
? "net-profit-row"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<td>{row.item}</td>
|
||||
<td>{row.unitCost}</td>
|
||||
<td>{row.laborCost}</td>
|
||||
<td>{row.totalCost}</td>
|
||||
<td>{row.sellingPrice}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="tips-section">
|
||||
<div className="tip-header">
|
||||
<span className="lightbulb-icon">
|
||||
<LightBulpIcon />
|
||||
</span>
|
||||
<span className="tip-title">How to improve ROI?</span>
|
||||
</div>
|
||||
<div className="tip-description">
|
||||
Increase CNC utilization by{" "}
|
||||
<span className="highlight">10%</span> to shave{" "}
|
||||
<span className="highlight">0.5</span> months of payback period
|
||||
<div className="placeHolder-wrapper">
|
||||
<div className="placeHolder"></div>
|
||||
<div className="placeHolder"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="get-tips-button">
|
||||
<div className="btn">Get ROI Boost Tips</div>
|
||||
</button>
|
||||
</div> */}
|
||||
</>
|
||||
) : (
|
||||
<SkeletonUI type={"default"} />
|
||||
// <div> No Data</div>
|
||||
)}
|
||||
</div>
|
||||
</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ROISummary;
|
||||
52
app/src/components/ui/analysis/SemiCircleProgress.tsx
Normal file
52
app/src/components/ui/analysis/SemiCircleProgress.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
const SemiCircleProgress = ({ progress = 60, years = 4.02 }) => {
|
||||
const clampedProgress = Math.min(Math.max(progress, 0), 100);
|
||||
const radius = 80;
|
||||
const strokeWidth = 26;
|
||||
const circumference = Math.PI * radius;
|
||||
const strokeDashoffset =
|
||||
circumference - (clampedProgress / 100) * circumference;
|
||||
|
||||
return (
|
||||
<div className="svg-half-donut">
|
||||
<svg width="200" height="100" viewBox="0 0 200 100">
|
||||
{/* Background track */}
|
||||
<path
|
||||
d="M20,100 A80,80 0 0,1 180,100"
|
||||
fill="none"
|
||||
stroke="#6F6F7A"
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
{/* Progress track */}
|
||||
<path
|
||||
d="M20,100 A80,80 0 0,1 180,100"
|
||||
fill="none"
|
||||
stroke="url(#paint0_linear_4200_2273)"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_4200_2273"
|
||||
x1="26.7278"
|
||||
y1="279.417"
|
||||
x2="298.886"
|
||||
y2="65.0002"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#3D3795" />
|
||||
<stop offset="0.535019" stopColor="#2A9BCF" />
|
||||
<stop offset="1" stopColor="#0DB3F4" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<div className="label-wrapper">
|
||||
<div className="label">{years}</div>
|
||||
<div className="label-content">Years</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SemiCircleProgress;
|
||||
98
app/src/components/ui/analysis/ThroughputSummary.tsx
Normal file
98
app/src/components/ui/analysis/ThroughputSummary.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMachineCount, useMachineUptime, useMaterialCycle, useProductionCapacityData, useThroughPutData } from "../../../store/builder/store";
|
||||
import {
|
||||
ThroughputSummaryIcon,
|
||||
} from "../../icons/analysis";
|
||||
import SkeletonUI from "../../templates/SkeletonUI";
|
||||
|
||||
const ProductionCapacity = ({
|
||||
avgProcessTime = "28.4 Secs/unit",
|
||||
machineUtilization = "78%",
|
||||
throughputValue = 128,
|
||||
timeRange = { startTime: "08:00 AM", endTime: "09:00 AM" },
|
||||
}) => {
|
||||
const { machineActiveTime } = useMachineUptime();
|
||||
const { materialCycleTime } = useMaterialCycle();
|
||||
const { throughputData } = useThroughPutData()
|
||||
|
||||
const progressPercent = machineActiveTime;
|
||||
|
||||
|
||||
const totalBars = 6;
|
||||
const barsToFill = Math.floor((progressPercent / 100) * totalBars);
|
||||
const partialFillPercent =
|
||||
((progressPercent / 100) * totalBars - barsToFill) * 100;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (throughputData > 0) {
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
setIsLoading(true);
|
||||
}
|
||||
}, [throughputData])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{!isLoading && <div className="throughtputSummary-container analysis-card">
|
||||
<div className="throughtputSummary-wrapper analysis-card-wrapper">
|
||||
<div className="card-header">
|
||||
<div className="header">
|
||||
<div className="main-header">Throughput Summary</div>
|
||||
<div className="sub-header">
|
||||
{timeRange.startTime} - {timeRange.endTime}
|
||||
</div>
|
||||
</div>
|
||||
<div className="icon-wrapper">
|
||||
<ThroughputSummaryIcon />
|
||||
</div>
|
||||
</div>
|
||||
{!isLoading ? (
|
||||
<>
|
||||
<div className="process-container">
|
||||
<div className="throughput-value">
|
||||
<span className="value">{throughputData}</span> Units/hour
|
||||
|
||||
</div>
|
||||
|
||||
{/* Dynamic Progress Bar */}
|
||||
<div className="progress-bar-wrapper">
|
||||
{[...Array(totalBars)].map((_, i) => (
|
||||
<div className="progress-bar" key={i}>
|
||||
{i < barsToFill ? (
|
||||
<div className="bar-fill full" />
|
||||
) : i === barsToFill ? (
|
||||
<div
|
||||
className="bar-fill partial"
|
||||
style={{ width: `${partialFillPercent}%` }}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metrics-section">
|
||||
<div className="metric">
|
||||
<span className="label">Avg. Process Time</span>
|
||||
<span className="value">{materialCycleTime} secs/unit </span>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="label">Machine Utilization</span>
|
||||
<span className="value">{machineActiveTime}</span>
|
||||
{/* <span className="value">{machineActiveTime}</span> */}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<SkeletonUI type={"default"} />
|
||||
)}
|
||||
</div>
|
||||
</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductionCapacity;
|
||||
Reference in New Issue
Block a user