Print Log Update
This commit is contained in:
@@ -1,259 +1,376 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from "$app/forms";
|
import { enhance } from "$app/forms";
|
||||||
import Button from "$lib/components/ui/Button.svelte";
|
import Button from "$lib/components/ui/Button.svelte";
|
||||||
import Modal from "$lib/components/ui/Modal.svelte";
|
import Modal from "$lib/components/ui/Modal.svelte";
|
||||||
import Input from "$lib/components/ui/Input.svelte";
|
import Input from "$lib/components/ui/Input.svelte";
|
||||||
import Icon from "@iconify/svelte";
|
import Icon from "@iconify/svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
print: any;
|
print: any;
|
||||||
printers: any[];
|
printers: any[];
|
||||||
spools: any[];
|
spools: any[];
|
||||||
onclose: () => void;
|
onclose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { open, print, printers, spools, onclose }: Props = $props();
|
let { open, print, printers, spools, onclose }: Props = $props();
|
||||||
let isSubmitting = $state(false);
|
let isSubmitting = $state(false);
|
||||||
let editStatus = $state("Success");
|
let editStatus = $state("Success");
|
||||||
|
|
||||||
// Update editStatus when print changes
|
// Derived values for hours/minutes from print.duration_minutes
|
||||||
$effect(() => {
|
let durationHours = $derived(
|
||||||
if (print) {
|
print ? Math.floor(print.duration_minutes / 60) : 0,
|
||||||
editStatus = print.status || "Success";
|
);
|
||||||
}
|
let durationMins = $derived(print ? print.duration_minutes % 60 : 0);
|
||||||
});
|
|
||||||
|
|
||||||
function handleClose() {
|
// Update editStatus when print changes
|
||||||
editStatus = "Success";
|
$effect(() => {
|
||||||
onclose();
|
if (print) {
|
||||||
}
|
editStatus = print.status || "Success";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function handleDelete() {
|
function handleClose() {
|
||||||
if (!confirm("Are you sure you want to delete this print log?")) return;
|
editStatus = "Success";
|
||||||
isSubmitting = true;
|
onclose();
|
||||||
const formData = new FormData();
|
}
|
||||||
formData.append("id", print._id);
|
|
||||||
await fetch("?/delete", { method: "POST", body: formData });
|
async function handleDelete() {
|
||||||
isSubmitting = false;
|
if (!confirm("Are you sure you want to delete this print log?")) return;
|
||||||
handleClose();
|
isSubmitting = true;
|
||||||
window.location.reload();
|
const formData = new FormData();
|
||||||
}
|
formData.append("id", print._id);
|
||||||
|
await fetch("?/delete", { method: "POST", body: formData });
|
||||||
|
isSubmitting = false;
|
||||||
|
handleClose();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal title="Edit Print Log" {open} onclose={handleClose}>
|
<Modal title="Edit Print Log" {open} onclose={handleClose}>
|
||||||
{#if print}
|
{#if print}
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/edit"
|
action="?/edit"
|
||||||
use:enhance={() => {
|
use:enhance={({ formData }) => {
|
||||||
isSubmitting = true;
|
// Convert hours + minutes to total minutes
|
||||||
return async ({ update }) => {
|
const hours = Number(formData.get("duration_hours") || 0);
|
||||||
await update();
|
const mins = Number(formData.get("duration_mins") || 0);
|
||||||
isSubmitting = false;
|
formData.set("duration_minutes", String(hours * 60 + mins));
|
||||||
handleClose();
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
class="space-y-4"
|
|
||||||
>
|
|
||||||
<input type="hidden" name="id" value={print._id} />
|
|
||||||
|
|
||||||
<!-- Status Selection -->
|
// Convert elapsed hours + minutes to total minutes
|
||||||
<div class="space-y-2">
|
const elapsedHours = Number(formData.get("elapsed_hours") || 0);
|
||||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
const elapsedMins = Number(formData.get("elapsed_mins") || 0);
|
||||||
<label
|
formData.set(
|
||||||
class="block text-xs font-medium text-slate-400 uppercase tracking-wider"
|
"elapsed_minutes",
|
||||||
>Status</label
|
String(elapsedHours * 60 + elapsedMins),
|
||||||
>
|
);
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<label class="cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="status"
|
|
||||||
value="In Progress"
|
|
||||||
class="peer sr-only"
|
|
||||||
bind:group={editStatus}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-center gap-2 py-2 rounded-lg border border-slate-700 text-slate-400 peer-checked:bg-blue-500/20 peer-checked:text-blue-400 peer-checked:border-blue-500/50 transition-all"
|
|
||||||
>
|
|
||||||
<Icon icon="mdi:printer-3d" class="w-4 h-4" /> In Progress
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label class="cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="status"
|
|
||||||
value="Success"
|
|
||||||
class="peer sr-only"
|
|
||||||
bind:group={editStatus}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-center gap-2 py-2 rounded-lg border border-slate-700 text-slate-400 peer-checked:bg-green-500/20 peer-checked:text-green-400 peer-checked:border-green-500/50 transition-all"
|
|
||||||
>
|
|
||||||
<Icon icon="mdi:check" class="w-4 h-4" /> Success
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label class="cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="status"
|
|
||||||
value="Fail"
|
|
||||||
class="peer sr-only"
|
|
||||||
bind:group={editStatus}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-center gap-2 py-2 rounded-lg border border-slate-700 text-slate-400 peer-checked:bg-red-500/20 peer-checked:text-red-400 peer-checked:border-red-500/50 transition-all"
|
|
||||||
>
|
|
||||||
<Icon icon="mdi:close" class="w-4 h-4" /> Fail
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label class="cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="status"
|
|
||||||
value="Cancelled"
|
|
||||||
class="peer sr-only"
|
|
||||||
bind:group={editStatus}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-center gap-2 py-2 rounded-lg border border-slate-700 text-slate-400 peer-checked:bg-slate-600/20 peer-checked:text-slate-300 peer-checked:border-slate-500/50 transition-all"
|
|
||||||
>
|
|
||||||
<Icon icon="mdi:cancel" class="w-4 h-4" /> Cancelled
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Input label="Print Name" name="name" value={print.name} required />
|
isSubmitting = true;
|
||||||
|
return async ({ update }) => {
|
||||||
|
await update();
|
||||||
|
isSubmitting = false;
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="id" value={print._id} />
|
||||||
|
|
||||||
{#if editStatus === "In Progress"}
|
<!-- Status Selection -->
|
||||||
<!-- In Progress specific fields -->
|
<div class="space-y-2">
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||||
<div class="space-y-2">
|
<label
|
||||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
class="block text-xs font-medium text-slate-400 uppercase tracking-wider"
|
||||||
<label
|
>Status</label
|
||||||
class="block text-xs font-medium text-slate-400 uppercase tracking-wider"
|
>
|
||||||
>Printer</label
|
<div class="grid grid-cols-2 gap-2">
|
||||||
>
|
<label class="cursor-pointer">
|
||||||
<select
|
<input
|
||||||
name="printer_id"
|
type="radio"
|
||||||
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"
|
name="status"
|
||||||
>
|
value="In Progress"
|
||||||
{#each printers as p}
|
class="peer sr-only"
|
||||||
<option
|
bind:group={editStatus}
|
||||||
value={p._id}
|
/>
|
||||||
selected={print.printer_id?._id === p._id ||
|
<div
|
||||||
print.printer_id === p._id}>{p.name}</option
|
class="flex items-center justify-center gap-2 py-2 rounded-lg border border-slate-700 text-slate-400 peer-checked:bg-blue-500/20 peer-checked:text-blue-400 peer-checked:border-blue-500/50 transition-all"
|
||||||
>
|
>
|
||||||
{/each}
|
<Icon icon="mdi:printer-3d" class="w-4 h-4" /> In Progress
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</label>
|
||||||
<div class="space-y-2">
|
<label class="cursor-pointer">
|
||||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
<input
|
||||||
<label
|
type="radio"
|
||||||
class="block text-xs font-medium text-slate-400 uppercase tracking-wider"
|
name="status"
|
||||||
>Spool</label
|
value="Success"
|
||||||
>
|
class="peer sr-only"
|
||||||
<select
|
bind:group={editStatus}
|
||||||
name="spool_id"
|
/>
|
||||||
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"
|
<div
|
||||||
>
|
class="flex items-center justify-center gap-2 py-2 rounded-lg border border-slate-700 text-slate-400 peer-checked:bg-green-500/20 peer-checked:text-green-400 peer-checked:border-green-500/50 transition-all"
|
||||||
{#each spools as s}
|
>
|
||||||
<option
|
<Icon icon="mdi:check" class="w-4 h-4" /> Success
|
||||||
value={s._id}
|
</div>
|
||||||
selected={print.spool_id?._id === s._id ||
|
</label>
|
||||||
print.spool_id === s._id}
|
<label class="cursor-pointer">
|
||||||
>{s.brand} {s.material} ({s.weight_remaining_g}g left)</option
|
<input
|
||||||
>
|
type="radio"
|
||||||
{/each}
|
name="status"
|
||||||
</select>
|
value="Fail"
|
||||||
</div>
|
class="peer sr-only"
|
||||||
</div>
|
bind:group={editStatus}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center gap-2 py-2 rounded-lg border border-slate-700 text-slate-400 peer-checked:bg-red-500/20 peer-checked:text-red-400 peer-checked:border-red-500/50 transition-all"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:close" class="w-4 h-4" /> Fail
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="status"
|
||||||
|
value="Cancelled"
|
||||||
|
class="peer sr-only"
|
||||||
|
bind:group={editStatus}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center gap-2 py-2 rounded-lg border border-slate-700 text-slate-400 peer-checked:bg-slate-600/20 peer-checked:text-slate-300 peer-checked:border-slate-500/50 transition-all"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:cancel" class="w-4 h-4" /> Cancelled
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="p-3 rounded-lg bg-blue-500/10 border border-blue-500/20">
|
<Input label="Print Name" name="name" value={print.name} required />
|
||||||
<p class="text-xs text-blue-300 mb-3">
|
|
||||||
<Icon icon="mdi:information" class="w-4 h-4 inline mr-1" />
|
|
||||||
Update the total print time and how long it's been running.
|
|
||||||
</p>
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<Input
|
|
||||||
label="Total Duration (min)"
|
|
||||||
name="duration_minutes"
|
|
||||||
type="number"
|
|
||||||
value={print.duration_minutes}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="Already Elapsed (min)"
|
|
||||||
name="elapsed_minutes"
|
|
||||||
type="number"
|
|
||||||
placeholder="0"
|
|
||||||
value="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-4 mt-3">
|
|
||||||
<Input
|
|
||||||
label="Expected Filament (g)"
|
|
||||||
name="filament_used_g"
|
|
||||||
type="number"
|
|
||||||
value={print.filament_used_g}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="Cost ($)"
|
|
||||||
name="manual_cost"
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
value={print.calculated_cost_filament}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<!-- Completed print fields -->
|
|
||||||
<div class="grid grid-cols-3 gap-4">
|
|
||||||
<Input
|
|
||||||
label="Duration (min)"
|
|
||||||
name="duration_minutes"
|
|
||||||
type="number"
|
|
||||||
value={print.duration_minutes}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="Used (g)"
|
|
||||||
name="filament_used_g"
|
|
||||||
type="number"
|
|
||||||
value={print.filament_used_g}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="Cost ($)"
|
|
||||||
name="manual_cost"
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
value={print.calculated_cost_filament}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="pt-4 flex justify-between">
|
{#if editStatus === "In Progress"}
|
||||||
<Button
|
<!-- In Progress specific fields -->
|
||||||
variant="destructive"
|
<div class="grid grid-cols-2 gap-4">
|
||||||
type="button"
|
<div class="space-y-2">
|
||||||
disabled={isSubmitting}
|
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||||
onclick={handleDelete}
|
<label
|
||||||
>
|
class="block text-xs font-medium text-slate-400 uppercase tracking-wider"
|
||||||
Delete
|
>Printer</label
|
||||||
</Button>
|
>
|
||||||
<div class="flex gap-3">
|
<select
|
||||||
<Button variant="ghost" onclick={handleClose} type="button"
|
name="printer_id"
|
||||||
>Cancel</Button
|
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"
|
||||||
>
|
>
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
{#each printers as p}
|
||||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
<option
|
||||||
</Button>
|
value={p._id}
|
||||||
</div>
|
selected={print.printer_id?._id === p._id ||
|
||||||
</div>
|
print.printer_id === p._id}
|
||||||
</form>
|
>{p.name}</option
|
||||||
{/if}
|
>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</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"
|
||||||
|
>Spool</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name="spool_id"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{#each spools as s}
|
||||||
|
<option
|
||||||
|
value={s._id}
|
||||||
|
selected={print.spool_id?._id === s._id ||
|
||||||
|
print.spool_id === s._id}
|
||||||
|
>{s.brand}
|
||||||
|
{s.material} ({s.weight_remaining_g}g left)</option
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="p-3 rounded-lg bg-blue-500/10 border border-blue-500/20"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-blue-300 mb-3">
|
||||||
|
<Icon
|
||||||
|
icon="mdi:information"
|
||||||
|
class="w-4 h-4 inline mr-1"
|
||||||
|
/>
|
||||||
|
Update the total print time and how long it's been running.
|
||||||
|
</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<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"
|
||||||
|
>Total Duration</label
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="duration_hours"
|
||||||
|
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="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 class="grid grid-cols-2 gap-4 mt-3">
|
||||||
|
<Input
|
||||||
|
label="Expected Filament (g)"
|
||||||
|
name="filament_used_g"
|
||||||
|
type="number"
|
||||||
|
value={print.filament_used_g}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Cost ($)"
|
||||||
|
name="manual_cost"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="Auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Completed print fields -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<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"
|
||||||
|
>Duration</label
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="duration_hours"
|
||||||
|
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
|
||||||
|
label="Used (g)"
|
||||||
|
name="filament_used_g"
|
||||||
|
type="number"
|
||||||
|
value={print.filament_used_g}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Cost ($)"
|
||||||
|
name="manual_cost"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="Auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="pt-4 flex justify-between">
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
type="button"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onclick={handleDelete}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<Button variant="ghost" onclick={handleClose} type="button"
|
||||||
|
>Cancel</Button
|
||||||
|
>
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -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,28 +211,37 @@
|
|||||||
</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>
|
||||||
<Input
|
</div>
|
||||||
label="Used (g)"
|
<div class="relative">
|
||||||
name="filament_used_g"
|
<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="15"
|
</div>
|
||||||
required
|
</div>
|
||||||
/>
|
</div>
|
||||||
<Input
|
<div class="grid grid-cols-2 gap-4">
|
||||||
label="Cost ($)"
|
<Input
|
||||||
name="manual_cost"
|
label="Used (g)"
|
||||||
type="number"
|
name="filament_used_g"
|
||||||
step="0.01"
|
type="number"
|
||||||
placeholder="Auto"
|
placeholder="15"
|
||||||
/>
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Cost ($)"
|
||||||
|
name="manual_cost"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="Auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user