Print Log Update

This commit is contained in:
2025-12-25 17:11:15 +00:00
parent 920f892ca7
commit 8142b22b21
4 changed files with 487 additions and 294 deletions

View File

@@ -17,6 +17,12 @@
let isSubmitting = $state(false); let isSubmitting = $state(false);
let editStatus = $state("Success"); let editStatus = $state("Success");
// Derived values for hours/minutes from print.duration_minutes
let durationHours = $derived(
print ? Math.floor(print.duration_minutes / 60) : 0,
);
let durationMins = $derived(print ? print.duration_minutes % 60 : 0);
// Update editStatus when print changes // Update editStatus when print changes
$effect(() => { $effect(() => {
if (print) { if (print) {
@@ -46,7 +52,20 @@
<form <form
method="POST" method="POST"
action="?/edit" action="?/edit"
use:enhance={() => { use:enhance={({ formData }) => {
// Convert hours + minutes to total minutes
const hours = Number(formData.get("duration_hours") || 0);
const mins = Number(formData.get("duration_mins") || 0);
formData.set("duration_minutes", String(hours * 60 + mins));
// Convert elapsed hours + minutes to total minutes
const elapsedHours = Number(formData.get("elapsed_hours") || 0);
const elapsedMins = Number(formData.get("elapsed_mins") || 0);
formData.set(
"elapsed_minutes",
String(elapsedHours * 60 + elapsedMins),
);
isSubmitting = true; isSubmitting = true;
return async ({ update }) => { return async ({ update }) => {
await update(); await update();
@@ -144,7 +163,8 @@
<option <option
value={p._id} value={p._id}
selected={print.printer_id?._id === p._id || selected={print.printer_id?._id === p._id ||
print.printer_id === p._id}>{p.name}</option print.printer_id === p._id}
>{p.name}</option
> >
{/each} {/each}
</select> </select>
@@ -164,33 +184,99 @@
value={s._id} value={s._id}
selected={print.spool_id?._id === s._id || selected={print.spool_id?._id === s._id ||
print.spool_id === s._id} print.spool_id === s._id}
>{s.brand} {s.material} ({s.weight_remaining_g}g left)</option >{s.brand}
{s.material} ({s.weight_remaining_g}g left)</option
> >
{/each} {/each}
</select> </select>
</div> </div>
</div> </div>
<div class="p-3 rounded-lg bg-blue-500/10 border border-blue-500/20"> <div
class="p-3 rounded-lg bg-blue-500/10 border border-blue-500/20"
>
<p class="text-xs text-blue-300 mb-3"> <p class="text-xs text-blue-300 mb-3">
<Icon icon="mdi:information" class="w-4 h-4 inline mr-1" /> <Icon
icon="mdi:information"
class="w-4 h-4 inline mr-1"
/>
Update the total print time and how long it's been running. Update the total print time and how long it's been running.
</p> </p>
<div class="grid grid-cols-2 gap-4"> <div class="space-y-3">
<Input <div class="space-y-2">
label="Total Duration (min)" <!-- svelte-ignore a11y_label_has_associated_control -->
name="duration_minutes" <label
class="block text-xs font-medium text-slate-400 uppercase tracking-wider"
>Total Duration</label
>
<div class="grid grid-cols-2 gap-2">
<div class="relative">
<input
type="number" type="number"
value={print.duration_minutes} name="duration_hours"
required value={durationHours}
min="0"
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-8"
/> />
<Input <span
label="Already Elapsed (min)" class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500"
name="elapsed_minutes" >hr</span
>
</div>
<div class="relative">
<input
type="number" type="number"
name="duration_mins"
value={durationMins}
min="0"
max="59"
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-10"
/>
<span
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500"
>min</span
>
</div>
</div>
</div>
<div class="space-y-2">
<!-- svelte-ignore a11y_label_has_associated_control -->
<label
class="block text-xs font-medium text-slate-400 uppercase tracking-wider"
>Already Elapsed</label
>
<div class="grid grid-cols-2 gap-2">
<div class="relative">
<input
type="number"
name="elapsed_hours"
placeholder="0" placeholder="0"
min="0"
value="0" value="0"
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-8"
/> />
<span
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500"
>hr</span
>
</div>
<div class="relative">
<input
type="number"
name="elapsed_mins"
placeholder="0"
min="0"
max="59"
value="0"
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-10"
/>
<span
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500"
>min</span
>
</div>
</div>
</div>
</div> </div>
<div class="grid grid-cols-2 gap-4 mt-3"> <div class="grid grid-cols-2 gap-4 mt-3">
<Input <Input
@@ -205,20 +291,50 @@
name="manual_cost" name="manual_cost"
type="number" type="number"
step="0.01" step="0.01"
value={print.calculated_cost_filament} placeholder="Auto"
/> />
</div> </div>
</div> </div>
{:else} {:else}
<!-- Completed print fields --> <!-- Completed print fields -->
<div class="grid grid-cols-3 gap-4"> <div class="space-y-4">
<Input <div class="space-y-2">
label="Duration (min)" <!-- svelte-ignore a11y_label_has_associated_control -->
name="duration_minutes" <label
class="block text-xs font-medium text-slate-400 uppercase tracking-wider"
>Duration</label
>
<div class="grid grid-cols-2 gap-2">
<div class="relative">
<input
type="number" type="number"
value={print.duration_minutes} name="duration_hours"
required value={durationHours}
min="0"
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-8"
/> />
<span
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500"
>hr</span
>
</div>
<div class="relative">
<input
type="number"
name="duration_mins"
value={durationMins}
min="0"
max="59"
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-10"
/>
<span
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500"
>min</span
>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<Input <Input
label="Used (g)" label="Used (g)"
name="filament_used_g" name="filament_used_g"
@@ -231,9 +347,10 @@
name="manual_cost" name="manual_cost"
type="number" type="number"
step="0.01" step="0.01"
value={print.calculated_cost_filament} placeholder="Auto"
/> />
</div> </div>
</div>
{/if} {/if}
<div class="pt-4 flex justify-between"> <div class="pt-4 flex justify-between">

View File

@@ -26,7 +26,17 @@
<form <form
method="POST" method="POST"
action="?/log" action="?/log"
use:enhance={() => { use:enhance={({ formData }) => {
// Convert hours + minutes to total minutes
const hours = Number(formData.get('duration_hours') || 0);
const mins = Number(formData.get('duration_mins') || 0);
formData.set('duration_minutes', String(hours * 60 + mins));
// Convert elapsed hours + minutes to total minutes
const elapsedHours = Number(formData.get('elapsed_hours') || 0);
const elapsedMins = Number(formData.get('elapsed_mins') || 0);
formData.set('elapsed_minutes', String(elapsedHours * 60 + elapsedMins));
isSubmitting = true; isSubmitting = true;
return async ({ update }) => { return async ({ update }) => {
await update(); await update();
@@ -152,21 +162,35 @@
<Icon icon="mdi:information" class="w-4 h-4 inline mr-1" /> <Icon icon="mdi:information" class="w-4 h-4 inline mr-1" />
Enter the expected total print time and how long it's been running. Enter the expected total print time and how long it's been running.
</p> </p>
<div class="grid grid-cols-2 gap-4"> <div class="space-y-3">
<Input <div class="space-y-2">
label="Total Duration (min)" <!-- svelte-ignore a11y_label_has_associated_control -->
name="duration_minutes" <label class="block text-xs font-medium text-slate-400 uppercase tracking-wider">Total Duration</label>
type="number" <div class="grid grid-cols-2 gap-2">
placeholder="120" <div class="relative">
required <input type="number" name="duration_hours" placeholder="2" min="0" class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-8" />
/> <span class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500">hr</span>
<Input </div>
label="Already Elapsed (min)" <div class="relative">
name="elapsed_minutes" <input type="number" name="duration_mins" placeholder="30" min="0" max="59" class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-10" />
type="number" <span class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500">min</span>
placeholder="0" </div>
value="0" </div>
/> </div>
<div class="space-y-2">
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="block text-xs font-medium text-slate-400 uppercase tracking-wider">Already Elapsed</label>
<div class="grid grid-cols-2 gap-2">
<div class="relative">
<input type="number" name="elapsed_hours" placeholder="0" min="0" value="0" class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-8" />
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500">hr</span>
</div>
<div class="relative">
<input type="number" name="elapsed_mins" placeholder="0" min="0" max="59" value="0" class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-10" />
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500">min</span>
</div>
</div>
</div>
</div> </div>
<div class="grid grid-cols-2 gap-4 mt-3"> <div class="grid grid-cols-2 gap-4 mt-3">
<Input <Input
@@ -187,14 +211,22 @@
</div> </div>
{:else} {:else}
<!-- Completed print fields --> <!-- Completed print fields -->
<div class="grid grid-cols-3 gap-4"> <div class="space-y-4">
<Input <div class="space-y-2">
label="Duration (min)" <!-- svelte-ignore a11y_label_has_associated_control -->
name="duration_minutes" <label class="block text-xs font-medium text-slate-400 uppercase tracking-wider">Duration</label>
type="number" <div class="grid grid-cols-2 gap-2">
placeholder="60" <div class="relative">
required <input type="number" name="duration_hours" placeholder="2" min="0" class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-8" />
/> <span class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500">hr</span>
</div>
<div class="relative">
<input type="number" name="duration_mins" placeholder="30" min="0" max="59" class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-10" />
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500">min</span>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<Input <Input
label="Used (g)" label="Used (g)"
name="filament_used_g" name="filament_used_g"
@@ -210,6 +242,7 @@
placeholder="Auto" placeholder="Auto"
/> />
</div> </div>
</div>
{/if} {/if}
<div class="pt-4 flex justify-end gap-3"> <div class="pt-4 flex justify-end gap-3">

View File

@@ -1,6 +1,7 @@
import { PrintJob } from '$lib/models/PrintJob'; import { PrintJob } from '$lib/models/PrintJob';
import { Spool } from '$lib/models/Spool'; import { Spool } from '$lib/models/Spool';
import { Printer } from '$lib/models/Printer'; import { Printer } from '$lib/models/Printer';
import { User } from '$lib/models/User';
import { connectDB } from '$lib/server/db'; import { connectDB } from '$lib/server/db';
import type { PageServerLoad, Actions } from './$types'; import type { PageServerLoad, Actions } from './$types';
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
@@ -59,16 +60,35 @@ export const actions: Actions = {
const printer = await Printer.findOne({ _id: printer_id, user_id: locals.user.id }); const printer = await Printer.findOne({ _id: printer_id, user_id: locals.user.id });
if (!printer) return fail(404, { printerNotFound: true }); if (!printer) return fail(404, { printerNotFound: true });
const weightUsed = Number(filament_used_g); // Get user's electricity rate
const user = await User.findById(locals.user.id);
const electricityRate = user?.electricity_rate || 0.12; // Default $/kWh
// Calculate Cost: use manual if provided, otherwise calculate const weightUsed = Number(filament_used_g);
const durationMins = Number(duration_minutes);
// Calculate Filament Cost: use manual if provided, otherwise calculate
let costFilament: number; let costFilament: number;
if (manual_cost && String(manual_cost).trim() !== '') { if (manual_cost && String(manual_cost).trim() !== '') {
// If manual cost provided, it's the total cost (filament + electricity)
costFilament = Number(manual_cost); costFilament = Number(manual_cost);
} else { } else {
costFilament = (spool.price / spool.weight_initial_g) * weightUsed; costFilament = (spool.price / spool.weight_initial_g) * weightUsed;
} }
// Calculate Electricity Cost: Power (kW) × Duration (hours) × Rate ($/kWh)
const powerKw = (printer.power_consumption_watts || 0) / 1000;
const durationHours = durationMins / 60;
const costEnergy = powerKw * durationHours * electricityRate;
// Total cost = filament + electricity (only if not manual)
let totalCost: number;
if (manual_cost && String(manual_cost).trim() !== '') {
totalCost = Number(manual_cost);
} else {
totalCost = costFilament + costEnergy;
}
// 2. Create Print Job // 2. Create Print Job
const isInProgress = status === 'In Progress'; const isInProgress = status === 'In Progress';
@@ -84,9 +104,10 @@ export const actions: Actions = {
name: name || 'Untitled Print', name: name || 'Untitled Print',
spool_id, spool_id,
printer_id, printer_id,
duration_minutes: Number(duration_minutes), duration_minutes: durationMins,
filament_used_g: weightUsed, filament_used_g: weightUsed,
calculated_cost_filament: Number(costFilament.toFixed(2)), calculated_cost_filament: Number(totalCost.toFixed(2)),
calculated_cost_energy: Number(costEnergy.toFixed(2)),
status, status,
started_at: startedAt, started_at: startedAt,
date: new Date() date: new Date()
@@ -126,19 +147,43 @@ export const actions: Actions = {
await connectDB(); await connectDB();
try { try {
const printJob = await PrintJob.findOne({ _id: id, user_id: locals.user.id }).populate('spool_id'); const printJob = await PrintJob.findOne({ _id: id, user_id: locals.user.id }).populate('spool_id').populate('printer_id');
if (!printJob) return fail(404, { notFound: true }); if (!printJob) return fail(404, { notFound: true });
const weightUsed = Number(filament_used_g); // Get user's electricity rate
const user = await User.findById(locals.user.id);
const electricityRate = user?.electricity_rate || 0.12;
// Calculate Cost: use manual if provided, otherwise calculate const weightUsed = Number(filament_used_g);
const durationMins = Number(duration_minutes);
// Get printer for power calculation
const printerForCalc = printer_id
? await Printer.findById(printer_id)
: printJob.printer_id;
// Calculate Filament Cost: use manual if provided, otherwise calculate
let costFilament: number; let costFilament: number;
if (manual_cost && String(manual_cost).trim() !== '') { if (manual_cost && String(manual_cost).trim() !== '') {
// Manual cost is the total, we'll calculate energy separately for tracking
costFilament = Number(manual_cost); costFilament = Number(manual_cost);
} else if (printJob.spool_id?.price && printJob.spool_id?.weight_initial_g) { } else if (printJob.spool_id?.price && printJob.spool_id?.weight_initial_g) {
costFilament = (printJob.spool_id.price / printJob.spool_id.weight_initial_g) * weightUsed; costFilament = (printJob.spool_id.price / printJob.spool_id.weight_initial_g) * weightUsed;
} else { } else {
costFilament = printJob.calculated_cost_filament || 0; costFilament = 0;
}
// Calculate Electricity Cost
const powerKw = (printerForCalc?.power_consumption_watts || 0) / 1000;
const durationHours = durationMins / 60;
const costEnergy = powerKw * durationHours * electricityRate;
// Total cost
let totalCost: number;
if (manual_cost && String(manual_cost).trim() !== '') {
totalCost = Number(manual_cost);
} else {
totalCost = costFilament + costEnergy;
} }
// Calculate started_at based on elapsed time for In Progress // Calculate started_at based on elapsed time for In Progress
@@ -155,9 +200,10 @@ export const actions: Actions = {
// Build update object // Build update object
const updateData: any = { const updateData: any = {
name, name,
duration_minutes: Number(duration_minutes), duration_minutes: durationMins,
filament_used_g: weightUsed, filament_used_g: weightUsed,
calculated_cost_filament: Number(costFilament.toFixed(2)), calculated_cost_filament: Number(totalCost.toFixed(2)),
calculated_cost_energy: Number(costEnergy.toFixed(2)),
status, status,
started_at: startedAt started_at: startedAt
}; };

View File

@@ -5,10 +5,7 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = { const config = {
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
adapter: adapter(), adapter: adapter()
csrf: {
checkOrigin: true
}
} }
}; };