@dilipod/ui 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/impact-metrics-form.d.ts.map +1 -1
- package/dist/index.js +97 -109
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +97 -109
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/impact-metrics-form.tsx +99 -122
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react'
|
|
4
|
-
import { Card, CardContent
|
|
4
|
+
import { Card, CardContent } from './card'
|
|
5
5
|
import { Button } from './button'
|
|
6
|
-
import { IconBox } from './icon-box'
|
|
7
6
|
import { toast } from './use-toast'
|
|
8
7
|
import { cn } from '../lib/utils'
|
|
9
8
|
|
|
@@ -138,6 +137,13 @@ export function ImpactMetricsForm({
|
|
|
138
137
|
// Calculate hours saved per year based on FTE
|
|
139
138
|
const hoursSavedPerYear = Math.round(metrics.fte_equivalent * HOURS_PER_FTE_YEAR)
|
|
140
139
|
|
|
140
|
+
// Calculate implied frequency based on time per task and FTE%
|
|
141
|
+
const timePerTaskHours = metrics.time_saved_minutes_per_run / 60
|
|
142
|
+
const impliedFrequencyPerYear = timePerTaskHours > 0
|
|
143
|
+
? Math.round(hoursSavedPerYear / timePerTaskHours)
|
|
144
|
+
: 0
|
|
145
|
+
const impliedFrequencyPerMonth = Math.round(impliedFrequencyPerYear / 12)
|
|
146
|
+
|
|
141
147
|
// Calculate labor savings (before worker cost)
|
|
142
148
|
const laborSavingsPerYear = metrics.fte_equivalent * HOURS_PER_FTE_YEAR * metrics.hourly_rate_euros
|
|
143
149
|
|
|
@@ -145,17 +151,11 @@ export function ImpactMetricsForm({
|
|
|
145
151
|
const netAnnualSavings = laborSavingsPerYear - workerCostPerYear
|
|
146
152
|
|
|
147
153
|
return (
|
|
148
|
-
<Card className={cn("
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 256 256">
|
|
154
|
-
<path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-88a16,16,0,1,1-16-16A16,16,0,0,1,144,128Zm-56,0a16,16,0,1,1-16-16A16,16,0,0,1,88,128Zm112,0a16,16,0,1,1-16-16A16,16,0,0,1,200,128Z"/>
|
|
155
|
-
</svg>
|
|
156
|
-
</IconBox>
|
|
157
|
-
Impact Metrics (ROI)
|
|
158
|
-
</CardTitle>
|
|
154
|
+
<Card className={cn("", className)}>
|
|
155
|
+
<CardContent className="p-5">
|
|
156
|
+
{/* Header */}
|
|
157
|
+
<div className="flex items-center justify-between mb-4">
|
|
158
|
+
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Impact Metrics (ROI)</p>
|
|
159
159
|
<div className="flex items-center gap-2">
|
|
160
160
|
{isEditing ? (
|
|
161
161
|
<>
|
|
@@ -187,127 +187,104 @@ export function ImpactMetricsForm({
|
|
|
187
187
|
)}
|
|
188
188
|
</div>
|
|
189
189
|
</div>
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
<div className="grid
|
|
190
|
+
|
|
191
|
+
{/* Metrics Grid */}
|
|
192
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6">
|
|
193
193
|
{/* Time per task */}
|
|
194
|
-
<div
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
</div>
|
|
216
|
-
) : (
|
|
217
|
-
<p className="text-2xl font-bold">
|
|
218
|
-
{metrics.time_saved_minutes_per_run} <span className="text-base font-normal text-muted-foreground">min</span>
|
|
219
|
-
</p>
|
|
220
|
-
)}
|
|
221
|
-
<p className="text-xs text-muted-foreground mt-1">How long manually</p>
|
|
222
|
-
</div>
|
|
194
|
+
<div>
|
|
195
|
+
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-1">Time per Task</p>
|
|
196
|
+
{isEditing ? (
|
|
197
|
+
<div className="flex items-baseline gap-1">
|
|
198
|
+
<input
|
|
199
|
+
type="number"
|
|
200
|
+
value={metrics.time_saved_minutes_per_run}
|
|
201
|
+
onChange={(e) => setMetrics(prev => ({
|
|
202
|
+
...prev,
|
|
203
|
+
time_saved_minutes_per_run: parseInt(e.target.value) || 0
|
|
204
|
+
}))}
|
|
205
|
+
className="w-16 px-2 py-1 text-2xl font-bold border border-border rounded-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan)] bg-background"
|
|
206
|
+
min="0"
|
|
207
|
+
/>
|
|
208
|
+
<span className="text-sm text-muted-foreground">min</span>
|
|
209
|
+
</div>
|
|
210
|
+
) : (
|
|
211
|
+
<p className="text-2xl font-bold">
|
|
212
|
+
{metrics.time_saved_minutes_per_run}<span className="text-sm font-normal text-muted-foreground ml-1">min</span>
|
|
213
|
+
</p>
|
|
214
|
+
)}
|
|
223
215
|
</div>
|
|
224
216
|
|
|
225
217
|
{/* Manual cost */}
|
|
226
|
-
<div
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
</div>
|
|
250
|
-
) : (
|
|
251
|
-
<p className="text-2xl font-bold">
|
|
252
|
-
€{metrics.hourly_rate_euros} <span className="text-base font-normal text-muted-foreground">/hr</span>
|
|
253
|
-
</p>
|
|
254
|
-
)}
|
|
255
|
-
<p className="text-xs text-muted-foreground mt-1">Employee hourly cost</p>
|
|
256
|
-
</div>
|
|
218
|
+
<div>
|
|
219
|
+
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-1">Manual Cost</p>
|
|
220
|
+
{isEditing ? (
|
|
221
|
+
<div className="flex items-baseline gap-1">
|
|
222
|
+
<span className="text-sm text-muted-foreground">€</span>
|
|
223
|
+
<input
|
|
224
|
+
type="number"
|
|
225
|
+
value={metrics.hourly_rate_euros}
|
|
226
|
+
onChange={(e) => setMetrics(prev => ({
|
|
227
|
+
...prev,
|
|
228
|
+
hourly_rate_euros: parseFloat(e.target.value) || 0
|
|
229
|
+
}))}
|
|
230
|
+
className="w-16 px-2 py-1 text-2xl font-bold border border-border rounded-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan)] bg-background"
|
|
231
|
+
min="0"
|
|
232
|
+
step="0.5"
|
|
233
|
+
/>
|
|
234
|
+
<span className="text-sm text-muted-foreground">/hr</span>
|
|
235
|
+
</div>
|
|
236
|
+
) : (
|
|
237
|
+
<p className="text-2xl font-bold">
|
|
238
|
+
€{metrics.hourly_rate_euros}<span className="text-sm font-normal text-muted-foreground ml-1">/hr</span>
|
|
239
|
+
</p>
|
|
240
|
+
)}
|
|
257
241
|
</div>
|
|
258
242
|
|
|
259
243
|
{/* Job portion */}
|
|
260
|
-
<div
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
) : (
|
|
285
|
-
<p className="text-2xl font-bold">
|
|
286
|
-
{Math.round(metrics.fte_equivalent * 100)} <span className="text-base font-normal text-muted-foreground">%</span>
|
|
287
|
-
</p>
|
|
288
|
-
)}
|
|
289
|
-
<p className="text-xs text-muted-foreground mt-1">% of FTE ({hoursSavedPerYear}h/year)</p>
|
|
290
|
-
</div>
|
|
244
|
+
<div>
|
|
245
|
+
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-1">Job Portion</p>
|
|
246
|
+
{isEditing ? (
|
|
247
|
+
<div className="flex items-baseline gap-1">
|
|
248
|
+
<input
|
|
249
|
+
type="number"
|
|
250
|
+
value={Math.round(metrics.fte_equivalent * 100)}
|
|
251
|
+
onChange={(e) => setMetrics(prev => ({
|
|
252
|
+
...prev,
|
|
253
|
+
fte_equivalent: (parseFloat(e.target.value) || 0) / 100
|
|
254
|
+
}))}
|
|
255
|
+
className="w-16 px-2 py-1 text-2xl font-bold border border-border rounded-sm focus:outline-none focus:ring-2 focus:ring-[var(--cyan)] bg-background"
|
|
256
|
+
min="0"
|
|
257
|
+
max="1000"
|
|
258
|
+
step="5"
|
|
259
|
+
/>
|
|
260
|
+
<span className="text-sm text-muted-foreground">%</span>
|
|
261
|
+
</div>
|
|
262
|
+
) : (
|
|
263
|
+
<p className="text-2xl font-bold">
|
|
264
|
+
{Math.round(metrics.fte_equivalent * 100)}<span className="text-sm font-normal text-muted-foreground ml-1">%</span>
|
|
265
|
+
</p>
|
|
266
|
+
)}
|
|
267
|
+
<p className="text-xs text-muted-foreground mt-0.5">{hoursSavedPerYear}h/year</p>
|
|
291
268
|
</div>
|
|
292
269
|
|
|
293
270
|
{/* Net Annual Savings */}
|
|
294
|
-
<div
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
<p className={cn("text-2xl font-bold", netAnnualSavings >= 0 ? "text-[var(--cyan)]" : "text-red-500")}>
|
|
303
|
-
€{netAnnualSavings.toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
|
304
|
-
</p>
|
|
305
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
306
|
-
€{laborSavingsPerYear.toLocaleString(undefined, { maximumFractionDigits: 0 })} labor − €{workerCostPerYear} worker
|
|
307
|
-
</p>
|
|
308
|
-
</div>
|
|
271
|
+
<div>
|
|
272
|
+
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-1">Net Annual Savings</p>
|
|
273
|
+
<p className={cn("text-2xl font-bold", netAnnualSavings >= 0 ? "text-[var(--cyan)]" : "text-red-500")}>
|
|
274
|
+
€{netAnnualSavings.toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
|
275
|
+
</p>
|
|
276
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
277
|
+
€{laborSavingsPerYear.toLocaleString(undefined, { maximumFractionDigits: 0 })} − €{workerCostPerYear}
|
|
278
|
+
</p>
|
|
309
279
|
</div>
|
|
310
280
|
</div>
|
|
281
|
+
|
|
282
|
+
{/* Implied frequency indicator */}
|
|
283
|
+
{impliedFrequencyPerYear > 0 && (
|
|
284
|
+
<p className="text-xs text-muted-foreground mt-4 pt-3 border-t border-border/50">
|
|
285
|
+
Implied: ~{impliedFrequencyPerMonth}×/month ({impliedFrequencyPerYear}×/year)
|
|
286
|
+
</p>
|
|
287
|
+
)}
|
|
311
288
|
</CardContent>
|
|
312
289
|
</Card>
|
|
313
290
|
)
|