Initial Code Commit
This commit is contained in:
203
src/routes/prints/+page.server.ts
Normal file
203
src/routes/prints/+page.server.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { PrintJob } from '$lib/models/PrintJob';
|
||||
import { Spool } from '$lib/models/Spool';
|
||||
import { Printer } from '$lib/models/Printer';
|
||||
import { connectDB } from '$lib/server/db';
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
if (!locals.user) throw redirect(303, '/login');
|
||||
|
||||
await connectDB();
|
||||
|
||||
// Fetch prints with populated fields (Spool and Printer) - filtered by user
|
||||
const prints = await PrintJob.find({ user_id: locals.user.id })
|
||||
.populate('spool_id', 'brand material color_hex')
|
||||
.populate('printer_id', 'name model')
|
||||
.sort({ date: -1 })
|
||||
.lean();
|
||||
|
||||
// Fetch active spools and printers for the "Log Print" form - filtered by user
|
||||
const [spools, printers] = await Promise.all([
|
||||
Spool.find({ user_id: locals.user.id, is_active: true }).lean(),
|
||||
Printer.find({ user_id: locals.user.id }).lean()
|
||||
]);
|
||||
|
||||
return {
|
||||
prints: JSON.parse(JSON.stringify(prints)),
|
||||
spools: JSON.parse(JSON.stringify(spools)),
|
||||
printers: JSON.parse(JSON.stringify(printers))
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
log: async ({ request, locals }) => {
|
||||
if (!locals.user) return fail(401, { unauthorized: true });
|
||||
|
||||
const formData = await request.formData();
|
||||
const name = formData.get('name');
|
||||
const spool_id = formData.get('spool_id');
|
||||
const printer_id = formData.get('printer_id');
|
||||
const duration_minutes = formData.get('duration_minutes');
|
||||
const filament_used_g = formData.get('filament_used_g');
|
||||
const status = formData.get('status');
|
||||
const manual_cost = formData.get('manual_cost');
|
||||
const elapsed_minutes = formData.get('elapsed_minutes');
|
||||
|
||||
if (!spool_id || !printer_id || !filament_used_g) {
|
||||
return fail(400, { missing: true });
|
||||
}
|
||||
|
||||
await connectDB();
|
||||
|
||||
try {
|
||||
// 1. Get the spool to calculate cost and deduct weight
|
||||
const spool = await Spool.findOne({ _id: spool_id, user_id: locals.user.id });
|
||||
if (!spool) return fail(404, { spoolNotFound: true });
|
||||
|
||||
// Verify printer belongs to user
|
||||
const printer = await Printer.findOne({ _id: printer_id, user_id: locals.user.id });
|
||||
if (!printer) return fail(404, { printerNotFound: true });
|
||||
|
||||
const weightUsed = Number(filament_used_g);
|
||||
|
||||
// Calculate Cost: use manual if provided, otherwise calculate
|
||||
let costFilament: number;
|
||||
if (manual_cost && String(manual_cost).trim() !== '') {
|
||||
costFilament = Number(manual_cost);
|
||||
} else {
|
||||
costFilament = (spool.price / spool.weight_initial_g) * weightUsed;
|
||||
}
|
||||
|
||||
// 2. Create Print Job
|
||||
const isInProgress = status === 'In Progress';
|
||||
|
||||
// Calculate started_at based on elapsed time
|
||||
let startedAt: Date | null = null;
|
||||
if (isInProgress) {
|
||||
const elapsedMs = Number(elapsed_minutes || 0) * 60 * 1000;
|
||||
startedAt = new Date(Date.now() - elapsedMs);
|
||||
}
|
||||
|
||||
await PrintJob.create({
|
||||
user_id: locals.user.id,
|
||||
name: name || 'Untitled Print',
|
||||
spool_id,
|
||||
printer_id,
|
||||
duration_minutes: Number(duration_minutes),
|
||||
filament_used_g: weightUsed,
|
||||
calculated_cost_filament: Number(costFilament.toFixed(2)),
|
||||
status,
|
||||
started_at: startedAt,
|
||||
date: new Date()
|
||||
});
|
||||
|
||||
// 3. Deduct Filament from Spool (only if not In Progress)
|
||||
if (!isInProgress) {
|
||||
spool.weight_remaining_g = Math.max(0, spool.weight_remaining_g - weightUsed);
|
||||
await spool.save();
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return fail(500, { dbError: true });
|
||||
}
|
||||
},
|
||||
|
||||
edit: async ({ request, locals }) => {
|
||||
if (!locals.user) return fail(401, { unauthorized: true });
|
||||
|
||||
const formData = await request.formData();
|
||||
const id = formData.get('id');
|
||||
const name = formData.get('name');
|
||||
const duration_minutes = formData.get('duration_minutes');
|
||||
const filament_used_g = formData.get('filament_used_g');
|
||||
const status = formData.get('status');
|
||||
const manual_cost = formData.get('manual_cost');
|
||||
const elapsed_minutes = formData.get('elapsed_minutes');
|
||||
const printer_id = formData.get('printer_id');
|
||||
const spool_id = formData.get('spool_id');
|
||||
|
||||
if (!id || !name) {
|
||||
return fail(400, { missing: true });
|
||||
}
|
||||
|
||||
await connectDB();
|
||||
|
||||
try {
|
||||
const printJob = await PrintJob.findOne({ _id: id, user_id: locals.user.id }).populate('spool_id');
|
||||
if (!printJob) return fail(404, { notFound: true });
|
||||
|
||||
const weightUsed = Number(filament_used_g);
|
||||
|
||||
// Calculate Cost: use manual if provided, otherwise calculate
|
||||
let costFilament: number;
|
||||
if (manual_cost && String(manual_cost).trim() !== '') {
|
||||
costFilament = Number(manual_cost);
|
||||
} else if (printJob.spool_id?.price && printJob.spool_id?.weight_initial_g) {
|
||||
costFilament = (printJob.spool_id.price / printJob.spool_id.weight_initial_g) * weightUsed;
|
||||
} else {
|
||||
costFilament = printJob.calculated_cost_filament || 0;
|
||||
}
|
||||
|
||||
// Calculate started_at based on elapsed time for In Progress
|
||||
const isInProgress = status === 'In Progress';
|
||||
let startedAt = printJob.started_at;
|
||||
|
||||
if (isInProgress && elapsed_minutes) {
|
||||
const elapsedMs = Number(elapsed_minutes) * 60 * 1000;
|
||||
startedAt = new Date(Date.now() - elapsedMs);
|
||||
} else if (!isInProgress) {
|
||||
startedAt = null;
|
||||
}
|
||||
|
||||
// Build update object
|
||||
const updateData: any = {
|
||||
name,
|
||||
duration_minutes: Number(duration_minutes),
|
||||
filament_used_g: weightUsed,
|
||||
calculated_cost_filament: Number(costFilament.toFixed(2)),
|
||||
status,
|
||||
started_at: startedAt
|
||||
};
|
||||
|
||||
// Update printer/spool if provided (for In Progress)
|
||||
if (printer_id) {
|
||||
updateData.printer_id = printer_id;
|
||||
}
|
||||
if (spool_id) {
|
||||
updateData.spool_id = spool_id;
|
||||
}
|
||||
|
||||
await PrintJob.findOneAndUpdate(
|
||||
{ _id: id, user_id: locals.user.id },
|
||||
updateData
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return fail(500, { dbError: true });
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ request, locals }) => {
|
||||
if (!locals.user) return fail(401, { unauthorized: true });
|
||||
|
||||
const formData = await request.formData();
|
||||
const id = formData.get('id');
|
||||
|
||||
if (!id) return fail(400, { missing: true });
|
||||
|
||||
await connectDB();
|
||||
|
||||
try {
|
||||
await PrintJob.findOneAndDelete({ _id: id, user_id: locals.user.id });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return fail(500, { dbError: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
177
src/routes/prints/+page.svelte
Normal file
177
src/routes/prints/+page.svelte
Normal file
@@ -0,0 +1,177 @@
|
||||
<script lang="ts">
|
||||
import Button from "$lib/components/ui/Button.svelte";
|
||||
import Card from "$lib/components/ui/Card.svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
import LogPrintModal from "$lib/components/prints/LogPrintModal.svelte";
|
||||
import EditPrintModal from "$lib/components/prints/EditPrintModal.svelte";
|
||||
|
||||
let { data } = $props();
|
||||
let showLogModal = $state(false);
|
||||
let showEditModal = $state(false);
|
||||
let editingPrint: any = $state(null);
|
||||
|
||||
// svelte-ignore non_reactive_update
|
||||
let prints = $derived(data.prints || []);
|
||||
// svelte-ignore non_reactive_update
|
||||
let spools = $derived(data.spools || []);
|
||||
// svelte-ignore non_reactive_update
|
||||
let printers = $derived(data.printers || []);
|
||||
|
||||
function openEditModal(print: any) {
|
||||
editingPrint = { ...print };
|
||||
showEditModal = true;
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
showEditModal = false;
|
||||
editingPrint = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6 fade-in">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white">Print History</h1>
|
||||
<p class="text-slate-400 mt-1">
|
||||
Track your usage and successful prints
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => (showLogModal = true)}>
|
||||
<Icon icon="mdi:plus" class="w-4 h-4 mr-2" /> Log Print
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
{#each prints as print}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div onclick={() => openEditModal(print)} class="cursor-pointer">
|
||||
<Card
|
||||
class="flex flex-col md:flex-row items-start md:items-center gap-4 hover:bg-slate-800/80 transition-colors"
|
||||
>
|
||||
<!-- Status Icon -->
|
||||
<div
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center shrink-0
|
||||
{print.status === 'Success'
|
||||
? 'bg-green-500/10 text-green-400'
|
||||
: print.status === 'Fail'
|
||||
? 'bg-red-500/10 text-red-400'
|
||||
: print.status === 'In Progress'
|
||||
? 'bg-blue-500/10 text-blue-400 animate-pulse'
|
||||
: 'bg-slate-500/10 text-slate-400'}"
|
||||
>
|
||||
{#if print.status === "Success"}
|
||||
<Icon icon="mdi:check-circle" class="w-5 h-5" />
|
||||
{:else if print.status === "Fail"}
|
||||
<Icon icon="mdi:close-circle" class="w-5 h-5" />
|
||||
{:else if print.status === "In Progress"}
|
||||
<Icon
|
||||
icon="mdi:loading"
|
||||
class="w-5 h-5 animate-spin"
|
||||
/>
|
||||
{:else}
|
||||
<Icon icon="mdi:cancel" class="w-5 h-5" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-white font-medium truncate">
|
||||
{print.name}
|
||||
</h3>
|
||||
<div
|
||||
class="flex flex-wrap gap-x-4 gap-y-1 mt-1 text-xs text-slate-400"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<span
|
||||
class="w-2 h-2 rounded-full mr-1.5"
|
||||
style="background-color: {print.spool_id
|
||||
?.color_hex}"
|
||||
></span>
|
||||
{print.spool_id?.brand || "Unknown Spool"}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span
|
||||
>{print.printer_id?.name ||
|
||||
"Unknown Printer"}</span
|
||||
>
|
||||
<span>•</span>
|
||||
<span
|
||||
>{new Date(
|
||||
print.date,
|
||||
).toLocaleDateString()}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-6 text-sm text-slate-400">
|
||||
<div class="text-right">
|
||||
<p
|
||||
class="text-xs uppercase tracking-wider text-slate-500"
|
||||
>
|
||||
Duration
|
||||
</p>
|
||||
<p class="text-white">{print.duration_minutes}m</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p
|
||||
class="text-xs uppercase tracking-wider text-slate-500"
|
||||
>
|
||||
Weight
|
||||
</p>
|
||||
<p class="text-white">{print.filament_used_g}g</p>
|
||||
</div>
|
||||
<div class="text-right min-w-[60px]">
|
||||
<p
|
||||
class="text-xs uppercase tracking-wider text-slate-500"
|
||||
>
|
||||
Cost
|
||||
</p>
|
||||
<p class="text-white font-medium">
|
||||
${print.calculated_cost_filament?.toFixed(2) ||
|
||||
"0.00"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if prints.length === 0}
|
||||
<div class="text-center py-12 text-slate-500">
|
||||
No prints logged yet. Start printing!
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modals -->
|
||||
<LogPrintModal
|
||||
open={showLogModal}
|
||||
{printers}
|
||||
{spools}
|
||||
onclose={() => (showLogModal = false)}
|
||||
/>
|
||||
|
||||
<EditPrintModal
|
||||
open={showEditModal}
|
||||
print={editingPrint}
|
||||
{printers}
|
||||
{spools}
|
||||
onclose={closeEditModal}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.fade-in {
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user