Files
tilbudgivern/frontend/src/components/AllocationModal.js
T
alexpolo1 a03b6df348 feat: Crew scheduling system (Phase 1-3) - 8.5/10 rating
Phase 1: Quick Wins
- Calendar default tab, simplified 3-option filters
- Show all employees including free ones

Phase 2: Core Functionality
- POST /api/calendar/allocate endpoint
- AllocationModal component with validation
- Integration with Ordrestyring GraphQL API

Phase 2.5: UX Polish
- Prominent employee names with type badges
- NotificationToast system for success/error feedback
- Auto-refresh after allocation
- Removed confusing employee-type filter

Phase 3: Enhanced UX
- WhosFreeWidget with tabs (I dag | I morgen | Denne uge)
- Job names displayed on busy employees
- Quick-allocate with employee pre-selection
- Fixed undefined name sorting bugs

Lars validation: 8.5/10 - 'Replaces whiteboard for 90% of planning'
Real impact: 97% faster than manual process (5s vs 2-3 min)
2026-01-22 21:59:34 +00:00

249 lines
7.1 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
FormControl,
InputLabel,
Select,
MenuItem,
TextField,
Box,
Alert,
CircularProgress
} from '@mui/material';
import axios from 'axios';
/**
* AllocationModal - Modal for allocating employees to cases
* Used in PlanningDashboard to assign crew members to projects
*/
const AllocationModal = ({ open, onClose, onSuccess, selectedCase = null, preSelectedEmployee = null }) => {
const [employees, setEmployees] = useState([]);
const [selectedEmployee, setSelectedEmployee] = useState('');
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [hours, setHours] = useState('8');
const [description, setDescription] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [fetchingEmployees, setFetchingEmployees] = useState(false);
// Fetch employees when modal opens
useEffect(() => {
if (open) {
fetchEmployees();
// Set today as default start date
const today = new Date().toISOString().split('T')[0];
setStartDate(today);
setEndDate(''); // Single day allocation by default
setDescription(selectedCase ? `Arbejde på ${selectedCase.case_number}` : '');
// Pre-select employee if provided (from Who's Free widget)
if (preSelectedEmployee) {
setSelectedEmployee(preSelectedEmployee.id);
}
}
}, [open, selectedCase, preSelectedEmployee]);
const fetchEmployees = async () => {
setFetchingEmployees(true);
try {
const response = await axios.get('/api/planning/employees');
setEmployees(response.data.employees || response.data.data || []);
} catch (err) {
console.error('Failed to fetch employees:', err);
setError('Kunne ikke hente medarbejdere');
} finally {
setFetchingEmployees(false);
}
};
const handleAllocate = async () => {
// Validation
if (!selectedEmployee) {
setError('Vælg en medarbejder');
return;
}
if (!startDate) {
setError('Vælg startdato');
return;
}
if (!selectedCase) {
setError('Ingen sag valgt');
return;
}
setLoading(true);
setError(null);
try {
const response = await axios.post('/api/calendar/allocate', {
userId: selectedEmployee,
caseId: selectedCase.id,
startDate: startDate,
endDate: endDate || null,
hours: parseFloat(hours),
description: description
});
if (response.data.success) {
// Show success notification
const successMsg = `✅ ${response.data.data.userName} allokeret til ${response.data.data.caseNumber}`;
window.dispatchEvent(new CustomEvent('allocationSuccess', { detail: { message: successMsg } }));
// Reset form
setSelectedEmployee('');
setStartDate('');
setEndDate('');
setHours('8');
setDescription('');
// Notify parent component
if (onSuccess) {
onSuccess(response.data.data);
}
onClose();
} else {
setError(response.data.error || 'Fejl ved allokering');
}
} catch (err) {
console.error('Allocation failed:', err);
setError(err.response?.data?.error || 'Kunne ikke allokere medarbejder');
} finally {
setLoading(false);
}
};
const handleClose = () => {
if (!loading) {
setError(null);
setSelectedEmployee('');
setStartDate('');
setEndDate('');
setHours('8');
setDescription('');
onClose();
}
};
return (
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>
Allokér Medarbejder
{selectedCase && (
<Box sx={{ fontSize: '0.9rem', color: 'text.secondary', mt: 0.5 }}>
Sag: {selectedCase.case_number} - {selectedCase.yourref}
</Box>
)}
</DialogTitle>
<DialogContent>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
{/* Employee Selection */}
<FormControl fullWidth disabled={fetchingEmployees || loading}>
<InputLabel>Medarbejder</InputLabel>
<Select
value={selectedEmployee}
onChange={(e) => setSelectedEmployee(e.target.value)}
label="Medarbejder"
>
{fetchingEmployees ? (
<MenuItem disabled>
<CircularProgress size={20} sx={{ mr: 1 }} />
Henter medarbejdere...
</MenuItem>
) : employees.length === 0 ? (
<MenuItem disabled>Ingen medarbejdere fundet</MenuItem>
) : (
employees.map((emp) => (
<MenuItem key={emp.id} value={emp.id}>
{emp.full_name || emp.first_name + ' ' + emp.last_name}
</MenuItem>
))
)}
</Select>
</FormControl>
{/* Start Date */}
<TextField
fullWidth
label="Startdato"
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
InputLabelProps={{ shrink: true }}
disabled={loading}
/>
{/* End Date (optional) */}
<TextField
fullWidth
label="Slutdato (valgfri)"
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
InputLabelProps={{ shrink: true }}
disabled={loading}
helperText="Lad tom for enkelt dag"
/>
{/* Hours */}
<TextField
fullWidth
label="Timer"
type="number"
value={hours}
onChange={(e) => setHours(e.target.value)}
inputProps={{ min: 0.5, max: 24, step: 0.5 }}
disabled={loading}
/>
{/* Description */}
<TextField
fullWidth
label="Beskrivelse"
multiline
rows={2}
value={description}
onChange={(e) => setDescription(e.target.value)}
disabled={loading}
placeholder="F.eks. 'Tagdækning' eller 'Montage af vinduer'"
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={loading}>
Annuller
</Button>
<Button
onClick={handleAllocate}
variant="contained"
disabled={loading || fetchingEmployees || !selectedEmployee || !startDate}
>
{loading ? (
<>
<CircularProgress size={20} sx={{ mr: 1 }} />
Allokerer...
</>
) : (
'Allokér'
)}
</Button>
</DialogActions>
</Dialog>
);
};
export default AllocationModal;