diff --git a/frontend/src/components/MaterialPackages.js b/frontend/src/components/MaterialPackages.js
index 7920ede..5776e4a 100644
--- a/frontend/src/components/MaterialPackages.js
+++ b/frontend/src/components/MaterialPackages.js
@@ -1,12 +1,134 @@
-import React from 'react';
+import React, { useState, useEffect } from 'react';
import './MaterialPackages.css';
-import SmartPackages from './SmartPackages';
+import axios from 'axios';
+import {
+ Container, Typography, Paper, Grid, Box, Button, TextField, MenuItem
+} from '@mui/material';
+/**
+ * Material Packages komponent
+ * Viser en liste over materialer
+ */
const MaterialPackages = ({ apiBaseUrl }) => {
+ const [materials, setMaterials] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [categories, setCategories] = useState([]);
+ const [selectedCategory, setSelectedCategory] = useState('all');
+ const [searchTerm, setSearchTerm] = useState('');
+
+ useEffect(() => {
+ // Hent materiale kategorier
+ fetchCategories();
+
+ // Hent materialer
+ fetchMaterials();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [apiBaseUrl]);
+
+ const fetchCategories = async () => {
+ try {
+ const response = await axios.get(`${apiBaseUrl || ''}/api/materials/categories`);
+ if (response.data.success) {
+ setCategories(response.data.categories || []);
+ }
+ } catch (error) {
+ console.error('Fejl ved hentning af kategorier:', error);
+ }
+ };
+
+ const fetchMaterials = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.get(`${apiBaseUrl || ''}/api/materials`, {
+ params: {
+ category: selectedCategory !== 'all' ? selectedCategory : undefined,
+ search: searchTerm || undefined
+ }
+ });
+
+ if (response.data.success) {
+ setMaterials(response.data.materials || []);
+ }
+
+ setLoading(false);
+ } catch (error) {
+ console.error('Fejl ved hentning af materialer:', error);
+ setLoading(false);
+ }
+ };
+
return (
- {/* Kun Smart Materialpakker til flow */}
-
+
+
+
+ 🧱 Materialer
+
+
+ Administrer materialedatabase med priser og kategorier
+
+
+
+ {/* Filter og søgning */}
+
+
+
+ setSearchTerm(e.target.value)}
+ placeholder="Søg efter navn, varenummer..."
+ />
+
+
+ setSelectedCategory(e.target.value)}
+ >
+
+ {categories.map((category) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ {/* Materialer liste */}
+ {loading ? (
+ Indlæser materialer...
+ ) : materials.length === 0 ? (
+ Ingen materialer fundet. Prøv at ændre søgning.
+ ) : (
+
+ {/* Implementering af materialer liste */}
+
+
+ {materials.length} materialer fundet
+
+ {/* Materialeliste implementering kommer her */}
+
+
+ )}
+
);
};
diff --git a/frontend/src/components/TaskDialog.js b/frontend/src/components/TaskDialog.js
new file mode 100644
index 0000000..926971d
--- /dev/null
+++ b/frontend/src/components/TaskDialog.js
@@ -0,0 +1,745 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Dialog, DialogTitle, DialogContent, DialogActions,
+ Button, TextField, Typography, FormControl,
+ FormControlLabel, Switch, InputLabel, Select, MenuItem,
+ Grid, IconButton, Paper, Box, Tooltip, Chip,
+ Stepper, Step, StepLabel, StepContent, Alert, Divider
+} from '@mui/material';
+import {
+ Add as AddIcon,
+ Delete as DeleteIcon,
+ Check as CheckIcon,
+ Edit as EditIcon,
+ ArrowUpward as ArrowUpwardIcon,
+ ArrowDownward as ArrowDownwardIcon,
+ DragHandle as DragHandleIcon,
+ Image as ImageIcon,
+ VideoLibrary as VideoIcon,
+ Security as SecurityIcon,
+ Verified as VerifiedIcon,
+ Lightbulb as TipsIcon,
+ Timeline as TimelineIcon,
+ Warning as WarningIcon
+} from '@mui/icons-material';
+import { useSnackbar } from 'notistack';
+
+/**
+ * Dialog til oprettelse og redigering af en opgave (task) i en smart pakke
+ * Med håndtering af monteringstrin (steps) og afhængigheder til andre opgaver
+ */
+const TaskDialog = ({
+ open,
+ onClose,
+ onSave,
+ task = null,
+ allTasks = [],
+ apiBaseUrl = ''
+}) => {
+ const { enqueueSnackbar } = useSnackbar();
+
+ // Task data state
+ const [taskData, setTaskData] = useState({
+ name: '',
+ description: '',
+ hours: 0,
+ rate: 580,
+ skills: '',
+ tools: '',
+ optional: false,
+ dependsOn: '',
+ installationManualId: null
+ });
+
+ // Steps state
+ const [steps, setSteps] = useState([]);
+ const [newStep, setNewStep] = useState({
+ title: '',
+ description: '',
+ minutes: 15,
+ image: '',
+ video: '',
+ safety: '',
+ quality: '',
+ tips: ''
+ });
+ const [editingStepIndex, setEditingStepIndex] = useState(-1);
+ const [showAddStep, setShowAddStep] = useState(false);
+
+ // Hjælpevariable
+ const [totalTime, setTotalTime] = useState(0);
+ const [activeStep, setActiveStep] = useState(0);
+ const [errors, setErrors] = useState({});
+ const isEditMode = task !== null;
+
+ // Indlæs eksisterende data ved redigering
+ useEffect(() => {
+ if (task) {
+ setTaskData({
+ name: task.name || '',
+ description: task.description || '',
+ hours: task.hours || 0,
+ rate: task.rate || 580,
+ skills: task.skills || '',
+ tools: task.tools || '',
+ optional: task.optional || false,
+ dependsOn: task.depends_on_task_id || '',
+ installationManualId: task.installation_manual_id || null
+ });
+
+ // Hent monteringstrin hvis opgaven har et installations-manual ID
+ if (task.installation_manual_id) {
+ fetchSteps(task.installation_manual_id);
+ }
+ } else {
+ resetForm();
+ }
+ }, [task]);
+
+ // Opdater total time når steps ændres
+ useEffect(() => {
+ const totalMinutes = steps.reduce((total, step) => total + (parseFloat(step.minutes) || 0), 0);
+ const totalHours = Math.round((totalMinutes / 60) * 100) / 100; // Round to 2 decimals
+ setTotalTime(totalHours);
+
+ if (totalHours > 0) {
+ setTaskData(prev => ({
+ ...prev,
+ hours: totalHours
+ }));
+ }
+ }, [steps]);
+
+ // Hent steps for et installations-manual ID
+ const fetchSteps = async (manualId) => {
+ try {
+ const response = await fetch(`${apiBaseUrl}/api/smart-packages/installation-manuals/${manualId}`);
+ const data = await response.json();
+
+ if (data.success && data.steps) {
+ setSteps(data.steps);
+ }
+ } catch (error) {
+ console.error('Fejl ved hentning af monteringstrin:', error);
+ enqueueSnackbar('Kunne ikke hente monteringstrin', { variant: 'error' });
+ }
+ };
+
+ // Håndter ændringer i task data
+ const handleChange = (field) => (event) => {
+ const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
+ setTaskData({
+ ...taskData,
+ [field]: value
+ });
+
+ // Clear error for the field
+ if (errors[field]) {
+ setErrors({
+ ...errors,
+ [field]: null
+ });
+ }
+ };
+
+ // Håndter ændringer i new step data
+ const handleStepChange = (field) => (event) => {
+ const value = event.target.value;
+ setNewStep({
+ ...newStep,
+ [field]: value
+ });
+ };
+
+ // Validér form data
+ const validateForm = () => {
+ const newErrors = {};
+ if (!taskData.name.trim()) {
+ newErrors.name = 'Opgavenavn er påkrævet';
+ }
+ if (taskData.hours <= 0) {
+ newErrors.hours = 'Timer skal være større end 0';
+ }
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ // Tilføj et nyt monteringstrin
+ const handleAddStep = () => {
+ if (!newStep.title.trim()) {
+ enqueueSnackbar('Trin-titel er påkrævet', { variant: 'error' });
+ return;
+ }
+
+ if (editingStepIndex >= 0) {
+ // Opdater eksisterende trin
+ const updatedSteps = [...steps];
+ updatedSteps[editingStepIndex] = newStep;
+ setSteps(updatedSteps);
+ setEditingStepIndex(-1);
+ } else {
+ // Tilføj nyt trin
+ setSteps([...steps, newStep]);
+ }
+
+ // Nulstil formular
+ setNewStep({
+ title: '',
+ description: '',
+ minutes: 15,
+ image: '',
+ video: '',
+ safety: '',
+ quality: '',
+ tips: ''
+ });
+
+ setShowAddStep(false);
+ };
+
+ // Rediger et monteringstrin
+ const handleEditStep = (index) => {
+ setNewStep({ ...steps[index] });
+ setEditingStepIndex(index);
+ setShowAddStep(true);
+ };
+
+ // Slet et monteringstrin
+ const handleDeleteStep = (index) => {
+ const updatedSteps = steps.filter((_, i) => i !== index);
+ setSteps(updatedSteps);
+ };
+
+ // Flyt et trin op eller ned
+ const handleMoveStep = (index, direction) => {
+ if ((direction === 'up' && index === 0) ||
+ (direction === 'down' && index === steps.length - 1)) {
+ return;
+ }
+
+ const updatedSteps = [...steps];
+ const newIndex = direction === 'up' ? index - 1 : index + 1;
+
+ const temp = updatedSteps[index];
+ updatedSteps[index] = updatedSteps[newIndex];
+ updatedSteps[newIndex] = temp;
+
+ setSteps(updatedSteps);
+ };
+
+ // Gem opgave og monteringstrin
+ const handleSave = async () => {
+ if (!validateForm()) {
+ enqueueSnackbar('Ret venligst fejlene før du gemmer', { variant: 'error' });
+ return;
+ }
+
+ try {
+ const saveData = {
+ ...taskData,
+ steps: steps
+ };
+
+ onSave(saveData);
+ resetForm();
+ setActiveStep(0);
+ onClose();
+ } catch (error) {
+ console.error('Fejl ved gemning af opgave:', error);
+ enqueueSnackbar('Fejl ved gemning af opgave', { variant: 'error' });
+ }
+ };
+
+ // Nulstil formular
+ const resetForm = () => {
+ setTaskData({
+ name: '',
+ description: '',
+ hours: 0,
+ rate: 580,
+ skills: '',
+ tools: '',
+ optional: false,
+ dependsOn: '',
+ installationManualId: null
+ });
+ setSteps([]);
+ setNewStep({
+ title: '',
+ description: '',
+ minutes: 15,
+ image: '',
+ video: '',
+ safety: '',
+ quality: '',
+ tips: ''
+ });
+ setEditingStepIndex(-1);
+ setShowAddStep(false);
+ setErrors({});
+ };
+
+ // Handle stepper next/back
+ const handleNext = () => {
+ if (activeStep === 0 && !validateForm()) {
+ return;
+ }
+ setActiveStep((prevActiveStep) => prevActiveStep + 1);
+ };
+
+ const handleBack = () => {
+ setActiveStep((prevActiveStep) => prevActiveStep - 1);
+ };
+
+ return (
+
+ );
+};
+
+export default TaskDialog;
\ No newline at end of file
diff --git a/frontend/src/components/common/ConfirmDialog.js b/frontend/src/components/common/ConfirmDialog.js
new file mode 100644
index 0000000..a92bbab
--- /dev/null
+++ b/frontend/src/components/common/ConfirmDialog.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import {
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogContentText,
+ DialogActions,
+ Button
+} from '@mui/material';
+
+/**
+ * ConfirmDialog Component
+ *
+ * Bekræftelsesdialog til farlige handlinger
+ */
+const ConfirmDialog = ({
+ open,
+ title,
+ message,
+ confirmText = 'Bekræft',
+ cancelText = 'Annuller',
+ onConfirm,
+ onCancel,
+ severity = 'warning' // 'warning', 'error', 'info'
+}) => {
+ const getColor = () => {
+ switch (severity) {
+ case 'error':
+ return 'error';
+ case 'info':
+ return 'primary';
+ default:
+ return 'warning';
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default ConfirmDialog;
diff --git a/frontend/src/components/common/EmptyState.js b/frontend/src/components/common/EmptyState.js
new file mode 100644
index 0000000..3232e0c
--- /dev/null
+++ b/frontend/src/components/common/EmptyState.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import { Box, Typography, Button } from '@mui/material';
+
+/**
+ * EmptyState Component
+ *
+ * Viser en tom tilstand med ikon, besked og valgfri handling
+ */
+const EmptyState = ({
+ title,
+ message,
+ actionText,
+ onAction,
+ icon
+}) => {
+ return (
+
+ {icon && (
+
+ {icon}
+
+ )}
+
+
+ {title}
+
+
+ {message && (
+
+ {message}
+
+ )}
+
+ {actionText && onAction && (
+
+ )}
+
+ );
+};
+
+export default EmptyState;
diff --git a/frontend/src/components/common/LoadingOverlay.js b/frontend/src/components/common/LoadingOverlay.js
new file mode 100644
index 0000000..545e09e
--- /dev/null
+++ b/frontend/src/components/common/LoadingOverlay.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import { Box, CircularProgress, Typography, Backdrop } from '@mui/material';
+
+/**
+ * LoadingOverlay Component
+ *
+ * Viser en loading overlay med spinner og tekst
+ */
+const LoadingOverlay = ({ text = 'Indlæser...', open = true }) => {
+ return (
+
theme.zIndex.drawer + 1,
+ backgroundColor: 'rgba(255, 255, 255, 0.9)'
+ }}
+ open={open}
+ >
+
+
+ {text && (
+
+ {text}
+
+ )}
+
+
+ );
+};
+
+export default LoadingOverlay;
diff --git a/frontend/src/components/common/PageHeader.js b/frontend/src/components/common/PageHeader.js
new file mode 100644
index 0000000..ded8a02
--- /dev/null
+++ b/frontend/src/components/common/PageHeader.js
@@ -0,0 +1,31 @@
+import React from 'react';
+import { Box, Typography } from '@mui/material';
+
+/**
+ * PageHeader Component
+ *
+ * Standardiseret sidehovede med titel, undertitel og ikon
+ */
+const PageHeader = ({ title, subtitle, icon }) => {
+ return (
+
+
+ {icon && (
+
+ {icon}
+
+ )}
+
+ {title}
+
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ );
+};
+
+export default PageHeader;
diff --git a/frontend/src/components/smartPackages/PackageDetailsDialog.js b/frontend/src/components/smartPackages/PackageDetailsDialog.js
new file mode 100644
index 0000000..4275ffe
--- /dev/null
+++ b/frontend/src/components/smartPackages/PackageDetailsDialog.js
@@ -0,0 +1,706 @@
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import {
+ Dialog, DialogTitle, DialogContent, DialogActions, Button,
+ Typography, Box, Divider, Grid, Chip, IconButton, Tabs, Tab,
+ List, ListItem, ListItemText, ListItemIcon, ListItemSecondaryAction,
+ Accordion, AccordionSummary, AccordionDetails, Card, CardContent,
+ LinearProgress, Paper, TableContainer, Table, TableHead, TableBody, TableRow, TableCell,
+ Tooltip
+} from '@mui/material';
+import {
+ Close as CloseIcon, Edit as EditIcon, Description as DescriptionIcon,
+ CheckCircle as CheckCircleIcon, Assignment as AssignmentIcon,
+ Build as BuildIcon, Category as CategoryIcon, Timer as TimerIcon,
+ AttachMoney as MoneyIcon, ListAlt as ListAltIcon, ViewList as ViewListIcon,
+ ExpandMore as ExpandMoreIcon, Print as PrintIcon, Add as AddIcon,
+ Info as InfoIcon, Star as StarIcon
+} from '@mui/icons-material';
+import { useSnackbar } from 'notistack';
+import axios from 'axios';
+
+// Hjælpefunktioner
+import { formatCurrency, formatTime, formatDate } from '../../utils/formatters';
+
+/**
+ * Dialog til at vise detaljer om en Smart Pakke
+ */
+const PackageDetailsDialog = ({ open, onClose, packageId, onEdit }) => {
+ const { enqueueSnackbar } = useSnackbar();
+
+ // State
+ const [packageData, setPackageData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [activeTab, setActiveTab] = useState(0);
+ const [expandedTaskId, setExpandedTaskId] = useState(null);
+
+ // Hent pakkedetaljer
+ useEffect(() => {
+ if (open && packageId) {
+ fetchPackageDetails();
+ }
+ }, [open, packageId]);
+
+ // Hent pakkedetaljer fra API
+ const fetchPackageDetails = async () => {
+ setLoading(true);
+ try {
+ const response = await axios.get(`/api/smart-packages/${packageId}`);
+
+ if (response.data.success) {
+ setPackageData(response.data.package);
+ } else {
+ enqueueSnackbar('Kunne ikke hente pakkedetaljer: ' + response.data.error, { variant: 'error' });
+ onClose();
+ }
+ } catch (error) {
+ console.error('Fejl ved hentning af pakkedetaljer:', error);
+ enqueueSnackbar('Fejl ved hentning af pakkedetaljer', { variant: 'error' });
+ onClose();
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Print arbejdsbeskrivelse
+ const handlePrintWorkDescription = async () => {
+ try {
+ const response = await axios.get(`/api/smart-packages/${packageId}/work-description`);
+
+ if (response.data.success) {
+ // Opret et nyt vindue og indsæt arbejdsbeskrivelsen
+ const printWindow = window.open('', '_blank');
+ printWindow.document.write(`
+
+
+
Arbejdsbeskrivelse - ${packageData.name}
+
+
+
+ ${response.data.workDescription}
+
+
+
+
+ `);
+ printWindow.document.close();
+ } else {
+ enqueueSnackbar('Kunne ikke generere arbejdsbeskrivelse: ' + response.data.error, { variant: 'error' });
+ }
+ } catch (error) {
+ console.error('Fejl ved generering af arbejdsbeskrivelse:', error);
+ enqueueSnackbar('Fejl ved generering af arbejdsbeskrivelse', { variant: 'error' });
+ }
+ };
+
+ // Skift fane
+ const handleTabChange = (event, newValue) => {
+ setActiveTab(newValue);
+ };
+
+ // Håndter task udvid/skjul
+ const handleExpandTask = (taskId) => {
+ setExpandedTaskId(expandedTaskId === taskId ? null : taskId);
+ };
+
+ // Få sværhedsgrad som stjerner
+ const renderDifficultyStars = (level) => {
+ return (
+
+ {[...Array(level || 1)].map((_, i) => (
+
+ ))}
+ {[...Array(4 - (level || 1))].map((_, i) => (
+
+ ))}
+
+ );
+ };
+
+ // Hjælper funktion til at få sværhedsgrad tekst
+ const getDifficultyText = (level) => {
+ switch (level) {
+ case 1: return 'Let';
+ case 2: return 'Middel';
+ case 3: return 'Svær';
+ case 4: return 'Ekspert';
+ default: return 'Ukendt';
+ }
+ };
+
+ // Beregn total for materialer
+ const calculateTotalMaterialCost = (materials) => {
+ return materials.reduce((sum, material) => {
+ const price = material.custom_price !== null ? material.custom_price : material.price;
+ return sum + (price * material.quantity);
+ }, 0);
+ };
+
+ // Hvis der indlæses
+ if (loading) {
+ return (
+
+ );
+ }
+
+ // Hvis pakke ikke findes
+ if (!packageData) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
+PackageDetailsDialog.propTypes = {
+ open: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+ packageId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ onEdit: PropTypes.func
+};
+
+PackageDetailsDialog.defaultProps = {
+ onEdit: () => {}
+};
+
+export default PackageDetailsDialog;
\ No newline at end of file
diff --git a/frontend/src/components/smartPackages/SmartPackageForm.js b/frontend/src/components/smartPackages/SmartPackageForm.js
new file mode 100644
index 0000000..ae0fd00
--- /dev/null
+++ b/frontend/src/components/smartPackages/SmartPackageForm.js
@@ -0,0 +1,1320 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import {
+ Container, Typography, Paper, Grid, TextField, MenuItem, Button,
+ FormControlLabel, Switch, Divider, Box, Tabs, Tab, Stepper,
+ Step, StepLabel, IconButton, Card, CardContent, List, ListItem,
+ ListItemText, ListItemSecondaryAction, Autocomplete, FormHelperText
+} from '@mui/material';
+import {
+ Save as SaveIcon, ArrowBack as ArrowBackIcon, Add as AddIcon,
+ Delete as DeleteIcon, Edit as EditIcon, Reorder as ReorderIcon,
+ Description as DescriptionIcon, Build as BuildIcon, Category as CategoryIcon,
+ FormatListBulleted as FormatListBulletedIcon
+} from '@mui/icons-material';
+import { useSnackbar } from 'notistack';
+import axios from 'axios';
+
+// Import komponenter
+import PageHeader from '../common/PageHeader';
+import LoadingOverlay from '../common/LoadingOverlay';
+import ConfirmDialog from '../common/ConfirmDialog';
+
+// Dialog komponenter
+import TaskDialog from './dialogs/TaskDialog';
+import MaterialDialog from './dialogs/MaterialDialog';
+
+/**
+ * Smart Pakke formular til oprettelse og redigering
+ */
+const SmartPackageForm = () => {
+ const navigate = useNavigate();
+ const { id } = useParams();
+ const { enqueueSnackbar } = useSnackbar();
+ const isEditMode = Boolean(id);
+
+ // State variabler
+ const [packageData, setPackageData] = useState({
+ name: '',
+ description: '',
+ category: '',
+ package_type: 'standard',
+ difficulty_level: 2,
+ estimated_hours: 0,
+ hourly_rate: 580,
+ installation_notes: '',
+ is_template: false,
+ materials: [],
+ tasks: []
+ });
+
+ const [loading, setLoading] = useState(isEditMode);
+ const [saving, setSaving] = useState(false);
+ const [categories, setCategories] = useState([]);
+ const [packageTypes, setPackageTypes] = useState([]);
+ const [activeTab, setActiveTab] = useState(0);
+ const [activeStep, setActiveStep] = useState(0);
+
+ // Dialog states
+ const [taskDialogOpen, setTaskDialogOpen] = useState(false);
+ const [materialDialogOpen, setMaterialDialogOpen] = useState(false);
+ const [editingTask, setEditingTask] = useState(null);
+ const [editingMaterial, setEditingMaterial] = useState(null);
+ const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
+ const [itemToDelete, setItemToDelete] = useState(null);
+ const [deleteType, setDeleteType] = useState('');
+
+ // Materialliste
+ const [availableMaterials, setAvailableMaterials] = useState([]);
+
+ // Validering
+ const [errors, setErrors] = useState({});
+
+ // Hent data ved indlæsning
+ useEffect(() => {
+ fetchCategories();
+ fetchMaterials();
+
+ if (isEditMode) {
+ fetchPackageDetails();
+ }
+ }, [isEditMode, id]);
+
+ // Hent kategorier og pakketyper
+ const fetchCategories = async () => {
+ try {
+ const response = await axios.get('/api/smart-packages/categories');
+
+ if (response.data.success) {
+ setCategories(response.data.categories);
+ setPackageTypes(response.data.types);
+ } else {
+ enqueueSnackbar('Kunne ikke hente kategorier: ' + response.data.error, { variant: 'error' });
+ }
+ } catch (error) {
+ console.error('Fejl ved hentning af kategorier:', error);
+ enqueueSnackbar('Fejl ved hentning af kategorier', { variant: 'error' });
+ }
+ };
+
+ // Hent materialer til autocomplete
+ const fetchMaterials = async () => {
+ try {
+ const response = await axios.get('/api/materials');
+
+ if (response.data.success) {
+ setAvailableMaterials(response.data.materials);
+ } else {
+ enqueueSnackbar('Kunne ikke hente materialer: ' + response.data.error, { variant: 'error' });
+ }
+ } catch (error) {
+ console.error('Fejl ved hentning af materialer:', error);
+ enqueueSnackbar('Fejl ved hentning af materialer', { variant: 'error' });
+ }
+ };
+
+ // Hent pakkedetaljer hvis i redigeringstilstand
+ const fetchPackageDetails = async () => {
+ setLoading(true);
+ try {
+ const response = await axios.get(`/api/smart-packages/${id}`);
+
+ if (response.data.success) {
+ setPackageData(response.data.package);
+ } else {
+ enqueueSnackbar('Kunne ikke hente pakkedetaljer: ' + response.data.error, { variant: 'error' });
+ navigate('/smart-packages');
+ }
+ } catch (error) {
+ console.error('Fejl ved hentning af pakkedetaljer:', error);
+ enqueueSnackbar('Fejl ved hentning af pakkedetaljer', { variant: 'error' });
+ navigate('/smart-packages');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Håndter ændring af grundlæggende felter
+ const handleInputChange = (field) => (event) => {
+ setPackageData({
+ ...packageData,
+ [field]: event.target.value
+ });
+
+ // Fjern fejl for dette felt
+ if (errors[field]) {
+ setErrors({
+ ...errors,
+ [field]: null
+ });
+ }
+ };
+
+ // Håndter ændring af switch felter (boolean)
+ const handleSwitchChange = (field) => (event) => {
+ setPackageData({
+ ...packageData,
+ [field]: event.target.checked
+ });
+ };
+
+ // Skift fane
+ const handleTabChange = (event, newValue) => {
+ setActiveTab(newValue);
+ };
+
+ // Skift trin
+ const handleStepChange = (step) => {
+ setActiveStep(step);
+ };
+
+ // Åbn task dialog (opret ny)
+ const handleAddTask = () => {
+ setEditingTask(null);
+ setTaskDialogOpen(true);
+ };
+
+ // Åbn task dialog (rediger eksisterende)
+ const handleEditTask = (task) => {
+ setEditingTask(task);
+ setTaskDialogOpen(true);
+ };
+
+ // Gem task (fra dialog)
+ const handleSaveTask = (taskData) => {
+ let updatedTasks;
+
+ if (editingTask) {
+ // Opdater eksisterende task
+ updatedTasks = packageData.tasks.map(task =>
+ task.id === editingTask.id ? { ...task, ...taskData } : task
+ );
+ } else {
+ // Tilføj ny task med temp id
+ const newTask = {
+ ...taskData,
+ id: `temp-${Date.now()}`, // Midlertidigt ID indtil gemming i database
+ steps: taskData.steps || []
+ };
+ updatedTasks = [...packageData.tasks, newTask];
+ }
+
+ setPackageData({
+ ...packageData,
+ tasks: updatedTasks
+ });
+
+ setTaskDialogOpen(false);
+ setEditingTask(null);
+ };
+
+ // Åbn material dialog (opret nyt)
+ const handleAddMaterial = () => {
+ setEditingMaterial(null);
+ setMaterialDialogOpen(true);
+ };
+
+ // Åbn material dialog (rediger eksisterende)
+ const handleEditMaterial = (material) => {
+ setEditingMaterial(material);
+ setMaterialDialogOpen(true);
+ };
+
+ // Gem material (fra dialog)
+ const handleSaveMaterial = (materialData) => {
+ let updatedMaterials;
+
+ if (editingMaterial) {
+ // Opdater eksisterende material
+ updatedMaterials = packageData.materials.map(material =>
+ material.id === editingMaterial.id ? { ...material, ...materialData } : material
+ );
+ } else {
+ // Tilføj nyt material med temp id
+ const newMaterial = {
+ ...materialData,
+ id: `temp-${Date.now()}` // Midlertidigt ID indtil gemming i database
+ };
+ updatedMaterials = [...packageData.materials, newMaterial];
+ }
+
+ setPackageData({
+ ...packageData,
+ materials: updatedMaterials
+ });
+
+ setMaterialDialogOpen(false);
+ setEditingMaterial(null);
+ };
+
+ // Bekræft sletning af task/material
+ const confirmDelete = (item, type) => {
+ setItemToDelete(item);
+ setDeleteType(type);
+ setConfirmDialogOpen(true);
+ };
+
+ // Udfør sletning
+ const handleDelete = () => {
+ if (deleteType === 'task') {
+ // Slet task
+ const updatedTasks = packageData.tasks.filter(task => task.id !== itemToDelete.id);
+ setPackageData({
+ ...packageData,
+ tasks: updatedTasks
+ });
+ } else if (deleteType === 'material') {
+ // Slet material
+ const updatedMaterials = packageData.materials.filter(material => material.id !== itemToDelete.id);
+ setPackageData({
+ ...packageData,
+ materials: updatedMaterials
+ });
+ }
+
+ setConfirmDialogOpen(false);
+ setItemToDelete(null);
+ setDeleteType('');
+ };
+
+ // Valider form
+ const validateForm = () => {
+ const newErrors = {};
+
+ if (!packageData.name) {
+ newErrors.name = 'Navn er påkrævet';
+ }
+
+ if (!packageData.description) {
+ newErrors.description = 'Beskrivelse er påkrævet';
+ }
+
+ if (!packageData.category) {
+ newErrors.category = 'Kategori er påkrævet';
+ }
+
+ if (!packageData.package_type) {
+ newErrors.package_type = 'Type er påkrævet';
+ }
+
+ if (packageData.estimated_hours === '' || packageData.estimated_hours < 0) {
+ newErrors.estimated_hours = 'Estimeret tid skal være et positivt tal';
+ }
+
+ if (packageData.hourly_rate === '' || packageData.hourly_rate < 0) {
+ newErrors.hourly_rate = 'Timepris skal være et positivt tal';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ // Gem pakke
+ const handleSave = async () => {
+ if (!validateForm()) {
+ enqueueSnackbar('Ret venligst fejlene i formularen', { variant: 'error' });
+ return;
+ }
+
+ setSaving(true);
+
+ try {
+ let response;
+
+ if (isEditMode) {
+ // Opdater eksisterende pakke
+ response = await axios.put(`/api/smart-packages/${id}`, packageData);
+ } else {
+ // Opret ny pakke
+ response = await axios.post('/api/smart-packages', packageData);
+ }
+
+ if (response.data.success) {
+ enqueueSnackbar(
+ isEditMode ? 'Pakke opdateret' : 'Ny pakke oprettet',
+ { variant: 'success' }
+ );
+ navigate('/smart-packages');
+ } else {
+ enqueueSnackbar('Fejl: ' + response.data.error, { variant: 'error' });
+ }
+ } catch (error) {
+ console.error('Fejl ved gemning af pakke:', error);
+ enqueueSnackbar('Fejl ved gemning af pakke', { variant: 'error' });
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ // Navigér tilbage
+ const handleBack = () => {
+ navigate('/smart-packages');
+ };
+
+ // Trin indhold
+ const renderStepContent = () => {
+ switch (activeStep) {
+ case 0: // Basisoplysninger
+ return (
+
+
+
+ Grundlæggende oplysninger
+
+
+
+
+
+
+
+
+
+
+ {categories.map((category) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+ {packageTypes.map((type) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ label="Dette er en skabelon"
+ />
+
+ Skabeloner bruges som udgangspunkt for nye pakker, men tilføjes ikke direkte til projekter
+
+
+
+ );
+
+ case 1: // Materialer
+ return (
+
+
+
+ Materialer
+
+ }
+ onClick={handleAddMaterial}
+ disabled={saving}
+ >
+ Tilføj materiale
+
+
+
+ {packageData.materials.length === 0 ? (
+
+
+ Ingen materialer tilføjet endnu
+
+ }
+ onClick={handleAddMaterial}
+ sx={{ mt: 1 }}
+ disabled={saving}
+ >
+ Tilføj det første materiale
+
+
+ ) : (
+
+ {packageData.materials.map((material, index) => (
+
+
+
+
+
+ {`${material.quantity} ${material.unit} - ${(material.custom_price || material.price).toFixed(2)} kr/stk`}
+
+ {material.quantity_formula && (
+
+ Formel: {material.quantity_formula}
+
+ )}
+
+ }
+ />
+
+ handleEditMaterial(material)}
+ disabled={saving}
+ sx={{ mr: 1 }}
+ >
+
+
+ confirmDelete(material, 'material')}
+ disabled={saving}
+ >
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ );
+
+ case 2: // Opgaver
+ return (
+
+
+
+ Opgaver
+
+ }
+ onClick={handleAddTask}
+ disabled={saving}
+ >
+ Tilføj opgave
+
+
+
+ {packageData.tasks.length === 0 ? (
+
+
+ Ingen opgaver tilføjet endnu
+
+ }
+ onClick={handleAddTask}
+ sx={{ mt: 1 }}
+ disabled={saving}
+ >
+ Tilføj den første opgave
+
+
+ ) : (
+
+ {packageData.tasks.map((task, index) => (
+
+
+
+
+
+ {index + 1}. {task.name}
+
+ {task.optional && (
+
+ Valgfri
+
+ )}
+
+ }
+ secondary={
+
+
+ {task.description}
+
+
+
+ Tid: {task.hours} timer
+
+ {task.tools && (
+
+ Værktøj: {task.tools}
+
+ )}
+ {task.steps && (
+
+ Trin: {task.steps.length}
+
+ )}
+
+
+ }
+ />
+
+ handleEditTask(task)}
+ disabled={saving}
+ sx={{ mr: 1 }}
+ >
+
+
+ confirmDelete(task, 'task')}
+ disabled={saving}
+ >
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ {packageData.tasks.length > 1 && (
+
+ }
+ disabled={saving}
+ >
+ Ændre rækkefølge
+
+
+ )}
+
+ );
+
+ default:
+ return null;
+ }
+ };
+
+ if (loading) {
+ return
;
+ }
+
+ return (
+
+ }
+ onClick={handleBack}
+ disabled={saving}
+ >
+ Tilbage
+
+ }
+ />
+
+
+
+ }
+ iconPosition="start"
+ disabled={saving}
+ />
+ }
+ iconPosition="start"
+ disabled={saving}
+ />
+ }
+ iconPosition="start"
+ disabled={saving}
+ />
+
+
+
+ {activeTab === 0 && (
+
+
+
+ Basisoplysninger
+
+ 0}>
+ Materialer
+
+ 0}>
+ Opgaver
+
+
+
+ {renderStepContent()}
+
+
+
+
+ {activeStep < 2 ? (
+
+ ) : (
+ }
+ disabled={saving}
+ onClick={handleSave}
+ >
+ {isEditMode ? 'Opdater pakke' : 'Opret pakke'}
+
+ )}
+
+
+ )}
+
+ {activeTab === 1 && (
+
+
+
+
+ Materialer
+
+
+
+
+
+ {packageData.materials.length} materialer tilføjet
+
+ }
+ onClick={handleAddMaterial}
+ disabled={saving}
+ >
+ Tilføj materiale
+
+
+
+ {packageData.materials.length === 0 ? (
+
+
+ Ingen materialer endnu
+
+
+ ) : (
+
+ {packageData.materials.map((material) => (
+
+ handleEditMaterial(material)}
+ disabled={saving}
+ sx={{ mr: 1 }}
+ >
+
+
+ confirmDelete(material, 'material')}
+ disabled={saving}
+ >
+
+
+
+ }
+ >
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ Opgaver
+
+
+
+
+
+ {packageData.tasks.length} opgaver tilføjet
+
+ }
+ onClick={handleAddTask}
+ disabled={saving}
+ >
+ Tilføj opgave
+
+
+
+ {packageData.tasks.length === 0 ? (
+
+
+ Ingen opgaver endnu
+
+
+ ) : (
+
+ {packageData.tasks.map((task, index) => (
+
+ handleEditTask(task)}
+ disabled={saving}
+ sx={{ mr: 1 }}
+ >
+
+
+ confirmDelete(task, 'task')}
+ disabled={saving}
+ >
+
+
+
+ }
+ >
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ }
+ onClick={handleSave}
+ disabled={saving}
+ >
+ {isEditMode ? 'Opdater pakke' : 'Opret pakke'}
+
+
+
+ )}
+
+ {activeTab === 2 && (
+
+
+
+
+
+
+
+
+
+ {categories.map((category) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+ {packageTypes.map((type) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ label="Dette er en skabelon"
+ />
+
+ Skabeloner bruges som udgangspunkt for nye pakker, men tilføjes ikke direkte til projekter
+
+
+
+
+
+
+
+
+
+ Materialer ({packageData.materials.length})
+
+
+
+ }
+ onClick={handleAddMaterial}
+ disabled={saving}
+ >
+ Tilføj materiale
+
+
+
+
+
+ {packageData.materials.length === 0 ? (
+
+
+
+ ) : (
+ packageData.materials.map((material) => (
+
+ handleEditMaterial(material)}
+ disabled={saving}
+ sx={{ mr: 1 }}
+ >
+
+
+ confirmDelete(material, 'material')}
+ disabled={saving}
+ >
+
+
+
+ }
+ >
+
+
+ ))
+ )}
+
+
+
+
+
+
+ Opgaver ({packageData.tasks.length})
+
+
+
+ }
+ onClick={handleAddTask}
+ disabled={saving}
+ >
+ Tilføj opgave
+
+
+
+
+
+ {packageData.tasks.length === 0 ? (
+
+
+
+ ) : (
+ packageData.tasks.map((task, index) => (
+
+ handleEditTask(task)}
+ disabled={saving}
+ sx={{ mr: 1 }}
+ >
+
+
+ confirmDelete(task, 'task')}
+ disabled={saving}
+ >
+
+
+
+ }
+ >
+
+
+ {task.hours} timer • {task.steps?.length || 0} trin
+
+ {task.optional && (
+
+ Valgfri opgave
+
+ )}
+
+ }
+ />
+
+ ))
+ )}
+
+
+
+
+
+
+ }
+ onClick={handleSave}
+ disabled={saving}
+ >
+ {isEditMode ? 'Opdater pakke' : 'Opret pakke'}
+
+
+
+ )}
+
+
+
+ {/* Task dialog */}
+ {taskDialogOpen && (
+ setTaskDialogOpen(false)}
+ onSave={handleSaveTask}
+ task={editingTask}
+ packageData={packageData}
+ />
+ )}
+
+ {/* Material dialog */}
+ {materialDialogOpen && (
+ setMaterialDialogOpen(false)}
+ onSave={handleSaveMaterial}
+ material={editingMaterial}
+ availableMaterials={availableMaterials}
+ />
+ )}
+
+ {/* Bekræft sletning dialog */}
+ setConfirmDialogOpen(false)}
+ />
+
+ );
+};
+
+export default SmartPackageForm;
\ No newline at end of file
diff --git a/frontend/src/components/smartPackages/SmartPackages.css b/frontend/src/components/smartPackages/SmartPackages.css
new file mode 100644
index 0000000..e94c42f
--- /dev/null
+++ b/frontend/src/components/smartPackages/SmartPackages.css
@@ -0,0 +1,242 @@
+.smart-packages-container {
+ padding: 20px;
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+/* Filter section styling */
+.smart-packages-filters {
+ background-color: #f9f9f9;
+ border-radius: 8px;
+ padding: 16px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+/* Smart Package cards styling */
+.smart-package-card {
+ position: relative;
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.smart-package-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+}
+
+.smart-package-card .MuiCardContent-root {
+ height: 180px;
+}
+
+.smart-package-card .card-top {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+
+.smart-package-card .card-title {
+ font-weight: 600;
+ color: #2c3e50;
+ margin-bottom: 8px;
+}
+
+.smart-package-card .card-category {
+ font-size: 12px;
+ color: #7f8c8d;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.smart-package-card .card-description {
+ height: 60px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ color: #34495e;
+ margin-bottom: 12px;
+}
+
+.smart-package-card .card-meta {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: auto;
+ position: absolute;
+ bottom: 16px;
+ left: 16px;
+ right: 16px;
+}
+
+.smart-package-card .card-meta-item {
+ display: flex;
+ align-items: center;
+ font-size: 12px;
+ color: #7f8c8d;
+}
+
+.smart-package-card .card-meta-item svg {
+ font-size: 16px;
+ margin-right: 4px;
+}
+
+/* Task styling */
+.task-item {
+ margin-bottom: 12px;
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.task-header {
+ display: flex;
+ justify-content: space-between;
+ padding: 12px 16px;
+ background-color: #f5f5f5;
+ border-bottom: 1px solid #e0e0e0;
+}
+
+.task-content {
+ padding: 16px;
+}
+
+.task-steps {
+ margin-top: 12px;
+}
+
+.task-step {
+ display: flex;
+ padding: 8px 0;
+ border-bottom: 1px dashed #e0e0e0;
+}
+
+.task-step:last-child {
+ border-bottom: none;
+}
+
+.task-step-number {
+ min-width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ background-color: #3498db;
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 12px;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+/* Detail dialog styling */
+.package-detail-dialog .dialog-section {
+ margin-bottom: 24px;
+}
+
+.package-detail-dialog .section-title {
+ font-size: 18px;
+ font-weight: 600;
+ margin-bottom: 16px;
+ color: #2c3e50;
+ display: flex;
+ align-items: center;
+}
+
+.package-detail-dialog .section-title svg {
+ margin-right: 8px;
+}
+
+.package-detail-dialog .price-summary {
+ background-color: #f9f9f9;
+ border-radius: 8px;
+ padding: 16px;
+}
+
+.package-detail-dialog .price-summary-row {
+ display: flex;
+ justify-content: space-between;
+ padding: 4px 0;
+}
+
+.package-detail-dialog .price-total {
+ font-size: 18px;
+ font-weight: bold;
+ padding-top: 8px;
+ margin-top: 8px;
+ border-top: 2px solid #e0e0e0;
+}
+
+/* Form styling */
+.smart-package-form .form-section {
+ margin-bottom: 32px;
+}
+
+.smart-package-form .form-subtitle {
+ font-size: 16px;
+ font-weight: 600;
+ margin-bottom: 16px;
+ color: #2c3e50;
+}
+
+.smart-package-form .form-helper-text {
+ font-size: 12px;
+ color: #7f8c8d;
+ margin-top: 4px;
+}
+
+/* Drag and drop styling */
+.draggable-item {
+ cursor: grab;
+ transition: background-color 0.2s;
+}
+
+.draggable-item:hover {
+ background-color: #f5f5f5;
+}
+
+.draggable-item.dragging {
+ background-color: #ebf5fb;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+/* Material selection styling */
+.material-chip {
+ margin: 4px;
+}
+
+.material-chip .MuiChip-label {
+ display: flex;
+ align-items: center;
+}
+
+.material-chip .material-quantity {
+ font-size: 11px;
+ background-color: rgba(0, 0, 0, 0.08);
+ padding: 2px 4px;
+ border-radius: 4px;
+ margin-left: 4px;
+}
+
+/* Responsive styling */
+@media (max-width: 960px) {
+ .smart-packages-filters {
+ flex-direction: column;
+ }
+
+ .smart-packages-filters .MuiFormControl-root {
+ margin-bottom: 16px;
+ }
+
+ .smart-package-card .MuiCardContent-root {
+ height: auto;
+ }
+
+ .smart-package-card .card-meta {
+ position: relative;
+ margin-top: 16px;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/components/smartPackages/SmartPackages.js b/frontend/src/components/smartPackages/SmartPackages.js
new file mode 100644
index 0000000..0e6b4a3
--- /dev/null
+++ b/frontend/src/components/smartPackages/SmartPackages.js
@@ -0,0 +1,564 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import {
+ Container, Typography, Paper, Grid, Card, CardContent, CardMedia, CardActionArea,
+ Button, Divider, Box, TextField, MenuItem, IconButton, Chip, Tooltip, LinearProgress,
+ Pagination
+} from '@mui/material';
+import './SmartPackages.css';
+import {
+ Add as AddIcon, Search as SearchIcon, FilterList as FilterIcon,
+ Sort as SortIcon, Category as CategoryIcon, Build as BuildIcon,
+ AccessTime as TimeIcon, AttachMoney as MoneyIcon, Star as StarIcon,
+ StarBorder as StarBorderIcon, Delete as DeleteIcon, Edit as EditIcon,
+ Visibility as ViewIcon
+} from '@mui/icons-material';
+import { useSnackbar } from 'notistack';
+import axios from 'axios';
+
+// Import komponenter
+import PageHeader from '../common/PageHeader';
+import LoadingOverlay from '../common/LoadingOverlay';
+import EmptyState from '../common/EmptyState';
+import ConfirmDialog from '../common/ConfirmDialog';
+import PackageDetailsDialog from './PackageDetailsDialog';
+
+// Hjælpefunktioner
+import { formatCurrency, formatTime } from '../../utils/formatters';
+
+/**
+ * Smart Pakker hovedkomponent
+ * Viser en oversigt over alle Smart Pakker med filtrering og sortering
+ */
+const SmartPackages = () => {
+ const navigate = useNavigate();
+ const { enqueueSnackbar } = useSnackbar();
+
+ // State variabler
+ const [packages, setPackages] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [categories, setCategories] = useState([]);
+ const [packageTypes, setPackageTypes] = useState([]);
+ const [totalPackages, setTotalPackages] = useState(0);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
+ const [selectedPackage, setSelectedPackage] = useState(null);
+ const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
+ const [packageToDelete, setPackageToDelete] = useState(null);
+
+ // Filtre og søgning
+ const [filters, setFilters] = useState({
+ search: '',
+ category: '',
+ packageType: '',
+ difficulty: '',
+ isTemplate: null,
+ sortBy: 'created_at',
+ sortDir: 'desc'
+ });
+
+ const pageSize = 12;
+
+ // Hent pakker
+ useEffect(() => {
+ fetchPackages();
+ fetchCategories();
+ }, [currentPage, filters]);
+
+ // Hent pakker fra API
+ const fetchPackages = async () => {
+ setLoading(true);
+ try {
+ const offset = (currentPage - 1) * pageSize;
+
+ const queryParams = new URLSearchParams({
+ limit: pageSize,
+ offset,
+ ...filters
+ });
+
+ // Fjern tomme filtre
+ Object.keys(filters).forEach(key => {
+ if (filters[key] === '' || filters[key] === null) {
+ queryParams.delete(key);
+ }
+ });
+
+ const response = await axios.get(`/api/smart-packages?${queryParams.toString()}`);
+
+ if (response.data.success) {
+ setPackages(response.data.packages);
+ setTotalPackages(response.data.total || response.data.packages.length);
+ } else {
+ enqueueSnackbar('Kunne ikke hente pakker: ' + response.data.error, { variant: 'error' });
+ }
+ } catch (error) {
+ console.error('Fejl ved hentning af smart pakker:', error);
+ enqueueSnackbar('Fejl ved hentning af pakker', { variant: 'error' });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Hent kategorier og pakke typer
+ const fetchCategories = async () => {
+ try {
+ const response = await axios.get('/api/smart-packages/categories');
+
+ if (response.data.success) {
+ setCategories(response.data.categories);
+ setPackageTypes(response.data.types);
+ }
+ } catch (error) {
+ console.error('Fejl ved hentning af kategorier:', error);
+ }
+ };
+
+ // Håndter filter ændringer
+ const handleFilterChange = (field) => (event) => {
+ setFilters({
+ ...filters,
+ [field]: event.target.value
+ });
+ setCurrentPage(1); // Nulstil til side 1 ved filterændringer
+ };
+
+ // Ryd alle filtre
+ const clearFilters = () => {
+ setFilters({
+ search: '',
+ category: '',
+ packageType: '',
+ difficulty: '',
+ isTemplate: null,
+ sortBy: 'created_at',
+ sortDir: 'desc'
+ });
+ setCurrentPage(1);
+ };
+
+ // Side skift
+ const handlePageChange = (event, newPage) => {
+ setCurrentPage(newPage);
+ };
+
+ // Vis pakke detaljer
+ const handleViewPackage = (packageItem) => {
+ setSelectedPackage(packageItem);
+ setDetailsDialogOpen(true);
+ };
+
+ // Rediger pakke
+ const handleEditPackage = (packageId) => {
+ navigate(`/smart-packages/edit/${packageId}`);
+ };
+
+ // Opret ny pakke
+ const handleCreatePackage = () => {
+ navigate('/smart-packages/create');
+ };
+
+ // Bekræft sletning
+ const confirmDeletePackage = (packageItem) => {
+ setPackageToDelete(packageItem);
+ setConfirmDialogOpen(true);
+ };
+
+ // Slet pakke
+ const deletePackage = async () => {
+ if (!packageToDelete) return;
+
+ try {
+ const response = await axios.delete(`/api/smart-packages/${packageToDelete.id}`);
+
+ if (response.data.success) {
+ enqueueSnackbar('Pakken blev slettet', { variant: 'success' });
+ fetchPackages(); // Genindlæs listen
+ } else {
+ enqueueSnackbar('Kunne ikke slette pakke: ' + response.data.error, { variant: 'error' });
+ }
+ } catch (error) {
+ console.error('Fejl ved sletning af pakke:', error);
+ enqueueSnackbar('Fejl ved sletning af pakke', { variant: 'error' });
+ } finally {
+ setConfirmDialogOpen(false);
+ setPackageToDelete(null);
+ }
+ };
+
+ // Få farve baseret på vanskelighed
+ const getDifficultyColor = (level) => {
+ switch (level) {
+ case 1: return '#4caf50'; // Let (grøn)
+ case 2: return '#2196f3'; // Middel (blå)
+ case 3: return '#ff9800'; // Svær (orange)
+ case 4: return '#f44336'; // Ekspert (rød)
+ default: return '#757575'; // Standard (grå)
+ }
+ };
+
+ // Få vanskeligheds tekst
+ const getDifficultyText = (level) => {
+ switch (level) {
+ case 1: return 'Let';
+ case 2: return 'Middel';
+ case 3: return 'Svær';
+ case 4: return 'Ekspert';
+ default: return 'Ukendt';
+ }
+ };
+
+ // Render kategori chip
+ const renderCategoryChip = (category) => {
+ const categoryObj = categories.find(cat => cat.name === category);
+ const icon = categoryObj?.icon || 'category';
+
+ return (
+
}
+ label={category}
+ size="small"
+ variant="outlined"
+ sx={{ mr: 1 }}
+ />
+ );
+ };
+
+ return (
+
+ }
+ onClick={handleCreatePackage}
+ >
+ Opret ny pakke
+
+ }
+ />
+
+
+
+
+
+ }}
+ />
+
+
+
+
+
+ {categories.map((category) => (
+
+ ))}
+
+
+
+
+
+
+ {packageTypes.map((type) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {loading ? (
+
+ ) : packages.length === 0 ? (
+ }
+ onClick={handleCreatePackage}
+ >
+ Opret ny pakke
+
+ }
+ />
+ ) : (
+ <>
+
+ {packages.map((packageItem) => (
+
+
+ {packageItem.is_template && (
+
+ )}
+
+ handleViewPackage(packageItem)}
+ sx={{ flexGrow: 1 }}
+ >
+
+
+ {packageItem.name.substring(0, 2).toUpperCase()}
+
+
+
+
+
+ {packageItem.name}
+
+
+
+ {packageItem.description}
+
+
+
+ {packageItem.category && renderCategoryChip(packageItem.category)}
+
+ }
+ label={packageItem.task_count || 0}
+ size="small"
+ title="Antal opgaver"
+ sx={{ mr: 1, mb: 1 }}
+ />
+
+ }
+ label={formatTime(packageItem.estimated_hours)}
+ size="small"
+ title="Estimeret tid"
+ sx={{ mr: 1, mb: 1 }}
+ />
+
+
+
+
+
+
+
+
+
+
+ {[...Array(packageItem.difficulty_level || 1)].map((_, i) => (
+
+ ))}
+ {[...Array(4 - (packageItem.difficulty_level || 1))].map((_, i) => (
+
+ ))}
+
+
+
+
+
+ {formatCurrency(packageItem.total_package_price || 0)}
+
+
+
+
+
+
+
+ handleViewPackage(packageItem)}
+ >
+
+
+
+
+
+ handleEditPackage(packageItem.id)}
+ >
+
+
+
+
+
+ confirmDeletePackage(packageItem)}
+ >
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ >
+ )}
+
+ {/* Pakke detalje dialog */}
+ {selectedPackage && (
+ setDetailsDialogOpen(false)}
+ packageId={selectedPackage.id}
+ onEdit={() => {
+ setDetailsDialogOpen(false);
+ handleEditPackage(selectedPackage.id);
+ }}
+ />
+ )}
+
+ {/* Bekræft sletning dialog */}
+ setConfirmDialogOpen(false)}
+ />
+
+ );
+};
+
+export default SmartPackages;
\ No newline at end of file
diff --git a/frontend/src/components/smartPackages/SmartPackagesMenu.js b/frontend/src/components/smartPackages/SmartPackagesMenu.js
new file mode 100644
index 0000000..2397080
--- /dev/null
+++ b/frontend/src/components/smartPackages/SmartPackagesMenu.js
@@ -0,0 +1,202 @@
+import React, { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import {
+ Box,
+ List,
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ Paper,
+ Divider,
+ Typography,
+ Collapse,
+} from '@mui/material';
+import {
+ Dashboard as DashboardIcon,
+ Category as CategoryIcon,
+ Assignment as TasksIcon,
+ Build as BuildIcon,
+ ExpandLess,
+ ExpandMore,
+ AddCircle as AddIcon,
+ Settings as SettingsIcon,
+ Backup as BackupIcon,
+ Assessment as AssessmentIcon
+} from '@mui/icons-material';
+
+/**
+ * SmartPackagesMenu Component
+ *
+ * Sidepanel menu til Smart Pakker modulet med kategorier, navigation
+ * og genveje til de mest brugte funktioner.
+ */
+const SmartPackagesMenu = ({ activeSection }) => {
+ const [openCategories, setOpenCategories] = useState(true);
+ const [openSettings, setOpenSettings] = useState(false);
+ const [version, setVersion] = useState('1.0.0');
+
+ // Toggle åben/lukket tilstand for en menu-sektion
+ const handleToggle = (section) => {
+ if (section === 'categories') {
+ setOpenCategories(!openCategories);
+ } else if (section === 'settings') {
+ setOpenSettings(!openSettings);
+ }
+ };
+
+ // Hent version fra API eller load fra lokalt storage
+ useEffect(() => {
+ // For nu bruger vi en hardcoded version
+ // I fremtiden kan denne hentes fra backend
+ setVersion('1.2.0');
+ }, []);
+
+ return (
+
+
+
+ Smart Pakker
+
+
+ Modulbaseret byggesystem
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Kategorier sektion */}
+ handleToggle('categories')}>
+
+
+
+
+ {openCategories ? : }
+
+
+
+
+ {['Tilbygning', 'Ombygning', 'Renovering', 'Reparation', 'Nybygning'].map((category) => (
+
+
+
+ ))}
+
+
+
+
+
+ {/* Opgaver og elementer */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Indstillinger sektion */}
+ handleToggle('settings')}>
+
+
+
+
+ {openSettings ? : }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Smart Pakker v{version}
+
+
+
+ );
+};
+
+export default SmartPackagesMenu;
\ No newline at end of file
diff --git a/frontend/src/components/smartPackages/TaskManagement.js b/frontend/src/components/smartPackages/TaskManagement.js
new file mode 100644
index 0000000..63e376e
--- /dev/null
+++ b/frontend/src/components/smartPackages/TaskManagement.js
@@ -0,0 +1,271 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Container,
+ Typography,
+ Box,
+ Paper,
+ Grid,
+ Button,
+ TextField,
+ Divider,
+ LinearProgress,
+ Alert,
+ Card,
+ CardContent,
+ CardActions,
+ IconButton
+} from '@mui/material';
+import {
+ Add as AddIcon,
+ Edit as EditIcon,
+ Delete as DeleteIcon,
+ Search as SearchIcon,
+ Build as BuildIcon,
+ AccessTime as TimeIcon,
+ AttachMoney as MoneyIcon
+} from '@mui/icons-material';
+
+import TaskDialog from './dialogs/TaskDialog';
+import PageHeader from '../common/PageHeader';
+import EmptyState from '../common/EmptyState';
+
+/**
+ * TaskManagement Component
+ *
+ * Administration af opgaver til Smart Pakker systemet
+ * Bruges til at skabe, redigere og organisere opgaver
+ */
+const TaskManagement = ({ apiBaseUrl }) => {
+ // State variabler
+ const [tasks, setTasks] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [currentTask, setCurrentTask] = useState(null);
+ const [isNewTask, setIsNewTask] = useState(false);
+
+ // Hent opgaver ved indlæsning
+ useEffect(() => {
+ fetchTasks();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ // Hent alle opgaver fra API
+ const fetchTasks = async () => {
+ setLoading(true);
+ try {
+ const response = await fetch(`${apiBaseUrl}/api/smart-packages/tasks`);
+ const data = await response.json();
+
+ if (data.success) {
+ setTasks(data.tasks || []);
+ } else {
+ console.error('Fejl ved hentning af opgaver:', data.error);
+ }
+ } catch (error) {
+ console.error('Fejl ved hentning af opgaver:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Åbn dialog for at oprette en ny opgave
+ const handleCreateTask = () => {
+ setCurrentTask({});
+ setIsNewTask(true);
+ setDialogOpen(true);
+ };
+
+ // Åbn dialog for at redigere en eksisterende opgave
+ const handleEditTask = (task) => {
+ setCurrentTask(task);
+ setIsNewTask(false);
+ setDialogOpen(true);
+ };
+
+ // Håndter sletning af en opgave
+ const handleDeleteTask = async (taskId) => {
+ if (!window.confirm('Er du sikker på at du vil slette denne opgave?')) {
+ return;
+ }
+
+ try {
+ const response = await fetch(`${apiBaseUrl}/api/smart-packages/tasks/${taskId}`, {
+ method: 'DELETE'
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ // Opdater listen efter sletning
+ fetchTasks();
+ } else {
+ console.error('Fejl ved sletning af opgave:', data.error);
+ }
+ } catch (error) {
+ console.error('Fejl ved sletning af opgave:', error);
+ }
+ };
+
+ // Gem opgave (ny eller redigeret)
+ const handleSaveTask = async (taskData, steps) => {
+ try {
+ let response;
+
+ if (isNewTask) {
+ // Opret ny opgave
+ response = await fetch(`${apiBaseUrl}/api/smart-packages/tasks`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ task: taskData, steps })
+ });
+ } else {
+ // Opdater eksisterende opgave
+ response = await fetch(`${apiBaseUrl}/api/smart-packages/tasks/${taskData.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ task: taskData, steps })
+ });
+ }
+
+ const data = await response.json();
+
+ if (data.success) {
+ // Luk dialogen og opdater listen
+ setDialogOpen(false);
+ fetchTasks();
+ } else {
+ console.error('Fejl ved gem af opgave:', data.error);
+ }
+ } catch (error) {
+ console.error('Fejl ved gem af opgave:', error);
+ }
+ };
+
+ // Filtrerede opgaver baseret på søgning
+ const filteredTasks = tasks.filter(task =>
+ task.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ task.description.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+
+ return (
+
+ }
+ />
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ InputProps={{
+ startAdornment:
+ }}
+ />
+
+
+ }
+ onClick={handleCreateTask}
+ >
+ Opret ny opgave
+
+
+
+
+
+
+ {loading ? (
+
+
+
+ ) : filteredTasks.length === 0 ? (
+ }
+ />
+ ) : (
+
+ {filteredTasks.map(task => (
+
+
+
+
+ {task.name}
+
+
+
+ {task.description || 'Ingen beskrivelse'}
+
+
+
+
+
+
+
+ {task.hours} timer
+
+
+
+
+
+
+ {task.rate} kr/time
+
+
+
+ {task.optional && (
+
+ Valgfri opgave
+
+ )}
+
+
+
+ handleEditTask(task)}
+ >
+
+
+ handleDeleteTask(task.id)}
+ >
+
+
+
+
+
+ ))}
+
+ )}
+
+ {/* Opgave Dialog */}
+ setDialogOpen(false)}
+ onSave={handleSaveTask}
+ task={currentTask}
+ isNew={isNewTask}
+ apiBaseUrl={apiBaseUrl}
+ />
+
+ );
+};
+
+export default TaskManagement;
\ No newline at end of file
diff --git a/frontend/src/components/smartPackages/dialogs/MaterialDialog.js b/frontend/src/components/smartPackages/dialogs/MaterialDialog.js
new file mode 100644
index 0000000..dc058ee
--- /dev/null
+++ b/frontend/src/components/smartPackages/dialogs/MaterialDialog.js
@@ -0,0 +1,342 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Dialog, DialogTitle, DialogContent, DialogActions, Button,
+ TextField, Grid, Typography, Box, FormHelperText, Autocomplete,
+ FormControlLabel, Switch, InputAdornment
+} from '@mui/material';
+import { useSnackbar } from 'notistack';
+
+/**
+ * Dialog til at oprette eller redigere et materiale i en smart pakke
+ */
+const MaterialDialog = ({ open, onClose, onSave, material, availableMaterials }) => {
+ const { enqueueSnackbar } = useSnackbar();
+ const isEditMode = Boolean(material);
+
+ // State for material data
+ const [materialData, setMaterialData] = useState({
+ material_id: '',
+ material_name: '',
+ description: '',
+ quantity: 1,
+ unit: 'stk',
+ price: 0,
+ custom_price: null,
+ quantity_formula: '',
+ use_formula: false,
+ optional: false
+ });
+
+ // State for valgt materiale fra autocomplete
+ const [selectedMaterial, setSelectedMaterial] = useState(null);
+
+ // Validering
+ const [errors, setErrors] = useState({});
+
+ // Indlæs data hvis det er en redigering
+ useEffect(() => {
+ if (material) {
+ setMaterialData({
+ ...material,
+ use_formula: Boolean(material.quantity_formula)
+ });
+
+ // Find det valgte materiale fra availableMaterials baseret på material_id
+ if (material.material_id && availableMaterials?.length > 0) {
+ const foundMaterial = availableMaterials.find(m =>
+ m.id === material.material_id || m.id === parseInt(material.material_id)
+ );
+ setSelectedMaterial(foundMaterial || null);
+ }
+ }
+ }, [material, availableMaterials]);
+
+ // Håndter valg af materiale fra autocomplete
+ const handleMaterialSelect = (event, value) => {
+ setSelectedMaterial(value);
+
+ if (value) {
+ setMaterialData({
+ ...materialData,
+ material_id: value.id,
+ material_name: value.name,
+ description: value.description || '',
+ unit: value.unit || 'stk',
+ price: value.price || 0
+ });
+
+ // Fjern material_id fejl
+ if (errors.material_id) {
+ setErrors({
+ ...errors,
+ material_id: null
+ });
+ }
+ } else {
+ // Nulstil materiale data hvis der ikke er valgt noget
+ setMaterialData({
+ ...materialData,
+ material_id: '',
+ material_name: '',
+ description: '',
+ unit: 'stk',
+ price: 0
+ });
+ }
+ };
+
+ // Håndter ændringer i inputfelter
+ const handleInputChange = (field) => (event) => {
+ setMaterialData({
+ ...materialData,
+ [field]: ['quantity', 'price', 'custom_price'].includes(field)
+ ? parseFloat(event.target.value) || 0
+ : event.target.value
+ });
+
+ // Fjern fejl for dette felt
+ if (errors[field]) {
+ setErrors({
+ ...errors,
+ [field]: null
+ });
+ }
+ };
+
+ // Håndter ændring af switch felter (boolean)
+ const handleSwitchChange = (field) => (event) => {
+ const checked = event.target.checked;
+
+ setMaterialData({
+ ...materialData,
+ [field]: checked
+ });
+
+ // Hvis use_formula bliver slået fra, nulstil formula
+ if (field === 'use_formula' && !checked) {
+ setMaterialData(prev => ({
+ ...prev,
+ quantity_formula: ''
+ }));
+ }
+ };
+
+ // Validér form
+ const validateForm = () => {
+ const newErrors = {};
+
+ if (!materialData.material_id && !materialData.material_name) {
+ newErrors.material_id = 'Vælg et materiale';
+ }
+
+ if (materialData.quantity <= 0 && !materialData.use_formula) {
+ newErrors.quantity = 'Antal skal være større end 0';
+ }
+
+ if (materialData.use_formula && !materialData.quantity_formula) {
+ newErrors.quantity_formula = 'Angiv en formel';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ // Håndter gem
+ const handleSave = () => {
+ if (!validateForm()) {
+ enqueueSnackbar('Ret venligst fejlene i formularen', { variant: 'error' });
+ return;
+ }
+
+ // Fjern unødvendig data
+ const dataToSave = {
+ ...materialData,
+ // Hvis der ikke bruges formel, fjern formlen
+ quantity_formula: materialData.use_formula ? materialData.quantity_formula : null
+ };
+
+ onSave(dataToSave);
+ };
+
+ return (
+
+ );
+};
+
+export default MaterialDialog;
\ No newline at end of file
diff --git a/frontend/src/components/smartPackages/dialogs/TaskDialog.css b/frontend/src/components/smartPackages/dialogs/TaskDialog.css
new file mode 100644
index 0000000..3a7b24d
--- /dev/null
+++ b/frontend/src/components/smartPackages/dialogs/TaskDialog.css
@@ -0,0 +1,103 @@
+.task-dialog .MuiDialogTitle-root {
+ background-color: #f8f9fa;
+ border-bottom: 1px solid #e0e0e0;
+}
+
+.task-dialog .step-card {
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ margin-bottom: 16px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
+
+.task-dialog .step-card-header {
+ padding: 12px 16px;
+ background-color: #f5f5f5;
+ border-bottom: 1px solid #e0e0e0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.task-dialog .step-card-content {
+ padding: 16px;
+}
+
+.task-dialog .step-drag-handle {
+ cursor: grab;
+ color: #9e9e9e;
+}
+
+.task-dialog .step-drag-handle:active {
+ cursor: grabbing;
+}
+
+.task-dialog .safety-note {
+ background-color: #ffebee;
+ border-left: 4px solid #f44336;
+ padding: 8px 16px;
+ margin: 8px 0;
+}
+
+.task-dialog .quality-check {
+ background-color: #e8f5e9;
+ border-left: 4px solid #4caf50;
+ padding: 8px 16px;
+ margin: 8px 0;
+}
+
+.task-dialog .step-tip {
+ background-color: #fff8e1;
+ border-left: 4px solid #ffc107;
+ padding: 8px 16px;
+ margin: 8px 0;
+}
+
+.task-dialog .total-time-display {
+ display: flex;
+ align-items: center;
+ color: #455a64;
+ font-weight: 500;
+}
+
+.task-dialog .total-time-display svg {
+ margin-right: 8px;
+ color: #607d8b;
+}
+
+.task-dialog .time-indicator {
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+ color: #78909c;
+ margin-top: 4px;
+}
+
+.task-dialog .time-indicator svg {
+ font-size: 16px;
+ margin-right: 4px;
+ color: #90a4ae;
+}
+
+/* Animation for step transitions */
+.task-dialog .MuiCollapse-root {
+ transition: height 0.3s ease-in-out;
+}
+
+.task-dialog .MuiStepper-root .MuiStepLabel-label {
+ font-weight: 500;
+}
+
+/* Draggable step styles */
+.dragging-step {
+ opacity: 0.7;
+ transform: scale(0.98);
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1) !important;
+}
+
+/* Empty state when no steps */
+.task-dialog .empty-steps {
+ text-align: center;
+ padding: 32px 0;
+ color: #9e9e9e;
+}
\ No newline at end of file
diff --git a/frontend/src/components/smartPackages/dialogs/TaskDialog.js b/frontend/src/components/smartPackages/dialogs/TaskDialog.js
new file mode 100644
index 0000000..f2d810b
--- /dev/null
+++ b/frontend/src/components/smartPackages/dialogs/TaskDialog.js
@@ -0,0 +1,345 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Dialog, DialogTitle, DialogContent, DialogActions, Button,
+ TextField, FormControlLabel, Switch, Grid, Typography,
+ IconButton, List, ListItem, ListItemText, ListItemSecondaryAction,
+ Box, FormHelperText, Card, CardContent, Stepper, Step, StepLabel,
+ StepContent, Divider, Tooltip, InputAdornment, Paper
+} from '@mui/material';
+import {
+ Add as AddIcon,
+ Delete as DeleteIcon,
+ Edit as EditIcon,
+ SwapVert as SwapVertIcon,
+ Info as InfoIcon,
+ Build as BuildIcon,
+ Assignment as AssignmentIcon,
+ Construction as ConstructionIcon,
+ Timer as TimerIcon,
+ CheckCircle as CheckCircleIcon
+} from '@mui/icons-material';
+import { useSnackbar } from 'notistack';
+
+/**
+ * Dialog til at oprette eller redigere en opgave i en smart pakke
+ */
+const TaskDialog = ({ open, onClose, onSave, task, packageData }) => {
+ const { enqueueSnackbar } = useSnackbar();
+ const isEditMode = Boolean(task);
+
+ // State for task data
+ const [taskData, setTaskData] = useState({
+ name: '',
+ description: '',
+ hours: 1,
+ rate: 580,
+ difficulty_level: 2,
+ skills: '',
+ tools: '',
+ optional: false,
+ depends_on: '',
+ task_order: 0,
+ steps: []
+ });
+
+ // State for step der bliver redigeret
+ const [newStep, setNewStep] = useState('');
+
+ // State for det aktive trin i processen
+ const [activeStep, setActiveStep] = useState(0);
+
+ // State for step detaljer
+ const [stepDetails, setStepDetails] = useState({
+ title: '',
+ description: '',
+ minutes: 15,
+ image: '',
+ video: '',
+ safety: '',
+ quality: '',
+ tips: ''
+ });
+
+ // Validering
+ const [errors, setErrors] = useState({});
+
+ // Indlæs data hvis det er en redigering
+ useEffect(() => {
+ if (task) {
+ setTaskData({
+ ...task,
+ steps: task.steps || []
+ });
+ }
+ }, [task]);
+
+ // Håndter ændringer i inputfelter
+ const handleInputChange = (field) => (event) => {
+ setTaskData({
+ ...taskData,
+ [field]: field === 'hours' ? parseFloat(event.target.value) || 0 : event.target.value
+ });
+
+ // Fjern fejl for dette felt
+ if (errors[field]) {
+ setErrors({
+ ...errors,
+ [field]: null
+ });
+ }
+ };
+
+ // Håndter ændring af switch felter (boolean)
+ const handleSwitchChange = (field) => (event) => {
+ setTaskData({
+ ...taskData,
+ [field]: event.target.checked
+ });
+ };
+
+ // Tilføj et nyt step
+ const handleAddStep = () => {
+ if (!newStep.trim()) {
+ enqueueSnackbar('Trin kan ikke være tomt', { variant: 'error' });
+ return;
+ }
+
+ setTaskData({
+ ...taskData,
+ steps: [
+ ...taskData.steps,
+ {
+ id: `temp-step-${Date.now()}`,
+ description: newStep.trim(),
+ order: taskData.steps.length + 1
+ }
+ ]
+ });
+
+ setNewStep('');
+ };
+
+ // Slet et step
+ const handleDeleteStep = (stepId) => {
+ const updatedSteps = taskData.steps.filter(step => step.id !== stepId);
+
+ // Opdater order på de resterende steps
+ const reorderedSteps = updatedSteps.map((step, index) => ({
+ ...step,
+ order: index + 1
+ }));
+
+ setTaskData({
+ ...taskData,
+ steps: reorderedSteps
+ });
+ };
+
+ // Validér form
+ const validateForm = () => {
+ const newErrors = {};
+
+ if (!taskData.name) {
+ newErrors.name = 'Navn er påkrævet';
+ }
+
+ if (!taskData.description) {
+ newErrors.description = 'Beskrivelse er påkrævet';
+ }
+
+ if (taskData.hours <= 0) {
+ newErrors.hours = 'Timer skal være større end 0';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ // Håndter gem
+ const handleSave = () => {
+ if (!validateForm()) {
+ enqueueSnackbar('Ret venligst fejlene i formularen', { variant: 'error' });
+ return;
+ }
+
+ onSave(taskData);
+ };
+
+ return (
+
+ );
+};
+
+export default TaskDialog;
\ No newline at end of file
diff --git a/frontend/src/routes/SmartPackagesRoutes.js b/frontend/src/routes/SmartPackagesRoutes.js
new file mode 100644
index 0000000..2ac8890
--- /dev/null
+++ b/frontend/src/routes/SmartPackagesRoutes.js
@@ -0,0 +1,124 @@
+import React, { useState, useEffect } from 'react';
+import { Routes, Route, useNavigate, useLocation } from 'react-router-dom';
+import { Box, Button, Container, Grid, Typography } from '@mui/material';
+import { Add as AddIcon } from '@mui/icons-material';
+
+// Import smart package komponenter
+import SmartPackages from '../components/smartPackages/SmartPackages';
+import SmartPackageForm from '../components/smartPackages/SmartPackageForm';
+import SmartPackagesMenu from '../components/smartPackages/SmartPackagesMenu';
+import TaskManagement from '../components/smartPackages/TaskManagement';
+
+/**
+ * Router komponent for Smart Pakker, håndterer navigation og routing
+ */
+const SmartPackagesRoutes = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const [showAddButton, setShowAddButton] = useState(false);
+ const [activeSection, setActiveSection] = useState('dashboard');
+ const [isLoading, setIsLoading] = useState(true);
+ const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:4031';
+
+ // Opdaterer når URL ændres for at vise/skjule knappen og sætte aktiv sektion
+ useEffect(() => {
+ // Viser kun knappen på hovedlisten, ikke på formular-sider
+ const currentPath = location.pathname;
+ setShowAddButton(currentPath === '/smart-packages');
+
+ // Sæt aktivt menuitem baseret på URL
+ if (currentPath.includes('/create')) {
+ setActiveSection('create');
+ } else if (currentPath.includes('/tasks')) {
+ setActiveSection('tasks');
+ } else if (currentPath.includes('/components')) {
+ setActiveSection('components');
+ } else {
+ setActiveSection('dashboard');
+ }
+
+ // Simuler data loading
+ setIsLoading(true);
+ setTimeout(() => {
+ setIsLoading(false);
+ }, 500);
+ }, [location]);
+
+ // Navigerer til oprettelsesformular
+ const handleCreatePackage = () => {
+ navigate('/smart-packages/create');
+ };
+
+ return (
+
+
+
+ {/* Sidemenu */}
+
+
+
+
+ {/* Hovedindhold */}
+
+ {isLoading ? (
+
+
+
+
+
+ Indlæser Smart Pakker...
+
+
+ ) : (
+
+ } />
+ } />
+ } />
+ } />
+ Komponenter kommer snart } />
+
+ )}
+
+
+
+ {showAddButton && (
+