diff --git a/.cursor/rules/devdatabase.mdc b/.cursor/rules/devdatabase.mdc index dc222a2..10772ae 100644 --- a/.cursor/rules/devdatabase.mdc +++ b/.cursor/rules/devdatabase.mdc @@ -1,4 +1,6 @@ --- alwaysApply: true --- -sqlcmd -C -S tcp:192.168.56.1,1497 -U app -P 'readonly' -d eazybusiness -W \ No newline at end of file +sqlcmd -C -S tcp:192.168.56.1,1497 -U app -P 'readonly' -d eazybusiness -W + +sqlcmd -C -S tcp:192.168.56.1,1497 -U sa -P 'sa_tekno23' -d eazybusiness -W \ No newline at end of file diff --git a/client/src/components/KreditorSelector.js b/client/src/components/KreditorSelector.js new file mode 100644 index 0000000..5f635b2 --- /dev/null +++ b/client/src/components/KreditorSelector.js @@ -0,0 +1,288 @@ +import React, { Component } from 'react'; +import { + Select, + MenuItem, + FormControl, + InputLabel, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + Box, + Typography, + Alert, + CircularProgress +} from '@mui/material'; +import { Add as AddIcon } from '@mui/icons-material'; +import KreditorService from '../services/KreditorService'; + +class KreditorSelector extends Component { + constructor(props) { + super(props); + this.state = { + kreditors: [], + selectedKreditorId: props.selectedKreditorId || '', + loading: false, + createDialogOpen: false, + newKreditor: { + iban: '', + name: '', + kreditorId: '' + }, + validationErrors: [], + error: null, + creating: false + }; + + this.kreditorService = new KreditorService(); + } + + componentDidMount() { + this.loadKreditors(); + } + + componentDidUpdate(prevProps) { + if (prevProps.selectedKreditorId !== this.props.selectedKreditorId) { + this.setState({ selectedKreditorId: this.props.selectedKreditorId || '' }); + } + } + + loadKreditors = async () => { + this.setState({ loading: true, error: null }); + + try { + const kreditors = await this.kreditorService.getAllKreditors(); + this.setState({ kreditors, loading: false }); + } catch (error) { + console.error('Error loading kreditors:', error); + this.setState({ + error: error.message, + loading: false + }); + } + }; + + handleKreditorChange = (event) => { + const selectedKreditorId = event.target.value; + + if (selectedKreditorId === 'create_new') { + this.setState({ createDialogOpen: true }); + return; + } + + this.setState({ selectedKreditorId }); + + if (this.props.onKreditorChange) { + const selectedKreditor = this.state.kreditors.find(k => k.id === selectedKreditorId); + this.props.onKreditorChange(selectedKreditor); + } + }; + + handleCreateDialogClose = () => { + this.setState({ + createDialogOpen: false, + newKreditor: { iban: '', name: '', kreditorId: '' }, + validationErrors: [], + error: null + }); + }; + + handleNewKreditorChange = (field, value) => { + this.setState({ + newKreditor: { + ...this.state.newKreditor, + [field]: value + }, + validationErrors: [] // Clear validation errors when user types + }); + }; + + generateKreditorId = () => { + // Generate a kreditorId starting with 70 followed by random digits + const randomDigits = Math.floor(Math.random() * 10000).toString().padStart(4, '0'); + const kreditorId = `70${randomDigits}`; + + this.setState({ + newKreditor: { + ...this.state.newKreditor, + kreditorId + } + }); + }; + + handleCreateKreditor = async () => { + const { newKreditor } = this.state; + + // Validate the data + const validationErrors = this.kreditorService.validateKreditorData(newKreditor); + if (validationErrors.length > 0) { + this.setState({ validationErrors }); + return; + } + + this.setState({ creating: true, error: null }); + + try { + const createdKreditor = await this.kreditorService.createKreditor(newKreditor); + + // Add the new kreditor to the list and select it + const updatedKreditors = [...this.state.kreditors, createdKreditor]; + this.setState({ + kreditors: updatedKreditors, + selectedKreditorId: createdKreditor.id, + creating: false + }); + + // Notify parent component + if (this.props.onKreditorChange) { + this.props.onKreditorChange(createdKreditor); + } + + this.handleCreateDialogClose(); + } catch (error) { + console.error('Error creating kreditor:', error); + this.setState({ + error: error.message, + creating: false + }); + } + }; + + render() { + const { + kreditors, + selectedKreditorId, + loading, + createDialogOpen, + newKreditor, + validationErrors, + error, + creating + } = this.state; + + const { label = "Kreditor", disabled = false, fullWidth = true } = this.props; + + return ( + <> + + {label} + + + + {error && ( + + {error} + + )} + + {/* Create Kreditor Dialog */} + + Neuen Kreditor erstellen + + + {validationErrors.length > 0 && ( + +
    + {validationErrors.map((error, index) => ( +
  • {error}
  • + ))} +
+
+ )} + + {error && ( + + {error} + + )} + + this.handleNewKreditorChange('name', e.target.value)} + fullWidth + margin="normal" + required + /> + + this.handleNewKreditorChange('iban', e.target.value.toUpperCase())} + fullWidth + margin="normal" + required + placeholder="DE89 3704 0044 0532 0130 00" + /> + + + this.handleNewKreditorChange('kreditorId', e.target.value)} + margin="normal" + required + placeholder="70001" + sx={{ flexGrow: 1 }} + /> + + + + + Die Kreditor-ID muss mit "70" beginnen, gefolgt von mindestens 3 Ziffern. + +
+
+ + + + +
+ + ); + } +} + +export default KreditorSelector; \ No newline at end of file diff --git a/client/src/components/cellRenderers/DocumentRenderer.js b/client/src/components/cellRenderers/DocumentRenderer.js index ef7ac3a..64e58ef 100644 --- a/client/src/components/cellRenderers/DocumentRenderer.js +++ b/client/src/components/cellRenderers/DocumentRenderer.js @@ -25,6 +25,7 @@ import { ContentCopy as CopyIcon } from '@mui/icons-material'; import { AgGridReact } from 'ag-grid-react'; +import KreditorSelector from '../KreditorSelector'; const DocumentRenderer = (params) => { // Check for pdfs and links regardless of transaction source @@ -145,7 +146,10 @@ const DocumentRenderer = (params) => { currency: extractionData.currency || 'EUR', invoiceNumber: extractionData.invoice_number || '', date: extractionData.date || '', - sender: extractionData.sender || '' + sender: extractionData.sender || '', + kreditorId: extractionData.kreditor_id || null, + kreditorName: extractionData.kreditor_name || '', + kreditorCode: extractionData.kreditor_code || '' }); }); } @@ -184,6 +188,34 @@ const DocumentRenderer = (params) => { width: 150, tooltipField: 'sender' }, + { + headerName: 'Kreditor', + field: 'kreditor', + width: 200, + cellRenderer: (params) => { + return ( + + { + // Update the line item with the selected kreditor + if (params.data && params.api) { + params.data.kreditorId = kreditor ? kreditor.id : null; + params.data.kreditorName = kreditor ? kreditor.name : ''; + params.data.kreditorCode = kreditor ? kreditor.kreditorId : ''; + params.api.refreshCells({ rowNodes: [params.node] }); + } + }} + label="" + fullWidth={false} + /> + + ); + }, + editable: false, + sortable: false, + filter: false + }, { headerName: 'Netto', field: 'netAmount', @@ -371,7 +403,7 @@ const DocumentRenderer = (params) => { )} {tabValue === 1 && ( - + {lineItems.length > 0 ? (
{ rowData={lineItems} defaultColDef={defaultColDef} suppressRowTransform={true} - rowHeight={35} + rowHeight={50} headerHeight={35} domLayout="normal" /> diff --git a/client/src/services/KreditorService.js b/client/src/services/KreditorService.js new file mode 100644 index 0000000..159d708 --- /dev/null +++ b/client/src/services/KreditorService.js @@ -0,0 +1,178 @@ +class KreditorService { + constructor() { + this.baseURL = '/api'; + } + + async getAuthHeaders() { + const token = localStorage.getItem('token'); + return { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }; + } + + async handleResponse(response) { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + console.log('Server error response:', errorData); + + // Handle different types of errors with clearer messages + if (response.status === 502 || response.status === 503) { + throw new Error('FibDash Service nicht verfügbar - Bitte versuchen Sie es später erneut'); + } else if (response.status === 500) { + throw new Error('FibDash Server Fehler - Bitte kontaktieren Sie den Administrator'); + } else if (response.status === 403) { + const message = errorData.message || 'Zugriff verweigert'; + throw new Error(message); + } else if (response.status === 401) { + throw new Error('Authentifizierung fehlgeschlagen - Bitte melden Sie sich erneut an'); + } else if (response.status === 404) { + throw new Error('Kreditor nicht gefunden'); + } else if (response.status === 409) { + const message = errorData.error || 'Kreditor bereits vorhanden'; + throw new Error(message); + } else if (response.status === 400) { + const message = errorData.error || 'Ungültige Daten'; + throw new Error(message); + } else { + const errorMessage = errorData.error || errorData.message || `HTTP ${response.status}: Unbekannter Fehler`; + throw new Error(errorMessage); + } + } + + return await response.json(); + } + + async getAllKreditors() { + try { + const response = await fetch(`${this.baseURL}/data/kreditors`, { + method: 'GET', + headers: await this.getAuthHeaders(), + }); + + return await this.handleResponse(response); + } catch (error) { + console.error('Error fetching kreditors:', error); + + // Handle network errors + if (error instanceof TypeError && error.message.includes('fetch')) { + throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut'); + } + + throw error; + } + } + + async getKreditorById(id) { + try { + const response = await fetch(`${this.baseURL}/data/kreditors/${id}`, { + method: 'GET', + headers: await this.getAuthHeaders(), + }); + + return await this.handleResponse(response); + } catch (error) { + console.error('Error fetching kreditor:', error); + + // Handle network errors + if (error instanceof TypeError && error.message.includes('fetch')) { + throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut'); + } + + throw error; + } + } + + async createKreditor(kreditorData) { + try { + const response = await fetch(`${this.baseURL}/data/kreditors`, { + method: 'POST', + headers: await this.getAuthHeaders(), + body: JSON.stringify(kreditorData), + }); + + return await this.handleResponse(response); + } catch (error) { + console.error('Error creating kreditor:', error); + + // Handle network errors + if (error instanceof TypeError && error.message.includes('fetch')) { + throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut'); + } + + throw error; + } + } + + async updateKreditor(id, kreditorData) { + try { + const response = await fetch(`${this.baseURL}/data/kreditors/${id}`, { + method: 'PUT', + headers: await this.getAuthHeaders(), + body: JSON.stringify(kreditorData), + }); + + return await this.handleResponse(response); + } catch (error) { + console.error('Error updating kreditor:', error); + + // Handle network errors + if (error instanceof TypeError && error.message.includes('fetch')) { + throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut'); + } + + throw error; + } + } + + async deleteKreditor(id) { + try { + const response = await fetch(`${this.baseURL}/data/kreditors/${id}`, { + method: 'DELETE', + headers: await this.getAuthHeaders(), + }); + + return await this.handleResponse(response); + } catch (error) { + console.error('Error deleting kreditor:', error); + + // Handle network errors + if (error instanceof TypeError && error.message.includes('fetch')) { + throw new Error('FibDash Service nicht erreichbar - Prüfen Sie Ihre Internetverbindung oder versuchen Sie es später erneut'); + } + + throw error; + } + } + + // Utility method to validate kreditor data + validateKreditorData(kreditorData) { + const errors = []; + + if (!kreditorData.iban || kreditorData.iban.trim() === '') { + errors.push('IBAN ist erforderlich'); + } + + if (!kreditorData.name || kreditorData.name.trim() === '') { + errors.push('Name ist erforderlich'); + } + + if (!kreditorData.kreditorId || kreditorData.kreditorId.trim() === '') { + errors.push('Kreditor-ID ist erforderlich'); + } + + // Basic IBAN format validation (simplified) + if (kreditorData.iban && !/^[A-Z]{2}[0-9]{2}[A-Z0-9]{4}[0-9]{7}([A-Z0-9]?){0,16}$/i.test(kreditorData.iban.replace(/\s/g, ''))) { + errors.push('IBAN Format ist ungültig'); + } + + // Validate kreditorId format (should start with 70xxx) + if (kreditorData.kreditorId && !/^70\d{3,}$/.test(kreditorData.kreditorId)) { + errors.push('Kreditor-ID muss mit 70 beginnen gefolgt von mindestens 3 Ziffern'); + } + + return errors; + } +} + +export default KreditorService; \ No newline at end of file diff --git a/src/database/schema.sql b/src/database/schema.sql index 60eb0e1..8fba03a 100644 --- a/src/database/schema.sql +++ b/src/database/schema.sql @@ -1,34 +1,94 @@ --- FibDash Database Schema --- Run these commands in your MSSQL database --- Create Users table -CREATE TABLE Users ( +-- Create Kreditor table +CREATE TABLE Kreditor ( id INT IDENTITY(1,1) PRIMARY KEY, - google_id NVARCHAR(255) UNIQUE NOT NULL, - email NVARCHAR(255) UNIQUE NOT NULL, + iban NVARCHAR(34) NOT NULL, name NVARCHAR(255) NOT NULL, - picture NVARCHAR(500), - created_at DATETIME2 DEFAULT GETDATE(), - last_login DATETIME2, - is_active BIT DEFAULT 1 + kreditorId NVARCHAR(50) NOT NULL ); --- Create UserPreferences table -CREATE TABLE UserPreferences ( +-- Ensure kreditorId is unique to support FK references +ALTER TABLE Kreditor +ADD CONSTRAINT UQ_Kreditor_kreditorId UNIQUE (kreditorId); + +-- Create AccountingItems table +-- Based on CSV structure: umsatz brutto, soll/haben kz, konto, gegenkonto, bu, buchungsdatum, rechnungsnummer, buchungstext, beleglink +CREATE TABLE AccountingItems ( id INT IDENTITY(1,1) PRIMARY KEY, - user_id INT NOT NULL, - theme NVARCHAR(50) DEFAULT 'light', - language NVARCHAR(10) DEFAULT 'en', - notifications_enabled BIT DEFAULT 1, - created_at DATETIME2 DEFAULT GETDATE(), - updated_at DATETIME2 DEFAULT GETDATE(), - FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE + umsatz_brutto DECIMAL(15,2) NOT NULL, -- gross turnover amount + soll_haben_kz CHAR(1) NOT NULL CHECK (soll_haben_kz IN ('S', 'H')), -- S = eingang (debit), H = ausgang (credit) + konto NVARCHAR(10) NOT NULL, -- account (e.g. 5400 = wareneingang 19%) + gegenkonto NVARCHAR(50) NOT NULL, -- counter account references Kreditor(kreditorId) + bu NVARCHAR(10), -- tax code (9 = 19%vst, 8 = 7%vst, 506 = dienstleistung aus EU, 511 = dienstleistung ausserhalb EU) + buchungsdatum DATE NOT NULL, -- booking date + rechnungsnummer NVARCHAR(100), -- invoice number (belegfeld 1) + buchungstext NVARCHAR(500), -- booking text (supplier/customer name + purpose) + beleglink NVARCHAR(500) -- document link ); +-- Create Konto table +CREATE TABLE Konto ( + id INT IDENTITY(1,1) PRIMARY KEY, + konto NVARCHAR(10) NOT NULL, + name NVARCHAR(255) NOT NULL +); + +-- Create BU table +CREATE TABLE BU ( + id INT IDENTITY(1,1) PRIMARY KEY, + bu NVARCHAR(10) NOT NULL + name NVARCHAR(255) NOT NULL +); + +/* +CSV + umsatz brutto , + soll / haben kz ( S = eingang, H = ausgang), + ,,, + konto (XXXX , z.b. 5400 = wareneingang 19%), + gegenkonto (70XXX), + bu (9 = 19%vst , 8 = 7%vst, 506 = dienstleistung aus EU, 511 = dienstleistung ausserhalb EU), + buchungsdatum, (MDD) + rechnungsnummer (belegfeld 1), + ,, + buchungstext (lierferantenname / kundenname , + verwendungszweck) + ,,,,, + beleglink + + + -- + nicht abziehbare vorstreuer buchen auf 5600 + + + + +*/ + -- Create indexes for better performance CREATE INDEX IX_Users_Email ON Users(email); CREATE INDEX IX_Users_GoogleId ON Users(google_id); CREATE INDEX IX_UserPreferences_UserId ON UserPreferences(user_id); +CREATE INDEX IX_Kreditor_IBAN ON Kreditor(iban); +CREATE INDEX IX_Kreditor_KreditorId ON Kreditor(kreditorId); +CREATE INDEX IX_AccountingItems_Buchungsdatum ON AccountingItems(buchungsdatum); +CREATE INDEX IX_AccountingItems_Konto ON AccountingItems(konto); +CREATE INDEX IX_AccountingItems_Rechnungsnummer ON AccountingItems(rechnungsnummer); +CREATE INDEX IX_AccountingItems_SollHabenKz ON AccountingItems(soll_haben_kz); + +-- Add FK from AccountingItems.bu -> BU(bu) +ALTER TABLE AccountingItems +ADD CONSTRAINT FK_AccountingItems_BU_BU +FOREIGN KEY (bu) REFERENCES BU(bu); + +-- Add FK from AccountingItems.gegenkonto -> Kreditor(kreditorId) +ALTER TABLE AccountingItems +ADD CONSTRAINT FK_AccountingItems_Gegenkonto_Kreditor +FOREIGN KEY (gegenkonto) REFERENCES Kreditor(kreditorId); + +-- Add FK from AccountingItems.konto -> Konto(konto) +ALTER TABLE AccountingItems +ADD CONSTRAINT FK_AccountingItems_Konto_Konto +FOREIGN KEY (konto) REFERENCES Konto(konto); -- Insert sample data (optional) -- Note: This will only work after you have real Google user data diff --git a/src/routes/data.js b/src/routes/data.js index e6ed0c8..e5e7100 100644 --- a/src/routes/data.js +++ b/src/routes/data.js @@ -558,4 +558,183 @@ router.get('/pdf/pdfobject/:kPdfObjekt', authenticateToken, async (req, res) => } }); +// Kreditor API endpoints + +// Get all kreditors +router.get('/kreditors', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../config/database'); + const query = ` + SELECT id, iban, name, kreditorId, created_at, updated_at + FROM Kreditor + WHERE is_active = 1 + ORDER BY name ASC + `; + + const result = await executeQuery(query); + res.json(result.recordset || []); + } catch (error) { + console.error('Error fetching kreditors:', error); + res.status(500).json({ error: 'Failed to fetch kreditors' }); + } +}); + +// Get kreditor by ID +router.get('/kreditors/:id', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../config/database'); + const { id } = req.params; + + const query = ` + SELECT id, iban, name, kreditorId, created_at, updated_at + FROM Kreditor + WHERE id = @id AND is_active = 1 + `; + + const result = await executeQuery(query, [ + { name: 'id', type: 'int', value: parseInt(id) } + ]); + + if (result.recordset.length === 0) { + return res.status(404).json({ error: 'Kreditor not found' }); + } + + res.json(result.recordset[0]); + } catch (error) { + console.error('Error fetching kreditor:', error); + res.status(500).json({ error: 'Failed to fetch kreditor' }); + } +}); + +// Create new kreditor +router.post('/kreditors', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../config/database'); + const { iban, name, kreditorId } = req.body; + + // Validate required fields + if (!iban || !name || !kreditorId) { + return res.status(400).json({ error: 'IBAN, name, and kreditorId are required' }); + } + + // Check if kreditor with same IBAN or kreditorId already exists + const checkQuery = ` + SELECT id FROM Kreditor + WHERE (iban = @iban OR kreditorId = @kreditorId) AND is_active = 1 + `; + + const checkResult = await executeQuery(checkQuery, [ + { name: 'iban', type: 'nvarchar', value: iban }, + { name: 'kreditorId', type: 'nvarchar', value: kreditorId } + ]); + + if (checkResult.recordset.length > 0) { + return res.status(409).json({ error: 'Kreditor with this IBAN or kreditorId already exists' }); + } + + const insertQuery = ` + INSERT INTO Kreditor (iban, name, kreditorId, created_at, updated_at) + OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId, INSERTED.created_at, INSERTED.updated_at + VALUES (@iban, @name, @kreditorId, GETDATE(), GETDATE()) + `; + + const result = await executeQuery(insertQuery, [ + { name: 'iban', type: 'nvarchar', value: iban }, + { name: 'name', type: 'nvarchar', value: name }, + { name: 'kreditorId', type: 'nvarchar', value: kreditorId } + ]); + + res.status(201).json(result.recordset[0]); + } catch (error) { + console.error('Error creating kreditor:', error); + res.status(500).json({ error: 'Failed to create kreditor' }); + } +}); + +// Update kreditor +router.put('/kreditors/:id', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../config/database'); + const { id } = req.params; + const { iban, name, kreditorId } = req.body; + + // Validate required fields + if (!iban || !name || !kreditorId) { + return res.status(400).json({ error: 'IBAN, name, and kreditorId are required' }); + } + + // Check if kreditor exists + const checkQuery = `SELECT id FROM Kreditor WHERE id = @id AND is_active = 1`; + const checkResult = await executeQuery(checkQuery, [ + { name: 'id', type: 'int', value: parseInt(id) } + ]); + + if (checkResult.recordset.length === 0) { + return res.status(404).json({ error: 'Kreditor not found' }); + } + + // Check for conflicts with other kreditors + const conflictQuery = ` + SELECT id FROM Kreditor + WHERE (iban = @iban OR kreditorId = @kreditorId) AND id != @id AND is_active = 1 + `; + + const conflictResult = await executeQuery(conflictQuery, [ + { name: 'iban', type: 'nvarchar', value: iban }, + { name: 'kreditorId', type: 'nvarchar', value: kreditorId }, + { name: 'id', type: 'int', value: parseInt(id) } + ]); + + if (conflictResult.recordset.length > 0) { + return res.status(409).json({ error: 'Another kreditor with this IBAN or kreditorId already exists' }); + } + + const updateQuery = ` + UPDATE Kreditor + SET iban = @iban, name = @name, kreditorId = @kreditorId, updated_at = GETDATE() + OUTPUT INSERTED.id, INSERTED.iban, INSERTED.name, INSERTED.kreditorId, INSERTED.created_at, INSERTED.updated_at + WHERE id = @id + `; + + const result = await executeQuery(updateQuery, [ + { name: 'iban', type: 'nvarchar', value: iban }, + { name: 'name', type: 'nvarchar', value: name }, + { name: 'kreditorId', type: 'nvarchar', value: kreditorId }, + { name: 'id', type: 'int', value: parseInt(id) } + ]); + + res.json(result.recordset[0]); + } catch (error) { + console.error('Error updating kreditor:', error); + res.status(500).json({ error: 'Failed to update kreditor' }); + } +}); + +// Delete kreditor (soft delete) +router.delete('/kreditors/:id', authenticateToken, async (req, res) => { + try { + const { executeQuery } = require('../config/database'); + const { id } = req.params; + + const query = ` + UPDATE Kreditor + SET is_active = 0, updated_at = GETDATE() + WHERE id = @id AND is_active = 1 + `; + + const result = await executeQuery(query, [ + { name: 'id', type: 'int', value: parseInt(id) } + ]); + + if (result.rowsAffected[0] === 0) { + return res.status(404).json({ error: 'Kreditor not found' }); + } + + res.json({ message: 'Kreditor deleted successfully' }); + } catch (error) { + console.error('Error deleting kreditor:', error); + res.status(500).json({ error: 'Failed to delete kreditor' }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index e2de45e..28dba89 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -45,7 +45,7 @@ module.exports = { REACT_APP_GOOGLE_CLIENT_ID: JSON.stringify(process.env.GOOGLE_CLIENT_ID), }, }), - new ReactRefreshWebpackPlugin(), + ...(process.env.NODE_ENV === 'development' ? [new ReactRefreshWebpackPlugin()] : []), ], devServer: { static: {