mirror of
https://github.com/Alvin-Zilverstand/challenge-11.git
synced 2026-03-06 02:56:27 +01:00
Enhance CarModifications component by integrating API calls for fetching modifications and customers, implementing a dialog for adding modifications to customers, and improving error and success message handling. Refactor state management and UI elements for better user experience.
This commit is contained in:
4
.env
4
.env
@@ -1,3 +1,3 @@
|
|||||||
MONGODB_URI=mongodb://localhost:27017/car-tuning-crm
|
MONGODB_URI=mongodb://root:password@localhost:27017/car-tuning-crm
|
||||||
JWT_SECRET=your-secret-key
|
JWT_SECRET=password
|
||||||
PORT=5000
|
PORT=5000
|
||||||
@@ -24,6 +24,9 @@
|
|||||||
work correctly both with client-side routing and a non-root public URL.
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
<title>AutoTune Pro | Car Tuning Management</title>
|
<title>AutoTune Pro | Car Tuning Management</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -13,38 +13,126 @@ import CustomerManagement from './components/CustomerManagement';
|
|||||||
import PrivateRoute from './components/PrivateRoute';
|
import PrivateRoute from './components/PrivateRoute';
|
||||||
import UserManagement from './components/UserManagement';
|
import UserManagement from './components/UserManagement';
|
||||||
import BackToHome from './components/BackToHome';
|
import BackToHome from './components/BackToHome';
|
||||||
|
import Header from './components/Header';
|
||||||
|
|
||||||
// Create a theme that matches the "stoer en snel" (tough and fast) requirement
|
// Create a dark, modern theme that matches xatec.nl's style
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
mode: 'dark',
|
mode: 'dark',
|
||||||
primary: {
|
primary: {
|
||||||
main: '#ff3d00', // Bright orange for speed and energy
|
main: '#E02D1B', // Darker vibrant red
|
||||||
|
light: '#ff5a4d',
|
||||||
|
dark: '#A81F0A',
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: '#212121', // Dark gray for toughness
|
main: '#ffffff', // White for contrast
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
default: '#121212',
|
default: '#111111', // True black background
|
||||||
paper: '#1e1e1e',
|
paper: '#181818', // Slightly lighter for cards
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: '#ffffff',
|
||||||
|
secondary: '#b3b3b3',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
fontFamily: 'Inter, Roboto, Helvetica, Arial, sans-serif',
|
||||||
h1: {
|
h1: {
|
||||||
|
fontFamily: 'Orbitron, Inter, Roboto, Helvetica, Arial, sans-serif',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
|
fontSize: '2.7rem',
|
||||||
|
color: '#ffffff',
|
||||||
|
letterSpacing: '0.07em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
h2: {
|
h2: {
|
||||||
fontWeight: 700,
|
fontFamily: 'Orbitron, Inter, Roboto, Helvetica, Arial, sans-serif',
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '2.1rem',
|
||||||
|
color: '#ffffff',
|
||||||
|
letterSpacing: '0.06em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontFamily: 'Orbitron, Inter, Roboto, Helvetica, Arial, sans-serif',
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
color: '#ffffff',
|
||||||
|
letterSpacing: '0.05em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
},
|
||||||
|
body1: {
|
||||||
|
fontFamily: 'Inter, Roboto, Helvetica, Arial, sans-serif',
|
||||||
|
fontSize: '1.08rem',
|
||||||
|
lineHeight: 1.7,
|
||||||
|
color: '#ffffff',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
MuiButton: {
|
MuiButton: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
borderRadius: 0,
|
borderRadius: '4px',
|
||||||
textTransform: 'none',
|
textTransform: 'uppercase',
|
||||||
fontWeight: 600,
|
fontWeight: 700,
|
||||||
|
fontSize: '1.08rem',
|
||||||
|
padding: '14px 32px',
|
||||||
|
backgroundColor: '#E02D1B',
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 2px 12px rgba(224,45,27,0.10)',
|
||||||
|
letterSpacing: '0.08em',
|
||||||
|
margin: '8px 0',
|
||||||
|
transition: 'box-shadow 0.2s, background 0.2s',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#A81F0A',
|
||||||
|
boxShadow: '0 4px 20px rgba(224,45,27,0.18)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiCard: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderRadius: '4px',
|
||||||
|
boxShadow: '0 4px 24px rgba(0,0,0,0.35)',
|
||||||
|
backgroundColor: '#181818',
|
||||||
|
padding: '32px 28px',
|
||||||
|
margin: '24px 0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiAppBar: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
backgroundColor: '#181818',
|
||||||
|
color: '#ffffff',
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.25)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiTextField: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
'& fieldset': {
|
||||||
|
borderColor: '#333333',
|
||||||
|
},
|
||||||
|
'&:hover fieldset': {
|
||||||
|
borderColor: '#E02D1B',
|
||||||
|
},
|
||||||
|
'&.Mui-focused fieldset': {
|
||||||
|
borderColor: '#E02D1B',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiTableCell: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderBottom: '1px solid #222',
|
||||||
|
padding: '18px 12px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -59,6 +147,12 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
@@ -109,6 +203,10 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
<BackToHome />
|
<BackToHome />
|
||||||
</Router>
|
</Router>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
Typography,
|
Typography,
|
||||||
@@ -6,10 +6,23 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
Box,
|
CardActions,
|
||||||
|
Button,
|
||||||
TextField,
|
TextField,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
|
Box,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
Alert
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { Add as AddIcon } from '@mui/icons-material';
|
||||||
|
import axios from 'axios';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
|
|
||||||
// Import SVG icons
|
// Import SVG icons
|
||||||
@@ -19,180 +32,88 @@ import suspensionIcon from '../assets/icons/suspension.svg';
|
|||||||
import brakesIcon from '../assets/icons/brakes.svg';
|
import brakesIcon from '../assets/icons/brakes.svg';
|
||||||
import wheelsIcon from '../assets/icons/wheels.svg';
|
import wheelsIcon from '../assets/icons/wheels.svg';
|
||||||
|
|
||||||
|
// Create axios instance with default config
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: 'http://localhost:5000/api'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add request interceptor to include token
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const CarModifications = () => {
|
const CarModifications = () => {
|
||||||
|
const [modifications, setModifications] = useState([]);
|
||||||
|
const [customers, setCustomers] = useState([]);
|
||||||
|
const [selectedCustomer, setSelectedCustomer] = useState('');
|
||||||
|
const [selectedModification, setSelectedModification] = useState(null);
|
||||||
|
const [openDialog, setOpenDialog] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [success, setSuccess] = useState('');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
// Sample modifications data - in a real app, this would come from an API
|
useEffect(() => {
|
||||||
const modifications = [
|
fetchModifications();
|
||||||
{
|
fetchCustomers();
|
||||||
id: 1,
|
}, []);
|
||||||
name: 'Performance Chip',
|
|
||||||
description: 'Increase engine power and torque with our custom ECU tuning',
|
const fetchModifications = async () => {
|
||||||
price: '€299',
|
try {
|
||||||
icon: engineIcon,
|
const response = await api.get('/modifications');
|
||||||
category: 'Engine',
|
setModifications(response.data);
|
||||||
},
|
} catch (err) {
|
||||||
{
|
setError('Failed to fetch modifications');
|
||||||
id: 2,
|
|
||||||
name: 'Sport Exhaust System',
|
|
||||||
description: 'High-flow exhaust system for better sound and performance',
|
|
||||||
price: '€599',
|
|
||||||
icon: exhaustIcon,
|
|
||||||
category: 'Exhaust',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Lowering Springs',
|
|
||||||
description: 'Sport suspension lowering springs for improved handling',
|
|
||||||
price: '€399',
|
|
||||||
icon: suspensionIcon,
|
|
||||||
category: 'Suspension',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Cold Air Intake',
|
|
||||||
description: 'Improved air flow for better engine performance',
|
|
||||||
price: '€199',
|
|
||||||
icon: engineIcon,
|
|
||||||
category: 'Engine',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: 'Sport Brake Kit',
|
|
||||||
description: 'Upgraded brake system for better stopping power',
|
|
||||||
price: '€899',
|
|
||||||
icon: brakesIcon,
|
|
||||||
category: 'Brakes',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: 'Wheel Spacers',
|
|
||||||
description: 'Improve stance and handling with wheel spacers',
|
|
||||||
price: '€149',
|
|
||||||
icon: wheelsIcon,
|
|
||||||
category: 'Wheels',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
name: 'Turbocharger Kit',
|
|
||||||
description: 'Complete turbo upgrade kit for significant power gains',
|
|
||||||
price: '€2499',
|
|
||||||
icon: engineIcon,
|
|
||||||
category: 'Engine',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
name: 'Cat-Back Exhaust',
|
|
||||||
description: 'Performance exhaust system with sport sound',
|
|
||||||
price: '€799',
|
|
||||||
icon: exhaustIcon,
|
|
||||||
category: 'Exhaust',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
name: 'Coilover Suspension',
|
|
||||||
description: 'Fully adjustable suspension system for perfect handling',
|
|
||||||
price: '€1299',
|
|
||||||
icon: suspensionIcon,
|
|
||||||
category: 'Suspension',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
name: 'Big Brake Kit',
|
|
||||||
description: '6-piston caliper upgrade with larger rotors',
|
|
||||||
price: '€1499',
|
|
||||||
icon: brakesIcon,
|
|
||||||
category: 'Brakes',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
name: 'Forged Wheels',
|
|
||||||
description: 'Lightweight forged alloy wheels for better performance',
|
|
||||||
price: '€1999',
|
|
||||||
icon: wheelsIcon,
|
|
||||||
category: 'Wheels',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
name: 'Stage 2 Tune',
|
|
||||||
description: 'Advanced ECU remap for maximum power gains',
|
|
||||||
price: '€499',
|
|
||||||
icon: engineIcon,
|
|
||||||
category: 'Engine',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 13,
|
|
||||||
name: 'Downpipe',
|
|
||||||
description: 'High-flow downpipe for improved exhaust flow',
|
|
||||||
price: '€349',
|
|
||||||
icon: exhaustIcon,
|
|
||||||
category: 'Exhaust',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 14,
|
|
||||||
name: 'Sway Bars',
|
|
||||||
description: 'Upgraded sway bars for reduced body roll',
|
|
||||||
price: '€299',
|
|
||||||
icon: suspensionIcon,
|
|
||||||
category: 'Suspension',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 15,
|
|
||||||
name: 'Brake Pads',
|
|
||||||
description: 'High-performance brake pads for better stopping',
|
|
||||||
price: '€199',
|
|
||||||
icon: brakesIcon,
|
|
||||||
category: 'Brakes',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 16,
|
|
||||||
name: 'Wheel Bearings',
|
|
||||||
description: 'Upgraded wheel bearings for smoother rotation',
|
|
||||||
price: '€249',
|
|
||||||
icon: wheelsIcon,
|
|
||||||
category: 'Wheels',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 17,
|
|
||||||
name: 'Intercooler Upgrade',
|
|
||||||
description: 'Larger intercooler for better cooling efficiency',
|
|
||||||
price: '€699',
|
|
||||||
icon: engineIcon,
|
|
||||||
category: 'Engine',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 18,
|
|
||||||
name: 'Exhaust Manifold',
|
|
||||||
description: 'Equal-length exhaust manifold for better flow',
|
|
||||||
price: '€449',
|
|
||||||
icon: exhaustIcon,
|
|
||||||
category: 'Exhaust',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 19,
|
|
||||||
name: 'Strut Brace',
|
|
||||||
description: 'Front strut brace for improved chassis rigidity',
|
|
||||||
price: '€199',
|
|
||||||
icon: suspensionIcon,
|
|
||||||
category: 'Suspension',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 20,
|
|
||||||
name: 'Brake Lines',
|
|
||||||
description: 'Stainless steel braided brake lines',
|
|
||||||
price: '€149',
|
|
||||||
icon: brakesIcon,
|
|
||||||
category: 'Brakes',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 21,
|
|
||||||
name: 'Wheel Locks',
|
|
||||||
description: 'Security wheel locks to prevent theft',
|
|
||||||
price: '€89',
|
|
||||||
icon: wheelsIcon,
|
|
||||||
category: 'Wheels',
|
|
||||||
}
|
}
|
||||||
];
|
};
|
||||||
|
|
||||||
|
const fetchCustomers = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get('/customers');
|
||||||
|
setCustomers(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to fetch customers');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddToCustomer = (modification) => {
|
||||||
|
setSelectedModification(modification);
|
||||||
|
setOpenDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDialog = () => {
|
||||||
|
setOpenDialog(false);
|
||||||
|
setSelectedCustomer('');
|
||||||
|
setSelectedModification(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!selectedCustomer) {
|
||||||
|
setError('Please select a customer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.put(`/customers/${selectedCustomer}/modifications`, {
|
||||||
|
name: selectedModification.name,
|
||||||
|
description: selectedModification.description,
|
||||||
|
price: selectedModification.price,
|
||||||
|
category: selectedModification.category
|
||||||
|
});
|
||||||
|
setSuccess('Modification added to customer successfully');
|
||||||
|
handleCloseDialog();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || 'Failed to add modification to customer');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const filteredModifications = modifications.filter((mod) =>
|
const filteredModifications = modifications.filter((mod) =>
|
||||||
mod.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
mod.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
@@ -206,6 +127,9 @@ const CarModifications = () => {
|
|||||||
Car Modifications
|
Car Modifications
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||||
|
{success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
|
||||||
|
|
||||||
<Box sx={{ mb: 4 }}>
|
<Box sx={{ mb: 4 }}>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -225,7 +149,7 @@ const CarModifications = () => {
|
|||||||
|
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{filteredModifications.map((mod) => (
|
{filteredModifications.map((mod) => (
|
||||||
<Grid item xs={12} sm={6} md={4} key={mod.id}>
|
<Grid item xs={12} sm={6} md={4} key={mod._id}>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@@ -292,10 +216,48 @@ const CarModifications = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
<CardActions>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={() => handleAddToCustomer(mod)}
|
||||||
|
>
|
||||||
|
Add to Customer
|
||||||
|
</Button>
|
||||||
|
</CardActions>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle>Add Modification to Customer</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>Select Customer</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={selectedCustomer}
|
||||||
|
onChange={(e) => setSelectedCustomer(e.target.value)}
|
||||||
|
label="Select Customer"
|
||||||
|
>
|
||||||
|
{customers.map((customer) => (
|
||||||
|
<MenuItem key={customer._id} value={customer._id}>
|
||||||
|
{customer.name} - {customer.carModel} ({customer.carYear})
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleCloseDialog}>Cancel</Button>
|
||||||
|
<Button onClick={handleSubmit} variant="contained" color="primary">
|
||||||
|
Add to Customer
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ api.interceptors.request.use(
|
|||||||
|
|
||||||
const CustomerManagement = () => {
|
const CustomerManagement = () => {
|
||||||
const [customers, setCustomers] = useState([]);
|
const [customers, setCustomers] = useState([]);
|
||||||
|
const [modifications, setModifications] = useState([]);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [openModDialog, setOpenModDialog] = useState(false);
|
const [openModDialog, setOpenModDialog] = useState(false);
|
||||||
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
||||||
@@ -89,7 +90,8 @@ const CustomerManagement = () => {
|
|||||||
phone: '',
|
phone: '',
|
||||||
address: '',
|
address: '',
|
||||||
carModel: '',
|
carModel: '',
|
||||||
carYear: ''
|
carYear: '',
|
||||||
|
modifications: []
|
||||||
});
|
});
|
||||||
const [modificationData, setModificationData] = useState({
|
const [modificationData, setModificationData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -100,6 +102,7 @@ const CustomerManagement = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCustomers();
|
fetchCustomers();
|
||||||
|
fetchModifications();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchCustomers = async () => {
|
const fetchCustomers = async () => {
|
||||||
@@ -111,9 +114,21 @@ const CustomerManagement = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchModifications = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get('/modifications');
|
||||||
|
setModifications(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to fetch modifications');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpen = (customer = null) => {
|
const handleOpen = (customer = null) => {
|
||||||
if (customer) {
|
if (customer) {
|
||||||
setFormData(customer);
|
setFormData({
|
||||||
|
...customer,
|
||||||
|
modifications: customer.modifications || []
|
||||||
|
});
|
||||||
setSelectedCustomer(customer);
|
setSelectedCustomer(customer);
|
||||||
} else {
|
} else {
|
||||||
setFormData({
|
setFormData({
|
||||||
@@ -122,7 +137,8 @@ const CustomerManagement = () => {
|
|||||||
phone: '',
|
phone: '',
|
||||||
address: '',
|
address: '',
|
||||||
carModel: '',
|
carModel: '',
|
||||||
carYear: ''
|
carYear: '',
|
||||||
|
modifications: []
|
||||||
});
|
});
|
||||||
setSelectedCustomer(null);
|
setSelectedCustomer(null);
|
||||||
}
|
}
|
||||||
@@ -166,6 +182,15 @@ const CustomerManagement = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleModificationsChange = (e) => {
|
||||||
|
const selectedIds = e.target.value;
|
||||||
|
const selectedMods = modifications.filter((mod) => selectedIds.includes(mod._id));
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
modifications: selectedMods
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
@@ -354,6 +379,30 @@ const CustomerManagement = () => {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>Car Modifications</InputLabel>
|
||||||
|
<Select
|
||||||
|
multiple
|
||||||
|
name="modifications"
|
||||||
|
value={formData.modifications.map((mod) => mod._id)}
|
||||||
|
onChange={handleModificationsChange}
|
||||||
|
label="Car Modifications"
|
||||||
|
renderValue={(selected) =>
|
||||||
|
modifications
|
||||||
|
.filter((mod) => selected.includes(mod._id))
|
||||||
|
.map((mod) => mod.name)
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{modifications.map((mod) => (
|
||||||
|
<MenuItem key={mod._id} value={mod._id}>
|
||||||
|
{mod.name} ({mod.category})
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
33
client/src/components/Header.js
Normal file
33
client/src/components/Header.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import AppBar from '@mui/material/AppBar';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
navigate('/login');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppBar position="static" color="transparent" elevation={0} sx={{ mb: 4, background: 'none' }}>
|
||||||
|
<Toolbar>
|
||||||
|
<Typography variant="h5" sx={{ flexGrow: 1, fontWeight: 700, letterSpacing: '0.1em', fontFamily: 'Orbitron, Inter, Roboto, Helvetica, Arial, sans-serif' }}>
|
||||||
|
Xatec CRM
|
||||||
|
</Typography>
|
||||||
|
<Box>
|
||||||
|
<Button color="primary" variant="contained" onClick={handleLogout} sx={{ fontWeight: 700, ml: 2 }}>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
@@ -1,13 +1,106 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
sans-serif;
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: #111111;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: 'Orbitron', 'Inter', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
color: #fff;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Global styles for consistent spacing and layout */
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth transitions */
|
||||||
|
* {
|
||||||
|
transition: background-color 0.2s, color 0.2s, border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #181818;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #E02D1B;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #A81F0A;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection color */
|
||||||
|
::selection {
|
||||||
|
background: #E02D1B;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus outline */
|
||||||
|
:focus {
|
||||||
|
outline: 2px solid #E02D1B;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, .MuiButton-root {
|
||||||
|
border-radius: 4px !important;
|
||||||
|
font-family: 'Orbitron', 'Inter', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
background-color: #E02D1B;
|
||||||
|
color: #fff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 1.08rem;
|
||||||
|
padding: 14px 32px;
|
||||||
|
box-shadow: 0 2px 12px rgba(224,45,27,0.10);
|
||||||
|
margin: 8px 0;
|
||||||
|
transition: box-shadow 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover, .MuiButton-root:hover {
|
||||||
|
background-color: #A81F0A;
|
||||||
|
box-shadow: 0 4px 20px rgba(224,45,27,0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiCard-root, .card {
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #181818;
|
||||||
|
box-shadow: 0 4px 24px rgba(0,0,0,0.35);
|
||||||
|
padding: 32px 28px;
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, .MuiInputBase-root {
|
||||||
|
background: #181818 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiTableCell-root {
|
||||||
|
border-bottom: 1px solid #222 !important;
|
||||||
|
padding: 18px 12px !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- mongodb_data:/data/db
|
- mongodb_data:/data/db
|
||||||
environment:
|
environment:
|
||||||
|
- MONGO_INITDB_ROOT_USERNAME=root
|
||||||
|
- MONGO_INITDB_ROOT_PASSWORD=password
|
||||||
- MONGO_INITDB_DATABASE=car-tuning-crm
|
- MONGO_INITDB_DATABASE=car-tuning-crm
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
14
models/Modification.js
Normal file
14
models/Modification.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const modificationSchema = new mongoose.Schema({
|
||||||
|
name: { type: String, required: true },
|
||||||
|
description: { type: String, required: true },
|
||||||
|
price: { type: Number, required: true },
|
||||||
|
category: { type: String, required: true },
|
||||||
|
icon: { type: String }, // optional: path to SVG/icon
|
||||||
|
createdAt: { type: Date, default: Date.now }
|
||||||
|
});
|
||||||
|
|
||||||
|
const Modification = mongoose.model('Modification', modificationSchema);
|
||||||
|
|
||||||
|
module.exports = Modification;
|
||||||
27
routes/modifications.js
Normal file
27
routes/modifications.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const Modification = require('../models/Modification');
|
||||||
|
const auth = require('../middleware/auth');
|
||||||
|
|
||||||
|
// Get all modifications
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const modifications = await Modification.find().sort({ name: 1 });
|
||||||
|
res.json(modifications);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a new modification (admin only, or for seeding)
|
||||||
|
router.post('/', auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const mod = new Modification(req.body);
|
||||||
|
await mod.save();
|
||||||
|
res.status(201).json(mod);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -24,6 +24,7 @@ app.use('/api/auth', require('./routes/auth'));
|
|||||||
app.use('/api/users', require('./routes/users'));
|
app.use('/api/users', require('./routes/users'));
|
||||||
app.use('/api/customers', require('./routes/customers'));
|
app.use('/api/customers', require('./routes/customers'));
|
||||||
app.use('/api/contacts', require('./routes/contacts'));
|
app.use('/api/contacts', require('./routes/contacts'));
|
||||||
|
app.use('/api/modifications', require('./routes/modifications'));
|
||||||
|
|
||||||
const PORT = process.env.PORT || 5000;
|
const PORT = process.env.PORT || 5000;
|
||||||
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
|
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
|
||||||
Reference in New Issue
Block a user