From f66670137b800e3b4cd0beaadb38829201d78856 Mon Sep 17 00:00:00 2001 From: Alvin <524715@vistacollege.nl> Date: Thu, 12 Jun 2025 09:38:03 +0200 Subject: [PATCH] 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. --- .env | 4 +- client/public/index.html | 3 + client/src/App.js | 208 +++++++++---- client/src/components/CarModifications.js | 308 +++++++++----------- client/src/components/CustomerManagement.js | 55 +++- client/src/components/Header.js | 33 +++ client/src/index.css | 95 +++++- docker-compose.yml | 2 + models/Modification.js | 14 + routes/modifications.js | 27 ++ server.js | 1 + 11 files changed, 516 insertions(+), 234 deletions(-) create mode 100644 client/src/components/Header.js create mode 100644 models/Modification.js create mode 100644 routes/modifications.js diff --git a/.env b/.env index 25b659a..5478a1a 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ -MONGODB_URI=mongodb://localhost:27017/car-tuning-crm -JWT_SECRET=your-secret-key +MONGODB_URI=mongodb://root:password@localhost:27017/car-tuning-crm +JWT_SECRET=password PORT=5000 \ No newline at end of file diff --git a/client/public/index.html b/client/public/index.html index 10b4f00..05a376e 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -24,6 +24,9 @@ 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`. --> + + + AutoTune Pro | Car Tuning Management diff --git a/client/src/App.js b/client/src/App.js index f979769..d2d4d19 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -13,38 +13,126 @@ import CustomerManagement from './components/CustomerManagement'; import PrivateRoute from './components/PrivateRoute'; import UserManagement from './components/UserManagement'; 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({ palette: { mode: 'dark', primary: { - main: '#ff3d00', // Bright orange for speed and energy + main: '#E02D1B', // Darker vibrant red + light: '#ff5a4d', + dark: '#A81F0A', }, secondary: { - main: '#212121', // Dark gray for toughness + main: '#ffffff', // White for contrast }, background: { - default: '#121212', - paper: '#1e1e1e', + default: '#111111', // True black background + paper: '#181818', // Slightly lighter for cards + }, + text: { + primary: '#ffffff', + secondary: '#b3b3b3', }, }, typography: { - fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', + fontFamily: 'Inter, Roboto, Helvetica, Arial, sans-serif', h1: { + fontFamily: 'Orbitron, Inter, Roboto, Helvetica, Arial, sans-serif', fontWeight: 700, + fontSize: '2.7rem', + color: '#ffffff', + letterSpacing: '0.07em', + textTransform: 'uppercase', }, 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: { MuiButton: { styleOverrides: { root: { - borderRadius: 0, - textTransform: 'none', - fontWeight: 600, + borderRadius: '4px', + textTransform: 'uppercase', + 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', }, }, }, @@ -60,54 +148,64 @@ function App() { } /> } /> - - + <> +
+ + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + + } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - } /> diff --git a/client/src/components/CarModifications.js b/client/src/components/CarModifications.js index f916ac5..e67469f 100644 --- a/client/src/components/CarModifications.js +++ b/client/src/components/CarModifications.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Container, Typography, @@ -6,10 +6,23 @@ import { Paper, Card, CardContent, - Box, + CardActions, + Button, TextField, InputAdornment, + Box, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + FormControl, + InputLabel, + Select, + MenuItem, + Alert } from '@mui/material'; +import { Add as AddIcon } from '@mui/icons-material'; +import axios from 'axios'; import SearchIcon from '@mui/icons-material/Search'; // Import SVG icons @@ -19,180 +32,88 @@ import suspensionIcon from '../assets/icons/suspension.svg'; import brakesIcon from '../assets/icons/brakes.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 [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(''); - // Sample modifications data - in a real app, this would come from an API - const modifications = [ - { - id: 1, - name: 'Performance Chip', - description: 'Increase engine power and torque with our custom ECU tuning', - price: '€299', - icon: engineIcon, - category: 'Engine', - }, - { - 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', + useEffect(() => { + fetchModifications(); + fetchCustomers(); + }, []); + + const fetchModifications = async () => { + try { + const response = await api.get('/modifications'); + setModifications(response.data); + } catch (err) { + setError('Failed to fetch modifications'); } - ]; + }; + + 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) => mod.name.toLowerCase().includes(searchTerm.toLowerCase()) || @@ -206,6 +127,9 @@ const CarModifications = () => { Car Modifications + {error && {error}} + {success && {success}} + { {filteredModifications.map((mod) => ( - + { + + + ))} + + + Add Modification to Customer + + + + Select Customer + + + + + + + + + ); }; diff --git a/client/src/components/CustomerManagement.js b/client/src/components/CustomerManagement.js index 78bb3b0..1d7dffe 100644 --- a/client/src/components/CustomerManagement.js +++ b/client/src/components/CustomerManagement.js @@ -78,6 +78,7 @@ api.interceptors.request.use( const CustomerManagement = () => { const [customers, setCustomers] = useState([]); + const [modifications, setModifications] = useState([]); const [open, setOpen] = useState(false); const [openModDialog, setOpenModDialog] = useState(false); const [selectedCustomer, setSelectedCustomer] = useState(null); @@ -89,7 +90,8 @@ const CustomerManagement = () => { phone: '', address: '', carModel: '', - carYear: '' + carYear: '', + modifications: [] }); const [modificationData, setModificationData] = useState({ name: '', @@ -100,6 +102,7 @@ const CustomerManagement = () => { useEffect(() => { fetchCustomers(); + fetchModifications(); }, []); 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) => { if (customer) { - setFormData(customer); + setFormData({ + ...customer, + modifications: customer.modifications || [] + }); setSelectedCustomer(customer); } else { setFormData({ @@ -122,7 +137,8 @@ const CustomerManagement = () => { phone: '', address: '', carModel: '', - carYear: '' + carYear: '', + modifications: [] }); 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) => { e.preventDefault(); try { @@ -354,6 +379,30 @@ const CustomerManagement = () => { required /> + + + Car Modifications + + + diff --git a/client/src/components/Header.js b/client/src/components/Header.js new file mode 100644 index 0000000..6d8aeaf --- /dev/null +++ b/client/src/components/Header.js @@ -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 ( + + + + Xatec CRM + + + + + + + ); +}; + +export default Header; \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css index ec2585e..22bf2db 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,13 +1,106 @@ body { 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', sans-serif; -webkit-font-smoothing: antialiased; -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 { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 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; +} diff --git a/docker-compose.yml b/docker-compose.yml index a59cef2..9c4ed3c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: volumes: - mongodb_data:/data/db environment: + - MONGO_INITDB_ROOT_USERNAME=root + - MONGO_INITDB_ROOT_PASSWORD=password - MONGO_INITDB_DATABASE=car-tuning-crm volumes: diff --git a/models/Modification.js b/models/Modification.js new file mode 100644 index 0000000..8f3ff81 --- /dev/null +++ b/models/Modification.js @@ -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; \ No newline at end of file diff --git a/routes/modifications.js b/routes/modifications.js new file mode 100644 index 0000000..53ba956 --- /dev/null +++ b/routes/modifications.js @@ -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; \ No newline at end of file diff --git a/server.js b/server.js index ee281c2..9d50465 100644 --- a/server.js +++ b/server.js @@ -24,6 +24,7 @@ app.use('/api/auth', require('./routes/auth')); app.use('/api/users', require('./routes/users')); app.use('/api/customers', require('./routes/customers')); app.use('/api/contacts', require('./routes/contacts')); +app.use('/api/modifications', require('./routes/modifications')); const PORT = process.env.PORT || 5000; app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); \ No newline at end of file