UI consistency & code quality improvements
- Standardized InvoiceEditor CreateNew tab styling to match ObjectEditor design - Fixed CodeMirror focus issues during editing by removing problematic dependencies - Removed copy button from mindmap view for cleaner interface - Resolved ESLint warnings in PostmanTreeTable.js with useCallback optimization - Enhanced PDF generation with dynamic style swapping for better print output - Updated commits.json with latest changes
This commit is contained in:
152
src/components/CodeMirrorEditor.js
Normal file
152
src/components/CodeMirrorEditor.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { basicSetup } from 'codemirror';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
||||
|
||||
const CodeMirrorEditor = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = '',
|
||||
className = '',
|
||||
language = 'json',
|
||||
maxLines = 12,
|
||||
showToggle = true
|
||||
}) => {
|
||||
const editorRef = useRef(null);
|
||||
const viewRef = useRef(null);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
// Check for dark mode
|
||||
useEffect(() => {
|
||||
const checkDarkMode = () => {
|
||||
setIsDark(document.documentElement.classList.contains('dark'));
|
||||
};
|
||||
|
||||
checkDarkMode();
|
||||
|
||||
// Watch for dark mode changes
|
||||
const observer = new MutationObserver(checkDarkMode);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// Initialize editor only once
|
||||
useEffect(() => {
|
||||
if (!editorRef.current || viewRef.current) return;
|
||||
|
||||
const extensions = [
|
||||
basicSetup,
|
||||
language === 'json' ? json() : [],
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
fontSize: '14px',
|
||||
width: '100%',
|
||||
},
|
||||
'.cm-content': {
|
||||
padding: '12px',
|
||||
},
|
||||
'.cm-focused': {
|
||||
outline: 'none',
|
||||
},
|
||||
'.cm-editor': {
|
||||
borderRadius: '6px',
|
||||
}
|
||||
}),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged && onChange) {
|
||||
onChange(update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
...(isDark ? [oneDark] : [])
|
||||
].filter(Boolean);
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: value || '',
|
||||
extensions
|
||||
});
|
||||
|
||||
const view = new EditorView({
|
||||
state,
|
||||
parent: editorRef.current
|
||||
});
|
||||
|
||||
viewRef.current = view;
|
||||
|
||||
return () => {
|
||||
if (viewRef.current) {
|
||||
viewRef.current.destroy();
|
||||
viewRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isDark]); // Only recreate on theme change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
// Handle height changes without recreating editor
|
||||
useEffect(() => {
|
||||
if (!viewRef.current) return;
|
||||
|
||||
const editorElement = editorRef.current?.querySelector('.cm-editor');
|
||||
if (editorElement) {
|
||||
if (isExpanded) {
|
||||
editorElement.style.height = 'auto';
|
||||
editorElement.style.maxHeight = 'none';
|
||||
} else {
|
||||
editorElement.style.height = '350px';
|
||||
editorElement.style.maxHeight = '350px';
|
||||
}
|
||||
}
|
||||
}, [isExpanded]);
|
||||
|
||||
// Update content when value changes externally
|
||||
useEffect(() => {
|
||||
if (viewRef.current && value !== viewRef.current.state.doc.toString()) {
|
||||
const transaction = viewRef.current.state.update({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: viewRef.current.state.doc.length,
|
||||
insert: value || ''
|
||||
}
|
||||
});
|
||||
viewRef.current.dispatch(transaction);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
<div
|
||||
ref={editorRef}
|
||||
className={`dewedev-code-mirror border border-gray-300 dark:border-gray-600 rounded-md ${
|
||||
isDark ? 'bg-gray-900' : 'bg-white'
|
||||
} ${isExpanded ? 'h-auto' : 'h-[350px]'}`}
|
||||
/>
|
||||
{showToggle && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsExpanded(!isExpanded);
|
||||
setTimeout(() => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}, 50);
|
||||
}}
|
||||
className="absolute bottom-2 right-2 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-600 shadow-sm z-10"
|
||||
title={isExpanded ? 'Collapse' : 'Expand'}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
) : (
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeMirrorEditor;
|
||||
@@ -20,8 +20,6 @@ import {
|
||||
ToggleLeft,
|
||||
FileText,
|
||||
Zap,
|
||||
Copy,
|
||||
Check,
|
||||
Eye,
|
||||
Code,
|
||||
Maximize,
|
||||
@@ -35,27 +33,10 @@ import {
|
||||
// Custom node component for different data types
|
||||
const CustomNode = ({ data, selected }) => {
|
||||
const [renderHtml, setRenderHtml] = React.useState(true);
|
||||
const [isCopied, setIsCopied] = React.useState(false);
|
||||
|
||||
// Check if value contains HTML
|
||||
const isHtmlContent = data.value && typeof data.value === 'string' &&
|
||||
(data.value.includes('<') && data.value.includes('>'));
|
||||
|
||||
// Copy value to clipboard
|
||||
const copyValue = async () => {
|
||||
if (data.value) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(String(data.value));
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2500);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
// Still show feedback even if copy failed
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2500);
|
||||
}
|
||||
}
|
||||
};
|
||||
const getIcon = () => {
|
||||
switch (data.type) {
|
||||
case 'object':
|
||||
@@ -169,27 +150,6 @@ const CustomNode = ({ data, selected }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions - Third flex item */}
|
||||
<div className="flex-shrink-0 flex flex-col items-center space-y-1">
|
||||
{data.value && (
|
||||
<button
|
||||
onClick={copyValue}
|
||||
className={`rounded-full p-1 opacity-80 hover:opacity-100 transition-all shadow-md ${
|
||||
isCopied
|
||||
? 'bg-green-500 hover:bg-green-600'
|
||||
: 'bg-blue-500 hover:bg-blue-600'
|
||||
} text-white`}
|
||||
title={isCopied ? "Copied!" : "Copy value to clipboard"}
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-2.5 w-2.5" />
|
||||
) : (
|
||||
<Copy className="h-2.5 w-2.5" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{/* Future action buttons can be added here */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Output handle (right side) */}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { ChevronRight, ChevronDown, Copy, Search, Filter } from 'lucide-react';
|
||||
import CopyButton from './CopyButton';
|
||||
|
||||
@@ -8,7 +8,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
|
||||
const [filterType, setFilterType] = useState('all');
|
||||
|
||||
// Flatten the data structure for table display
|
||||
const flattenData = (obj, path = '', level = 0) => {
|
||||
const flattenData = useCallback((obj, path = '', level = 0) => {
|
||||
const result = [];
|
||||
|
||||
if (obj === null || obj === undefined) {
|
||||
@@ -74,7 +74,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}, [expandedPaths]);
|
||||
|
||||
const toggleExpanded = (path) => {
|
||||
const newExpanded = new Set(expandedPaths);
|
||||
@@ -86,7 +86,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
|
||||
setExpandedPaths(newExpanded);
|
||||
};
|
||||
|
||||
const flatData = useMemo(() => flattenData(data), [data, expandedPaths]);
|
||||
const flatData = useMemo(() => flattenData(data), [data, flattenData]);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
return flatData.filter(item => {
|
||||
@@ -129,7 +129,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
|
||||
};
|
||||
|
||||
const getKeyDisplay = (key, level) => {
|
||||
const parts = key.split(/[.\[\]]+/).filter(Boolean);
|
||||
const parts = key.split(/[.[\]]+/).filter(Boolean);
|
||||
return parts[parts.length - 1] || key;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
|
||||
const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
||||
// Get the chosen color scheme or default to golden
|
||||
const accentColor = invoiceData.settings?.colorScheme || '#D4AF37';
|
||||
@@ -21,7 +22,8 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.4',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
color: '#000000'
|
||||
color: '#000000',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{/* Header Section - 2 Column Layout */}
|
||||
<div style={{
|
||||
@@ -118,7 +120,7 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
||||
|
||||
{/* FROM Section */}
|
||||
{(invoiceData.settings?.showFromSection ?? true) && (
|
||||
<div style={{
|
||||
<div className={'invoice-from-to-card'} style={{
|
||||
padding: '20px',
|
||||
background: `${accentColor}08`,
|
||||
borderRadius: '4px',
|
||||
@@ -147,7 +149,7 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
||||
)}
|
||||
|
||||
{/* TO Section */}
|
||||
<div style={{
|
||||
<div className={'invoice-from-to-card'} style={{
|
||||
padding: '20px',
|
||||
background: `${accentColor}08`,
|
||||
borderRadius: '4px',
|
||||
@@ -167,8 +169,8 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
|
||||
{invoiceData.client.name || 'Acme Corporation'}
|
||||
</div>
|
||||
<div>{invoiceData.client.address || '456 Business Ave'}</div>
|
||||
<div>{invoiceData.client.city || 'New York, NY 10001'}</div>
|
||||
{invoiceData.client.address && <div>{invoiceData.client.address}</div>}
|
||||
{invoiceData.client.city && <div>{invoiceData.client.city}</div>}
|
||||
{invoiceData.client.phone && <div>{invoiceData.client.phone}</div>}
|
||||
{invoiceData.client.email && <div>{invoiceData.client.email}</div>}
|
||||
</div>
|
||||
@@ -312,7 +314,7 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
||||
className={pageBreaks.beforeItemsTable ? 'page-break-before' : ''}
|
||||
style={{ marginBottom: '20px', marginTop: spacingMap[sectionSpacing] }}
|
||||
>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<table className="invoice-table" style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
{/* Table Header */}
|
||||
<thead>
|
||||
<tr style={{
|
||||
@@ -320,36 +322,40 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
||||
backgroundColor: `${accentColor}15` // 15 = ~8% opacity
|
||||
}}>
|
||||
<th style={{
|
||||
padding: '12px 0 12px 16px',
|
||||
padding: '12px 0px 12px 16px',
|
||||
textAlign: 'left',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: accentColor,
|
||||
verticalAlign: 'middle'
|
||||
verticalAlign: 'middle',
|
||||
height: '48px'
|
||||
}}>Item</th>
|
||||
<th style={{
|
||||
padding: '12px 0',
|
||||
padding: '12px 0px',
|
||||
textAlign: 'center',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: accentColor,
|
||||
verticalAlign: 'middle'
|
||||
verticalAlign: 'middle',
|
||||
height: '48px'
|
||||
}}>Quantity</th>
|
||||
<th style={{
|
||||
padding: '12px 0',
|
||||
padding: '12px 0px',
|
||||
textAlign: 'center',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: accentColor,
|
||||
verticalAlign: 'middle'
|
||||
verticalAlign: 'middle',
|
||||
height: '48px'
|
||||
}}>Unit Price</th>
|
||||
<th style={{
|
||||
padding: '12px 16px 12px 0',
|
||||
padding: '12px 16px 12px 0px',
|
||||
textAlign: 'right',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: accentColor,
|
||||
verticalAlign: 'middle'
|
||||
verticalAlign: 'middle',
|
||||
height: '48px'
|
||||
}}>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -360,38 +366,42 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
||||
borderBottom: '1px solid #E5E5E5'
|
||||
}}>
|
||||
<td style={{
|
||||
padding: '16px 0 16px 16px',
|
||||
padding: '16px 0px 16px 16px',
|
||||
fontSize: '14px',
|
||||
color: '#000000',
|
||||
verticalAlign: 'middle'
|
||||
verticalAlign: 'middle',
|
||||
height: '52px'
|
||||
}}>
|
||||
{item.description}
|
||||
</td>
|
||||
<td style={{
|
||||
padding: '16px 0',
|
||||
padding: '16px 0px',
|
||||
textAlign: 'center',
|
||||
fontSize: '14px',
|
||||
color: '#000000',
|
||||
verticalAlign: 'middle'
|
||||
verticalAlign: 'middle',
|
||||
height: '52px'
|
||||
}}>
|
||||
{item.quantity}
|
||||
</td>
|
||||
<td style={{
|
||||
padding: '16px 0',
|
||||
textAlign: 'center',
|
||||
padding: '16px 0px',
|
||||
textAlign: 'right',
|
||||
fontSize: '14px',
|
||||
color: '#000000',
|
||||
verticalAlign: 'middle'
|
||||
verticalAlign: 'middle',
|
||||
height: '52px'
|
||||
}}>
|
||||
{formatCurrency(item.rate, true)}
|
||||
</td>
|
||||
<td style={{
|
||||
padding: '16px 16px 16px 0',
|
||||
padding: '16px 16px 16px 0px',
|
||||
textAlign: 'right',
|
||||
fontSize: '14px',
|
||||
color: '#000000',
|
||||
fontWeight: 'bold',
|
||||
verticalAlign: 'middle'
|
||||
verticalAlign: 'middle',
|
||||
height: '52px'
|
||||
}}>
|
||||
{formatCurrency(item.amount, true)}
|
||||
</td>
|
||||
@@ -439,7 +449,11 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
||||
{invoiceData.paymentMethod.bankDetails?.iban && (
|
||||
<div style={{ marginBottom: '4px' }}>IBAN: {invoiceData.paymentMethod.bankDetails.iban}</div>
|
||||
)}
|
||||
{invoiceData.dueDate && <div>Pay by: {invoiceData.dueDate}</div>}
|
||||
{invoiceData.dueDate && (
|
||||
<div>
|
||||
Pay by: {invoiceData.dueDate}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -484,17 +498,73 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
||||
<div style={{ fontSize: '12px', color: '#666666', marginBottom: '4px' }}>
|
||||
{invoiceData.paymentMethod.qrCode.label || 'Scan to Pay'}
|
||||
</div>
|
||||
{invoiceData.dueDate && <div>Pay by: {invoiceData.dueDate}</div>}
|
||||
{invoiceData.dueDate && (
|
||||
<div>
|
||||
Pay by: {invoiceData.dueDate}
|
||||
{invoiceData.settings?.paymentStatus === 'PAID' && invoiceData.settings?.paymentDate && (
|
||||
<div style={{ color: '#22c55e', fontWeight: 'bold', marginTop: '4px' }}>
|
||||
Paid at: {invoiceData.settings.paymentDate}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment Status Stamp */}
|
||||
{invoiceData.settings?.paymentStatus && (
|
||||
<div style={{
|
||||
marginTop: '20px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div className={"invoice-payment-status-stamp"} style={{
|
||||
display: 'inline-block',
|
||||
transform: 'rotate(-15deg)',
|
||||
border: `3px solid ${
|
||||
invoiceData.settings.paymentStatus === 'PAID' ? '#22c55e' :
|
||||
invoiceData.settings.paymentStatus === 'PARTIALLY PAID' ? '#f59e0b' : '#ef4444'
|
||||
}`,
|
||||
borderRadius: '8px',
|
||||
padding: '8px 16px',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
color: invoiceData.settings.paymentStatus === 'PAID' ? '#22c55e' :
|
||||
invoiceData.settings.paymentStatus === 'PARTIALLY PAID' ? '#f59e0b' : '#ef4444',
|
||||
textAlign: 'center',
|
||||
letterSpacing: '2px'
|
||||
}}>
|
||||
{invoiceData.settings.paymentStatus}
|
||||
</div>
|
||||
|
||||
{/* Payment Date for PAID status */}
|
||||
{invoiceData.settings?.paymentStatus === 'PAID' && invoiceData.settings?.paymentDate && (
|
||||
<div style={{
|
||||
marginTop: '10px',
|
||||
fontSize: '12px',
|
||||
color: '#22c55e',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
Paid at: {invoiceData.settings.paymentDate}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Totals */}
|
||||
<div style={{ textAlign: 'right', minWidth: '200px' }}>
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<span style={{ fontSize: '14px', color: '#000000' }}>Subtotal</span>
|
||||
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
|
||||
<div style={{ textAlign: 'right', minWidth: '300px', width: '100%', maxWidth: '400px' }}>
|
||||
<div className="invoice-totals-row" style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '12px 16px',
|
||||
borderBottom: '1px solid #e9ecef',
|
||||
minHeight: '44px'
|
||||
}}>
|
||||
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>Subtotal</span>
|
||||
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
|
||||
{formatCurrency(invoiceData.subtotal, true)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -502,11 +572,18 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
||||
|
||||
{/* Dynamic Fees */}
|
||||
{invoiceData.fees && invoiceData.fees.map((fee) => (
|
||||
<div key={fee.id} style={{ marginBottom: '12px' }}>
|
||||
<span style={{ fontSize: '14px', color: '#000000' }}>
|
||||
<div key={fee.id} className="invoice-totals-row" style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '12px 16px',
|
||||
borderBottom: '1px solid #e9ecef',
|
||||
minHeight: '44px'
|
||||
}}>
|
||||
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
|
||||
{fee.label || 'Fee'} {fee.type === 'percentage' ? `(${fee.value}%)` : ''}
|
||||
</span>
|
||||
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
|
||||
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
|
||||
+{formatCurrency(fee.type === 'percentage' ? (invoiceData.subtotal * fee.value) / 100 : fee.value, true)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -514,40 +591,38 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
||||
|
||||
{/* Dynamic Discounts */}
|
||||
{invoiceData.discounts && invoiceData.discounts.map((discount) => (
|
||||
<div key={discount.id} style={{ marginBottom: '12px' }}>
|
||||
<span style={{ fontSize: '14px', color: '#000000' }}>
|
||||
<div key={discount.id} className="invoice-totals-row" style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '12px 16px',
|
||||
borderBottom: '1px solid #e9ecef',
|
||||
minHeight: '44px'
|
||||
}}>
|
||||
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
|
||||
{discount.label || 'Discount'} {discount.type === 'percentage' ? `(${discount.value}%)` : ''}
|
||||
</span>
|
||||
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
|
||||
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
|
||||
-{formatCurrency(discount.type === 'percentage' ? (invoiceData.subtotal * discount.value) / 100 : discount.value, true)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Legacy Discount */}
|
||||
{invoiceData.discount > 0 && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<span style={{ fontSize: '14px', color: '#000000' }}>Discount</span>
|
||||
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
|
||||
-{formatCurrency(invoiceData.discount, true)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
padding: '12px 16px 12px 16px', // Match table head padding
|
||||
marginTop: '16px',
|
||||
<div className="invoice-total-final" style={{
|
||||
padding: '16px',
|
||||
backgroundColor: `${accentColor}10`, // 10 = ~6% opacity
|
||||
borderTop: `2px solid ${accentColor}`,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
minHeight: '56px'
|
||||
}}>
|
||||
<span style={{ fontSize: '18px', fontWeight: 'bold', color: accentColor }}>Total</span>
|
||||
<span style={{ fontSize: '18px', fontWeight: 'bold', color: accentColor, lineHeight: '1.4' }}>Total</span>
|
||||
<span style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
color: accentColor
|
||||
color: accentColor,
|
||||
lineHeight: '1.4'
|
||||
}}>
|
||||
{formatCurrency(invoiceData.total, true)}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user