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)
249 lines
7.1 KiB
JavaScript
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;
|