feat: complete 3d printing module files

This commit is contained in:
Marcio Bevervanso 2026-01-27 15:40:32 -03:00
parent b2fce05065
commit 100b397ede
9 changed files with 865 additions and 1 deletions

View file

@ -11,6 +11,9 @@ import Sales from './pages/Sales';
import Suppliers from './pages/Suppliers';
import Users from './pages/Users';
import Login from './pages/Login';
import Calculator from './pages/Printing/Calculator'; // 3D
import Filaments from './pages/Printing/Filaments'; // 3D
import Printers from './pages/Printing/Printers'; // 3D
import Products from './pages/Products'; // New
import Reports from './pages/Reports';
import Settings from './pages/Settings';
@ -110,6 +113,9 @@ const AppLayout: React.FC = () => {
const AppRoutes = () => (
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/printing/calculator" element={<Calculator />} />
<Route path="/printing/filaments" element={<Filaments />} />
<Route path="/printing/printers" element={<Printers />} />
<Route path="/sourcing" element={<Sourcing />} />
<Route path="/orders" element={<Orders />} />
<Route path="/sales" element={<Sales />} />

View file

@ -284,6 +284,7 @@ export const CRMProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
autoSyncSales: settingsData.auto_sync_sales,
autoSyncStock: settingsData.auto_sync_stock,
sourcingWebhook: settingsData.sourcing_webhook,
electricityCostKwh: settingsData.electricity_cost_kwh,
});
}
} catch (err) {

View file

@ -1,6 +1,7 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { LayoutDashboard, Search, History, Boxes, Store, Users, TrendingUp, LogOut, Wallet, ShoppingCart, Package, BarChart3, Settings } from 'lucide-react';
import { LayoutDashboard, Search, History, Boxes, Store, Users, TrendingUp, LogOut, Wallet, ShoppingCart, Package, BarChart3, Settings, Printer, Scroll } from 'lucide-react';
import { useCRM } from '../context/CRMContext';
import clsx from 'clsx';
@ -15,6 +16,9 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen = false, onClose }) => {
const navItems = [
{ to: '/', icon: LayoutDashboard, label: 'Dashboard' },
{ to: '/printing/calculator', icon: Printer, label: 'Calc. 3D' },
{ to: '/printing/filaments', icon: Scroll, label: 'Filamentos' },
{ to: '/printing/printers', icon: Settings, label: 'Impressoras 3D' },
{ to: '/sales', icon: ShoppingCart, label: 'Vendas' },
{ to: '/sourcing', icon: Search, label: 'Sourcing' },
{ to: '/products', icon: Package, label: 'Produtos' },

72
migration_3d_printing.sql Normal file
View file

@ -0,0 +1,72 @@
-- 3D Printing Module Migration
-- 1. PRINTERS Table
create table if not exists public.printers (
id uuid default uuid_generate_v4() primary key,
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
name text not null,
power_watts numeric default 0, -- Power consumption in Watts
depreciation_per_hour numeric default 0, -- Estimated depreciation cost per hour
user_id uuid references auth.users
);
-- 2. FILAMENTS Table
create table if not exists public.filaments (
id uuid default uuid_generate_v4() primary key,
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
name text not null, -- e.g. "PLA Generico"
brand text,
material text, -- PLA, PETG, ABS, etc.
color text,
density_g_cm3 numeric default 1.24, -- Optional, for volume calc
spool_weight_g numeric default 1000, -- e.g. 1000g (1kg)
price_brl numeric default 0, -- Cost of the spool
temp_nozzle integer,
temp_bed integer,
user_id uuid references auth.users
);
-- 3. PRINT JOBS / CALCULATIONS
create table if not exists public.print_jobs (
id uuid default uuid_generate_v4() primary key,
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
name text not null, -- Project/Part name
-- Inputs
printer_id uuid references public.printers,
filament_id uuid references public.filaments,
weight_g numeric default 0,
print_time_hours numeric default 0,
-- Costs Snapshot (Calculated at creation time)
energy_cost numeric default 0,
filament_cost numeric default 0,
depreciation_cost numeric default 0,
additional_cost numeric default 0,
total_cost numeric default 0,
markup_percentage numeric default 0,
final_price numeric default 0,
status text default 'Draft', -- Draft, Printing, Completed
user_id uuid references auth.users
);
-- 4. SETTINGS Update (Electricity Cost)
-- Adding a column safely
do $$
begin
if not exists (select 1 from information_schema.columns where table_name='settings' and column_name='electricity_cost_kwh') then
alter table public.settings add column electricity_cost_kwh numeric default 0.90;
end if;
end $$;
-- Enable RLS
alter table public.printers enable row level security;
alter table public.filaments enable row level security;
alter table public.print_jobs enable row level security;
-- Simple Policies (adjust as needed for authenticated users)
create policy "Enable all for users based on user_id" on public.printers for all using (auth.uid() = user_id);
create policy "Enable all for users based on user_id" on public.filaments for all using (auth.uid() = user_id);
create policy "Enable all for users based on user_id" on public.print_jobs for all using (auth.uid() = user_id);

View file

@ -0,0 +1,287 @@
import React, { useEffect, useState } from 'react';
import { supabase } from '../../services/supabase';
import { Filament, Printer } from '../../types';
import { Calculator as CalcIcon, Save, RefreshCw, Zap, Package, Clock, DollarSign } from 'lucide-react';
const Calculator: React.FC = () => {
// Data Lists
const [printers, setPrinters] = useState<Printer[]>([]);
const [filaments, setFilaments] = useState<Filament[]>([]);
const [elecCost, setElecCost] = useState(0.90);
// Form State
const [selectedPrinterId, setSelectedPrinterId] = useState<string>('');
const [selectedFilamentId, setSelectedFilamentId] = useState<string>('');
const [weightG, setWeightG] = useState<number>(0);
const [timeHours, setTimeHours] = useState<number>(0);
const [markup, setMarkup] = useState<number>(100); // 100% default
// Results State
const [results, setResults] = useState({
materialCost: 0,
energyCost: 0,
depreciationCost: 0,
totalCost: 0,
suggestedPrice: 0
});
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
const { data: printersData } = await supabase.from('printers').select('*');
const { data: filamentsData } = await supabase.from('filaments').select('*');
const { data: settingsData } = await supabase.from('settings').select('electricity_cost_kwh').single();
if (printersData) {
setPrinters(printersData.map((item: any) => ({
id: item.id,
name: item.name,
powerWatts: item.power_watts,
depreciationPerHour: item.depreciation_per_hour
})));
}
if (filamentsData) {
setFilaments(filamentsData.map((item: any) => ({
id: item.id,
name: item.name,
brand: item.brand,
material: item.material,
spoolWeightG: item.spool_weight_g,
priceBRL: item.price_brl
})));
}
if (settingsData) {
setElecCost(settingsData.electricity_cost_kwh || 0.90);
}
};
// Recalculate whenever inputs change
useEffect(() => {
calculate();
}, [selectedPrinterId, selectedFilamentId, weightG, timeHours, markup, elecCost]);
const calculate = () => {
const printer = printers.find(p => p.id === selectedPrinterId);
const filament = filaments.find(f => f.id === selectedFilamentId);
if (!printer || !filament) return;
// 1. Material Cost
const pricePerGram = filament.priceBRL / filament.spoolWeightG;
const matCost = pricePerGram * weightG;
// 2. Energy Cost
// Watts / 1000 = kW * Hours * Cost/kWh
const energyCost = (printer.powerWatts / 1000) * timeHours * elecCost;
// 3. Depreciation
const depCost = printer.depreciationPerHour * timeHours;
const total = matCost + energyCost + depCost;
const finalPrice = total * (1 + markup / 100);
setResults({
materialCost: matCost,
energyCost: energyCost,
depreciationCost: depCost,
totalCost: total,
suggestedPrice: finalPrice
});
};
const handleSaveJob = async () => {
if (!selectedPrinterId || !selectedFilamentId) return;
const name = prompt("Name this print job (e.g. 'Iron Man Helmet'):");
if (!name) return;
const payload = {
name,
printer_id: selectedPrinterId,
filament_id: selectedFilamentId,
weight_g: weightG,
print_time_hours: timeHours,
energy_cost: results.energyCost,
filament_cost: results.materialCost,
depreciation_cost: results.depreciationCost,
total_cost: results.totalCost,
markup_percentage: markup,
final_price: results.suggestedPrice,
user_id: (await supabase.auth.getUser()).data.user?.id
};
const { error } = await supabase.from('print_jobs').insert([payload]);
if (error) {
alert('Error saving job');
console.error(error);
} else {
alert('Job saved to history!');
}
};
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* INPUTS COLUMN */}
<div className="lg:col-span-2 space-y-6">
<div className="bg-card border border-border rounded-xl p-6 shadow-sm">
<div className="flex items-center gap-2 mb-6 text-primary">
<CalcIcon size={24} />
<h2 className="text-xl font-bold">Parameters</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Printer</label>
<select
className="w-full bg-background border border-border rounded-lg px-3 py-2 cursor-pointer focus:ring-2 focus:ring-primary/50 outline-none transition-all"
value={selectedPrinterId}
onChange={e => setSelectedPrinterId(e.target.value)}
>
<option value="">Select Printer</option>
{printers.map(p => (
<option key={p.id} value={p.id}>{p.name} ({p.powerWatts}W)</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Filament</label>
<select
className="w-full bg-background border border-border rounded-lg px-3 py-2 cursor-pointer focus:ring-2 focus:ring-primary/50 outline-none transition-all"
value={selectedFilamentId}
onChange={e => setSelectedFilamentId(e.target.value)}
>
<option value="">Select Filament</option>
{filaments.map(f => (
<option key={f.id} value={f.id}>{f.name} ({f.material})</option>
))}
</select>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Weight (grams)</label>
<div className="relative">
<Package className="absolute left-3 top-2.5 text-muted-foreground" size={18} />
<input
type="number"
className="w-full bg-background border border-border rounded-lg pl-10 pr-3 py-2"
value={weightG}
onChange={e => setWeightG(Number(e.target.value))}
placeholder="0"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1">Time (hours)</label>
<div className="relative">
<Clock className="absolute left-3 top-2.5 text-muted-foreground" size={18} />
<input
type="number"
className="w-full bg-background border border-border rounded-lg pl-10 pr-3 py-2"
value={timeHours}
onChange={e => setTimeHours(Number(e.target.value))}
placeholder="0"
/>
</div>
</div>
</div>
</div>
<div className="mt-6 pt-6 border-t border-border">
<label className="block text-sm font-medium mb-2">Profit Margin / Markup (%)</label>
<input
type="range"
min="0"
max="500"
step="5"
className="w-full h-2 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary"
value={markup}
onChange={e => setMarkup(Number(e.target.value))}
/>
<div className="text-right mt-1 font-mono text-primary font-bold">{markup}%</div>
</div>
</div>
</div>
{/* RESULTS COLUMN */}
<div className="space-y-6">
<div className="bg-card border border-border rounded-xl p-6 shadow-xl relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 to-purple-600"></div>
<h2 className="text-xl font-bold mb-6">Cost Breakdown</h2>
<div className="space-y-4">
<div className="flex justify-between items-center p-3 bg-muted/30 rounded-lg">
<div className="flex items-center gap-3">
<div className="bg-orange-500/10 p-2 rounded-full text-orange-500">
<Package size={18} />
</div>
<span className="text-sm font-medium">Material</span>
</div>
<span className="font-mono font-bold">R$ {results.materialCost.toFixed(2)}</span>
</div>
<div className="flex justify-between items-center p-3 bg-muted/30 rounded-lg">
<div className="flex items-center gap-3">
<div className="bg-yellow-500/10 p-2 rounded-full text-yellow-500">
<Zap size={18} />
</div>
<span className="text-sm font-medium">Energy</span>
</div>
<span className="font-mono font-bold">R$ {results.energyCost.toFixed(2)}</span>
</div>
{results.depreciationCost > 0 && (
<div className="flex justify-between items-center p-3 bg-muted/30 rounded-lg">
<div className="flex items-center gap-3">
<div className="bg-gray-500/10 p-2 rounded-full text-gray-500">
<RefreshCw size={18} />
</div>
<span className="text-sm font-medium">Wear & Tear</span>
</div>
<span className="font-mono font-bold">R$ {results.depreciationCost.toFixed(2)}</span>
</div>
)}
<div className="border-t border-border my-4"></div>
<div className="flex justify-between items-end">
<span className="text-muted-foreground">Total Cost</span>
<span className="text-xl font-bold">R$ {results.totalCost.toFixed(2)}</span>
</div>
<div className="flex justify-between items-end mt-2 text-green-500">
<span className="text-sm font-medium">Profit ({markup}%)</span>
<span className="text-lg font-bold">+ R$ {(results.suggestedPrice - results.totalCost).toFixed(2)}</span>
</div>
</div>
<div className="mt-8 pt-6 border-t border-border">
<div className="text-center mb-2 text-muted-foreground text-sm uppercase tracking-wide">Suggested Price</div>
<div className="text-center text-4xl font-black bg-clip-text text-transparent bg-gradient-to-r from-green-400 to-emerald-600">
R$ {results.suggestedPrice.toFixed(2)}
</div>
</div>
<button
onClick={handleSaveJob}
disabled={!selectedPrinterId || !selectedFilamentId}
className="w-full mt-6 bg-primary hover:bg-primary/90 text-primary-foreground py-3 rounded-xl font-bold shadow-lg shadow-primary/20 transition-all flex justify-center items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Save size={20} />
Save to History
</button>
</div>
</div>
</div>
);
};
export default Calculator;

View file

@ -0,0 +1,250 @@
import React, { useEffect, useState } from 'react';
import { supabase } from '../../services/supabase';
import { Filament } from '../../types';
import { Plus, Trash2, Save, Package } from 'lucide-react';
const Filaments: React.FC = () => {
const [filaments, setFilaments] = useState<Filament[]>([]);
const [loading, setLoading] = useState(true);
const [isAdding, setIsAdding] = useState(false);
// New Filament State
const [newFilament, setNewFilament] = useState<Partial<Filament>>({
name: '',
brand: '',
material: 'PLA',
color: '',
spoolWeightG: 1000,
priceBRL: 0,
tempNozzle: 200,
tempBed: 60
});
useEffect(() => {
fetchFilaments();
}, []);
const fetchFilaments = async () => {
try {
const { data, error } = await supabase.from('filaments').select('*');
if (error) throw error;
// Map snake_case database fields to camelCase TS interfaces
const mapped = (data || []).map((item: any) => ({
id: item.id,
name: item.name,
brand: item.brand,
material: item.material,
color: item.color,
spoolWeightG: item.spool_weight_g,
priceBRL: item.price_brl,
tempNozzle: item.temp_nozzle,
tempBed: item.temp_bed
}));
setFilaments(mapped);
} catch (error) {
console.error('Error fetching filaments:', error);
} finally {
setLoading(false);
}
};
const handleSave = async () => {
if (!newFilament.name || !newFilament.priceBRL) return;
try {
const payload = {
name: newFilament.name,
brand: newFilament.brand,
material: newFilament.material,
color: newFilament.color,
spool_weight_g: newFilament.spoolWeightG,
price_brl: newFilament.priceBRL,
temp_nozzle: newFilament.tempNozzle,
temp_bed: newFilament.tempBed,
user_id: (await supabase.auth.getUser()).data.user?.id
};
const { error } = await supabase.from('filaments').insert([payload]);
if (error) throw error;
setIsAdding(false);
setNewFilament({
name: '',
brand: '',
material: 'PLA',
color: '',
spoolWeightG: 1000,
priceBRL: 0,
tempNozzle: 200,
tempBed: 60
});
fetchFilaments();
} catch (error) {
console.error('Error saving filament:', error);
alert('Error saving filament');
}
};
const handleDelete = async (id: string) => {
if (!confirm('Are you sure?')) return;
try {
const { error } = await supabase.from('filaments').delete().eq('id', id);
if (error) throw error;
fetchFilaments();
} catch (error) {
console.error('Error deleting filament:', error);
}
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-pink-600">
Filaments Library
</h1>
<p className="text-muted-foreground mt-1">Manage your 3D printing materials.</p>
</div>
<button
onClick={() => setIsAdding(!isAdding)}
className="bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded-lg flex items-center gap-2 transition-all shadow-lg shadow-primary/20"
>
<Plus size={20} />
Add Filament
</button>
</div>
{isAdding && (
<div className="bg-card border border-border rounded-xl p-6 shadow-xl animate-in fade-in slide-in-from-top-4">
<h3 className="text-lg font-semibold mb-4">New Filament</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="md:col-span-2">
<label className="block text-sm font-medium mb-1">Name</label>
<input
className="w-full bg-background border border-border rounded-md px-3 py-2"
placeholder="e.g. PLA Basic White"
value={newFilament.name}
onChange={e => setNewFilament({ ...newFilament, name: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Brand</label>
<input
className="w-full bg-background border border-border rounded-md px-3 py-2"
placeholder="e.g. Voolt3D"
value={newFilament.brand}
onChange={e => setNewFilament({ ...newFilament, brand: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Material</label>
<select
className="w-full bg-background border border-border rounded-md px-3 py-2"
value={newFilament.material}
onChange={e => setNewFilament({ ...newFilament, material: e.target.value })}
>
<option value="PLA">PLA</option>
<option value="PETG">PETG</option>
<option value="ABS">ABS</option>
<option value="TPU">TPU</option>
<option value="ASA">ASA</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Color</label>
<input
className="w-full bg-background border border-border rounded-md px-3 py-2"
placeholder="e.g. White"
value={newFilament.color}
onChange={e => setNewFilament({ ...newFilament, color: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Weight (g)</label>
<input
type="number"
className="w-full bg-background border border-border rounded-md px-3 py-2"
value={newFilament.spoolWeightG}
onChange={e => setNewFilament({ ...newFilament, spoolWeightG: Number(e.target.value) })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Price (R$)</label>
<input
type="number"
className="w-full bg-background border border-border rounded-md px-3 py-2"
value={newFilament.priceBRL}
onChange={e => setNewFilament({ ...newFilament, priceBRL: Number(e.target.value) })}
/>
</div>
</div>
<div className="flex justify-end mt-4 gap-2">
<button
onClick={() => setIsAdding(false)}
className="px-4 py-2 text-muted-foreground hover:text-foreground"
>
Cancel
</button>
<button
onClick={handleSave}
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
>
<Save size={18} />
Save Filament
</button>
</div>
</div>
)}
{loading ? (
<div className="text-center py-10 text-muted-foreground">Loading...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filaments.map(filament => (
<div key={filament.id} className="bg-card border border-border rounded-xl p-4 shadow-sm hover:shadow-md transition-all relative group">
<div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleDelete(filament.id)}
className="text-red-500 hover:text-red-400 p-1 bg-background/80 rounded-full"
>
<Trash2 size={18} />
</button>
</div>
<div className="flex items-center gap-3 mb-3">
<div className="bg-primary/10 p-3 rounded-lg text-primary">
<Package size={24} />
</div>
<div>
<h3 className="font-semibold">{filament.name}</h3>
<p className="text-xs text-muted-foreground">{filament.brand} {filament.material}</p>
</div>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Price/Spool:</span>
<span className="font-medium">R$ {filament.priceBRL.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Weight:</span>
<span>{filament.spoolWeightG}g</span>
</div>
<div className="flex justify-between border-t border-border pt-2 mt-2">
<span className="text-muted-foreground">Cost/gram:</span>
<span className="text-green-500 font-bold">
R$ {(filament.priceBRL / filament.spoolWeightG).toFixed(4)}
</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
};
export default Filaments;

193
pages/Printing/Printers.tsx Normal file
View file

@ -0,0 +1,193 @@
import React, { useEffect, useState } from 'react';
import { supabase } from '../../services/supabase';
import { Printer } from '../../types';
import { Plus, Trash2, Save, Printer as PrinterIcon, Zap } from 'lucide-react';
const Printers: React.FC = () => {
const [printers, setPrinters] = useState<Printer[]>([]);
const [loading, setLoading] = useState(true);
const [isAdding, setIsAdding] = useState(false);
const [newPrinter, setNewPrinter] = useState<Partial<Printer>>({
name: '',
powerWatts: 350,
depreciationPerHour: 0
});
useEffect(() => {
fetchPrinters();
}, []);
const fetchPrinters = async () => {
try {
const { data, error } = await supabase.from('printers').select('*');
if (error) throw error;
const mapped = (data || []).map((item: any) => ({
id: item.id,
name: item.name,
powerWatts: item.power_watts,
depreciationPerHour: item.depreciation_per_hour
}));
setPrinters(mapped);
} catch (error) {
console.error('Error fetching printers:', error);
} finally {
setLoading(false);
}
};
const handleSave = async () => {
if (!newPrinter.name) return;
try {
const payload = {
name: newPrinter.name,
power_watts: newPrinter.powerWatts,
depreciation_per_hour: newPrinter.depreciationPerHour,
user_id: (await supabase.auth.getUser()).data.user?.id
};
const { error } = await supabase.from('printers').insert([payload]);
if (error) throw error;
setIsAdding(false);
setNewPrinter({ name: '', powerWatts: 350, depreciationPerHour: 0 });
fetchPrinters();
} catch (error) {
console.error('Error saving printer:', error);
alert('Error saving printer');
}
};
const handleDelete = async (id: string) => {
if (!confirm('Are you sure?')) return;
try {
const { error } = await supabase.from('printers').delete().eq('id', id);
if (error) throw error;
fetchPrinters();
} catch (error) {
console.error('Error deleting printer:', error);
}
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-cyan-600">
Printers
</h1>
<p className="text-muted-foreground mt-1">Manage your 3D printers configuration.</p>
</div>
<button
onClick={() => setIsAdding(!isAdding)}
className="bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded-lg flex items-center gap-2 transition-all shadow-lg shadow-primary/20"
>
<Plus size={20} />
Add Printer
</button>
</div>
{isAdding && (
<div className="bg-card border border-border rounded-xl p-6 shadow-xl animate-in fade-in slide-in-from-top-4">
<h3 className="text-lg font-semibold mb-4">New Printer</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Name</label>
<input
className="w-full bg-background border border-border rounded-md px-3 py-2"
placeholder="e.g. Ender 3 V2"
value={newPrinter.name}
onChange={e => setNewPrinter({ ...newPrinter, name: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Power Consumption (Watts)</label>
<input
type="number"
className="w-full bg-background border border-border rounded-md px-3 py-2"
value={newPrinter.powerWatts}
onChange={e => setNewPrinter({ ...newPrinter, powerWatts: Number(e.target.value) })}
/>
<p className="text-xs text-muted-foreground mt-1">Found on printer label (avg 350W)</p>
</div>
<div>
<label className="block text-sm font-medium mb-1">Depreciation (R$/hr)</label>
<input
type="number"
className="w-full bg-background border border-border rounded-md px-3 py-2"
value={newPrinter.depreciationPerHour}
onChange={e => setNewPrinter({ ...newPrinter, depreciationPerHour: Number(e.target.value) })}
/>
<p className="text-xs text-muted-foreground mt-1">Optional machine wear cost</p>
</div>
</div>
<div className="flex justify-end mt-4 gap-2">
<button
onClick={() => setIsAdding(false)}
className="px-4 py-2 text-muted-foreground hover:text-foreground"
>
Cancel
</button>
<button
onClick={handleSave}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
>
<Save size={18} />
Save Printer
</button>
</div>
</div>
)}
{loading ? (
<div className="text-center py-10 text-muted-foreground">Loading...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{printers.map(printer => (
<div key={printer.id} className="bg-card border border-border rounded-xl p-6 shadow-sm hover:shadow-md transition-all relative group">
<div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleDelete(printer.id)}
className="text-red-500 hover:text-red-400 p-1 bg-background/80 rounded-full"
>
<Trash2 size={18} />
</button>
</div>
<div className="flex items-center gap-4 mb-4">
<div className="bg-blue-500/10 p-3 rounded-full text-blue-500">
<PrinterIcon size={28} />
</div>
<div>
<h3 className="text-lg font-bold">{printer.name}</h3>
<div className="flex items-center gap-1 text-sm text-yellow-500">
<Zap size={14} fill="currentColor" />
<span>{printer.powerWatts}W</span>
</div>
</div>
</div>
<div className="bg-muted/50 rounded-lg p-3 space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Consumption:</span>
<span className="font-mono">{(printer.powerWatts / 1000).toFixed(2)} kWh</span>
</div>
{printer.depreciationPerHour > 0 && (
<div className="flex justify-between">
<span className="text-muted-foreground">Depreciation:</span>
<span className="font-mono">R$ {printer.depreciationPerHour.toFixed(2)}/h</span>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
);
};
export default Printers;

View file

@ -82,6 +82,7 @@ const Settings: React.FC = () => {
smtpPort: '587',
smtpUser: '',
smtpPass: '',
electricityCostKwh: 0.90,
});
// Sync state when settings load from DB
@ -216,6 +217,19 @@ const Settings: React.FC = () => {
/>
</div>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-slate-500 uppercase">Custo Energia (R$/kWh)</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500 text-xs font-bold">R$</span>
<input
type="number"
step="0.01"
value={config.electricityCostKwh}
onChange={e => setConfig({ ...config, electricityCostKwh: parseFloat(e.target.value) })}
className="w-full bg-black/20 border border-white/10 rounded-lg pl-8 pr-3 py-2 text-sm text-slate-200 focus:border-emerald-500 outline-none transition-all font-mono"
/>
</div>
</div>
</div>
</div>
</div>

View file

@ -165,3 +165,40 @@ export interface BiddingTender {
marketReferencePrice: number;
url: string;
}
// 3D Printing Module Types
export interface Printer {
id: string;
name: string;
powerWatts: number;
depreciationPerHour: number;
}
export interface Filament {
id: string;
name: string;
brand?: string;
material: string;
color?: string;
spoolWeightG: number;
priceBRL: number;
tempNozzle?: number;
tempBed?: number;
}
export interface PrintJob {
id: string;
name: string;
printerId: string;
filamentId: string;
weightG: number;
printTimeHours: number;
energyCost: number;
filamentCost: number;
depreciationCost: number;
additionalCost: number;
totalCost: number;
markupPercentage: number;
finalPrice: number;
status: 'Draft' | 'Printing' | 'Completed';
}