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:
dwindown
2025-09-28 23:30:44 +07:00
parent 78570f04f0
commit 68db19e076
13 changed files with 789 additions and 156 deletions

133
package-lock.json generated
View File

@@ -8,14 +8,16 @@
"name": "developer-tools-mvp",
"version": "1.0.0",
"dependencies": {
"@codemirror/basic-setup": "^0.20.0",
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.1",
"@codemirror/view": "^6.38.3",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@@ -1994,6 +1996,123 @@
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/basic-setup": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/@codemirror/basic-setup/-/basic-setup-0.20.0.tgz",
"integrity": "sha512-W/ERKMLErWkrVLyP5I8Yh8PXl4r+WFNkdYVSzkXYPQv2RMPSkWpr2BgggiSJ8AHF/q3GuApncDD8I4BZz65fyg==",
"deprecated": "In version 6.0, this package has been renamed to just 'codemirror'",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^0.20.0",
"@codemirror/commands": "^0.20.0",
"@codemirror/language": "^0.20.0",
"@codemirror/lint": "^0.20.0",
"@codemirror/search": "^0.20.0",
"@codemirror/state": "^0.20.0",
"@codemirror/view": "^0.20.0"
}
},
"node_modules/@codemirror/basic-setup/node_modules/@codemirror/autocomplete": {
"version": "0.20.3",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-0.20.3.tgz",
"integrity": "sha512-lYB+NPGP+LEzAudkWhLfMxhTrxtLILGl938w+RcFrGdrIc54A+UgmCoz+McE3IYRFp4xyQcL4uFJwo+93YdgHw==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^0.20.0",
"@codemirror/state": "^0.20.0",
"@codemirror/view": "^0.20.0",
"@lezer/common": "^0.16.0"
}
},
"node_modules/@codemirror/basic-setup/node_modules/@codemirror/commands": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-0.20.0.tgz",
"integrity": "sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^0.20.0",
"@codemirror/state": "^0.20.0",
"@codemirror/view": "^0.20.0",
"@lezer/common": "^0.16.0"
}
},
"node_modules/@codemirror/basic-setup/node_modules/@codemirror/language": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.20.2.tgz",
"integrity": "sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^0.20.0",
"@codemirror/view": "^0.20.0",
"@lezer/common": "^0.16.0",
"@lezer/highlight": "^0.16.0",
"@lezer/lr": "^0.16.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/basic-setup/node_modules/@codemirror/lint": {
"version": "0.20.3",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-0.20.3.tgz",
"integrity": "sha512-06xUScbbspZ8mKoODQCEx6hz1bjaq9m8W8DxdycWARMiiX1wMtfCh/MoHpaL7ws/KUMwlsFFfp2qhm32oaCvVA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^0.20.0",
"@codemirror/view": "^0.20.2",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/basic-setup/node_modules/@codemirror/search": {
"version": "0.20.1",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-0.20.1.tgz",
"integrity": "sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^0.20.0",
"@codemirror/view": "^0.20.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/basic-setup/node_modules/@codemirror/state": {
"version": "0.20.1",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz",
"integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==",
"license": "MIT"
},
"node_modules/@codemirror/basic-setup/node_modules/@codemirror/view": {
"version": "0.20.7",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz",
"integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^0.20.0",
"style-mod": "^4.0.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@codemirror/basic-setup/node_modules/@lezer/common": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz",
"integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==",
"license": "MIT"
},
"node_modules/@codemirror/basic-setup/node_modules/@lezer/highlight": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-0.16.0.tgz",
"integrity": "sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^0.16.0"
}
},
"node_modules/@codemirror/basic-setup/node_modules/@lezer/lr": {
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.16.3.tgz",
"integrity": "sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^0.16.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.8.1",
"license": "MIT",
@@ -2045,6 +2164,8 @@
},
"node_modules/@codemirror/lang-json": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@@ -2083,6 +2204,8 @@
},
"node_modules/@codemirror/state": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
@@ -2090,6 +2213,8 @@
},
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@@ -2099,7 +2224,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.38.1",
"version": "6.38.3",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.3.tgz",
"integrity": "sha512-x2t87+oqwB1mduiQZ6huIghjMt4uZKFEdj66IcXw7+a5iBEvv9lh7EWDRHI7crnD4BMGpnyq/RzmCGbiEZLcvQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
@@ -6115,6 +6242,8 @@
},
"node_modules/codemirror": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",

View File

@@ -4,14 +4,16 @@
"description": "Web Developer Tools MVP - Utilities Toolkit",
"private": true,
"dependencies": {
"@codemirror/basic-setup": "^0.20.0",
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.1",
"@codemirror/view": "^6.38.3",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",

View File

@@ -3,6 +3,30 @@
{
"date": "2025-09-28",
"changes": [
{
"datetime": "2025-09-28T23:26:38+07:00",
"type": "enhancement",
"title": "UI Consistency & Code Quality Improvements",
"description": "Standardized InvoiceEditor CreateNew tab styling to match ObjectEditor design, fixed CodeMirror focus issues during editing, removed copy button from mindmap view, resolved ESLint warnings, and improved overall code quality."
},
{
"datetime": "2025-09-28T20:11:39+07:00",
"type": "enhancement",
"title": "Invoice Settings & UI Improvements",
"description": "Reorganized invoice settings modal: renamed 'Payment Methods' to 'Payment', moved payment status controls to Payment tab, added decimal digits setting (0-3), enhanced subtotal/fees/discounts with consistent padding and background styling for better visual hierarchy."
},
{
"datetime": "2025-09-28T18:49:30+07:00",
"type": "enhancement",
"title": "Enhanced Invoice Payment Status System",
"description": "Improved payment status stamps with better positioning after total, wider subtotal card for Indonesian currency formats, added 'Partially Paid' option, payment date tracking for PAID invoices, and 'Paid at:' display in payment methods."
},
{
"datetime": "2025-09-28T17:43:49+07:00",
"type": "fix",
"title": "Invoice Preview & UI Improvements",
"description": "Fixed subtotal alignment in invoice preview, added conditional rendering for empty address fields, implemented payment status stamps, and resolved React key warnings in release notes."
},
{
"datetime": "2025-09-28T17:11:19+07:00",
"type": "enhancement",

View 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;

View File

@@ -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) */}

View File

@@ -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;
};

View File

@@ -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>

View File

@@ -5,6 +5,7 @@ import {
Globe, AlertTriangle, ChevronUp, ChevronDown
} from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CodeMirrorEditor from '../components/CodeMirrorEditor';
const InvoiceEditor = () => {
const navigate = useNavigate();
@@ -276,12 +277,13 @@ const InvoiceEditor = () => {
// Utility functions for formatting
const formatNumber = (num, useThousandSeparator = invoiceData.settings?.thousandSeparator) => {
if (!useThousandSeparator) return num.toString();
return new Intl.NumberFormat('en-US').format(num);
return new Intl.NumberFormat('en-US', { minimumFractionDigits: invoiceData.settings?.decimalDigits ?? 2 }).format(num);
};
const formatCurrency = (amount, showSymbol = true) => {
const currency = invoiceData.settings?.currency || { code: 'USD', symbol: '$' };
const formattedAmount = formatNumber(amount.toFixed(2));
const decimalDigits = invoiceData.settings?.decimalDigits ?? 2;
const formattedAmount = formatNumber(amount.toFixed(decimalDigits));
if (showSymbol && currency.symbol) {
return `${currency.symbol} ${formattedAmount}`;
@@ -777,39 +779,55 @@ const InvoiceEditor = () => {
{(activeTab !== 'create' || !createNewCompleted) && (
<div className="p-4">
{activeTab === 'create' && (
<div className="space-y-4">
<div className="text-center py-8">
<FileText className="mx-auto h-12 w-12 text-gray-400 mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
Start Building Your Invoice
</h3>
<p className="text-gray-500 dark:text-gray-400 mb-6">
Choose how you'd like to begin creating your professional invoice
</p>
<div className="text-center py-12">
<FileText className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500 mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Start Building Your Invoice
</h3>
<p className="text-gray-500 dark:text-gray-400 mb-8 max-w-md mx-auto">
Choose how you'd like to begin creating your professional invoice
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 max-w-2xl mx-auto">
<button
onClick={handleStartEmpty}
className="group relative overflow-hidden rounded-lg border-2 border-dashed border-blue-300 dark:border-blue-600 p-6 hover:border-blue-400 dark:hover:border-blue-500 transition-colors"
>
<div className="flex flex-col items-center">
<Plus className="h-8 w-8 text-blue-500 mb-3 group-hover:scale-110 transition-transform" />
<span className="text-sm font-medium text-blue-600 dark:text-blue-400">Start Empty</span>
<span className="text-xs text-gray-500 dark:text-gray-400 mt-1">Create a blank invoice</span>
</div>
</button>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<button
onClick={() => {
if (hasModifiedData()) {
setPendingTabChange('create_empty');
setShowInputChangeModal(true);
} else {
handleStartEmpty();
}
}}
className="flex flex-col items-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-blue-500 dark:hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors group"
>
<Plus className="h-8 w-8 text-gray-400 group-hover:text-blue-500 dark:group-hover:text-blue-400 mb-2" />
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-blue-600 dark:group-hover:text-blue-400">
Start Empty
</span>
<span className="text-xs text-gray-500 dark:text-gray-400 text-center mt-1">
Create a blank invoice
</span>
</button>
<button
onClick={handleLoadSample}
className="group relative overflow-hidden rounded-lg border-2 border-dashed border-green-300 dark:border-green-600 p-6 hover:border-green-400 dark:hover:border-green-500 transition-colors"
>
<div className="flex flex-col items-center">
<FileText className="h-8 w-8 text-green-500 mb-3 group-hover:scale-110 transition-transform" />
<span className="text-sm font-medium text-green-600 dark:text-green-400">Load Sample</span>
<span className="text-xs text-gray-500 dark:text-gray-400 mt-1">Start with example invoice</span>
</div>
</button>
</div>
<button
onClick={() => {
if (hasModifiedData()) {
setPendingTabChange('create_sample');
setShowInputChangeModal(true);
} else {
handleLoadSample();
}
}}
className="flex flex-col items-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-green-500 dark:hover:border-green-400 hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors group"
>
<FileText className="h-8 w-8 text-gray-400 group-hover:text-green-500 dark:group-hover:text-green-400 mb-2" />
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-green-600 dark:group-hover:text-green-400">
Load Sample
</span>
<span className="text-xs text-gray-500 dark:text-gray-400 text-center mt-1">
Start with example invoice
</span>
</button>
</div>
</div>
)}
@@ -864,11 +882,14 @@ const InvoiceEditor = () => {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Paste Invoice JSON Data
</label>
<textarea
<CodeMirrorEditor
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onChange={setInputText}
placeholder="Paste your invoice JSON data here..."
className="w-full h-32 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
language="json"
maxLines={12}
showToggle={true}
className="w-full"
/>
</div>
<button
@@ -1194,7 +1215,7 @@ const InvoiceEditor = () => {
/>
</td>
<td className="px-3 py-3 text-center" style={{ width: 'auto' }}>
<div className="relative flex justify-center items-center">
<div className="relative flex justify-end items-center">
<span className="text-gray-500 dark:text-gray-400 px-2 py-1 text-xs rounded-1 bg-gray-100 dark:bg-gray-900/20">{invoiceData.settings?.currency?.symbol || '$'}</span>
<input
type="text"
@@ -1203,7 +1224,7 @@ const InvoiceEditor = () => {
const numValue = parseFloat(e.target.value.replace(/,/g, '')) || 0;
updateLineItem(item.id, 'rate', numValue);
}}
className="pl-2 pr-2 py-1 text-center border-0 bg-transparent focus:outline-none focus:ring-0 focus:bg-blue-50 dark:focus:bg-blue-900/20 rounded text-gray-900 dark:text-white transition-colors"
className="pl-2 py-1 text-center border-0 bg-transparent focus:outline-none focus:ring-0 focus:bg-blue-50 dark:focus:bg-blue-900/20 rounded text-gray-900 dark:text-white transition-colors"
style={{ width: `${Math.max(formatNumber(item.rate).length * 8 + 20, 40)}px` }}
onFocus={(e) => {
// Show raw number on focus
@@ -2156,7 +2177,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}
>
Payment Methods
Payment
{activeTab === 'payment' && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500 dark:bg-blue-400"></div>
)}
@@ -2264,6 +2285,27 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
</div>
</label>
</div>
{/* Decimal Digits */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Decimal Digits
</label>
<select
value={invoiceData.settings?.decimalDigits || 2}
onChange={(e) => onUpdateSettings('decimalDigits', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value={0}>0 (1000)</option>
<option value={1}>1 (1000.0)</option>
<option value={2}>2 (1000.00)</option>
<option value={3}>3 (1000.000)</option>
</select>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
Number of decimal places to display
</p>
</div>
</div>
)}
@@ -2601,6 +2643,46 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
</div>
</div>
)}
{/* Payment Status */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Payment Status Stamp
</label>
<select
value={invoiceData.settings?.paymentStatus || ''}
onChange={(e) => onUpdateSettings('paymentStatus', e.target.value || null)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="">No Status Stamp</option>
<option value="PAID">PAID</option>
<option value="PARTIALLY PAID">PARTIALLY PAID</option>
<option value="UNPAID">UNPAID</option>
<option value="OVERDUE">OVERDUE</option>
<option value="PENDING">PENDING</option>
</select>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
Add a status stamp to your invoice PDF
</p>
</div>
{/* Payment Date - only show when PAID is selected */}
{invoiceData.settings?.paymentStatus === 'PAID' && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Payment Date
</label>
<input
type="date"
value={invoiceData.settings?.paymentDate || ''}
onChange={(e) => onUpdateSettings('paymentDate', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
/>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
Date when payment was received
</p>
</div>
)}
</div>
)}
</div>

View File

@@ -101,22 +101,130 @@ const InvoicePreview = () => {
// Format number with thousand separator
const formatNumber = (num) => {
if (!invoiceData?.settings?.thousandSeparator) return num.toString();
return num.toLocaleString('en-US', { minimumFractionDigits: 2 });
const decimalDigits = invoiceData?.settings?.decimalDigits ?? 2;
if (!invoiceData?.settings?.thousandSeparator) return num.toFixed(decimalDigits);
return num.toLocaleString('en-US', { minimumFractionDigits: decimalDigits, maximumFractionDigits: decimalDigits });
};
// Format currency
const formatCurrency = (amount, useThousandSeparator = false) => {
const symbol = invoiceData?.settings?.currency?.symbol || '$';
const formattedAmount = useThousandSeparator ? formatNumber(amount) : amount.toFixed(2);
const decimalDigits = invoiceData?.settings?.decimalDigits ?? 2;
const formattedAmount = useThousandSeparator ? formatNumber(amount) : amount.toFixed(decimalDigits);
return `${symbol} ${formattedAmount}`;
};
// Utility function to temporarily apply print-optimized styles
const applyPrintStyles = () => {
const element = document.getElementById('invoice-content');
if (!element) return null;
// Store original styles
const originalStyles = new Map();
// Find all table elements and store their original styles
const tables = element.querySelectorAll('table');
tables.forEach((table, index) => {
originalStyles.set(`table-${index}`, table.style.cssText);
table.style.borderCollapse = 'collapse';
});
// Find all th and td elements and apply print styles
const cells = element.querySelectorAll('th, td');
cells.forEach((cell, index) => {
originalStyles.set(`cell-${index}`, cell.style.cssText);
cell.style.verticalAlign = 'middle';
cell.style.padding = '4px 12px 20px';
cell.style.lineHeight = '1.4';
});
// Find all totals rows and apply print styles
const totalsRows = element.querySelectorAll('.invoice-totals-row, .invoice-total-final');
totalsRows.forEach((row, index) => {
originalStyles.set(`totals-${index}`, row.style.cssText);
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.justifyContent = 'space-between';
row.style.padding = '4px 12px 20px';
row.style.minHeight = '44px';
});
// Find all stamp cards and apply print styles
const stampCards = element.querySelectorAll('.invoice-payment-status-stamp');
stampCards.forEach((stampCard, index) => {
originalStyles.set(`stamp-${index}`, stampCard.style.cssText);
stampCard.style.padding = '4px 12px 20px';
});
// Find all cards and apply print styles
const fromToCards = element.querySelectorAll('.invoice-from-to-card');
fromToCards.forEach((fromToCard, index) => {
originalStyles.set(`fromTo-${index}`, fromToCard.style.cssText);
fromToCard.style.padding = '4px 12px 20px';
});
return originalStyles;
};
// Utility function to restore original styles
const restoreOriginalStyles = (originalStyles) => {
if (!originalStyles) return;
const element = document.getElementById('invoice-content');
if (!element) return;
// Restore table styles
const tables = element.querySelectorAll('table');
tables.forEach((table, index) => {
const originalStyle = originalStyles.get(`table-${index}`);
if (originalStyle !== undefined) {
table.style.cssText = originalStyle;
}
});
// Restore cell styles
const cells = element.querySelectorAll('th, td');
cells.forEach((cell, index) => {
const originalStyle = originalStyles.get(`cell-${index}`);
if (originalStyle !== undefined) {
cell.style.cssText = originalStyle;
}
});
// Restore totals row styles
const totalsRows = element.querySelectorAll('.invoice-totals-row, .invoice-total-final');
totalsRows.forEach((row, index) => {
const originalStyle = originalStyles.get(`totals-${index}`);
if (originalStyle !== undefined) {
row.style.cssText = originalStyle;
}
});
// Restore stamp card styles
const stampCards = element.querySelectorAll('.invoice-payment-status-stamp');
stampCards.forEach((stampCard, index) => {
const originalStyle = originalStyles.get(`stamp-${index}`);
if (originalStyle !== undefined) {
stampCard.style.cssText = originalStyle;
}
});
// Restore fromTo card styles
const fromToCards = element.querySelectorAll('.invoice-from-to-card');
fromToCards.forEach((fromToCard, index) => {
const originalStyle = originalStyles.get(`fromTo-${index}`);
if (originalStyle !== undefined) {
fromToCard.style.cssText = originalStyle;
}
});
};
// Generate PDF from the visible invoice
const handleDownloadPDF = async () => {
if (!invoiceData) return;
setIsGenerating(true);
let originalStyles = null;
try {
const element = document.getElementById('invoice-content');
@@ -124,8 +232,14 @@ const InvoicePreview = () => {
throw new Error('Invoice content not found');
}
// Apply print-optimized styles temporarily
originalStyles = applyPrintStyles();
// Small delay to ensure styles are applied
await new Promise(resolve => setTimeout(resolve, 100));
const opt = {
margin: [0.2, 0.4, 0.5, 0.4], // top, left, bottom, right margins in inches - reduced top margin for first page
margin: [0.2, 0.4, 0.8, 0.4], // top, left, bottom, right margins in inches - increased bottom margin to prevent elements dropping
filename: `invoice-${invoiceData.invoiceNumber || 'draft'}.pdf`,
image: { type: 'png', quality: 0.98 },
html2canvas: {
@@ -151,6 +265,11 @@ const InvoicePreview = () => {
console.error('PDF generation failed:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
// Restore original styles after a short delay
setTimeout(() => {
restoreOriginalStyles(originalStyles);
}, 500);
setIsGenerating(false);
}
};

View File

@@ -36,22 +36,98 @@ const InvoicePreviewMinimal = () => {
// Format number with thousand separator
const formatNumber = (num) => {
if (!invoiceData?.settings?.thousandSeparator) return num.toString();
return num.toLocaleString();
const decimalDigits = invoiceData?.settings?.decimalDigits ?? 2;
if (!invoiceData?.settings?.thousandSeparator) return num.toFixed(decimalDigits);
return num.toLocaleString('en-US', { minimumFractionDigits: decimalDigits, maximumFractionDigits: decimalDigits });
};
// Format currency
const formatCurrency = (amount, useThousandSeparator = false) => {
const symbol = invoiceData?.settings?.currency?.symbol || '$';
const formattedAmount = useThousandSeparator ? formatNumber(amount) : amount.toFixed(2);
const decimalDigits = invoiceData?.settings?.decimalDigits ?? 2;
const formattedAmount = useThousandSeparator ? formatNumber(amount) : amount.toFixed(decimalDigits);
return `${symbol}${formattedAmount}`;
};
// Utility function to temporarily apply print-optimized styles
const applyPrintStyles = () => {
const element = document.getElementById('minimal-invoice-content');
if (!element) return null;
// Store original styles
const originalStyles = new Map();
// Find all table elements and store their original styles
const tables = element.querySelectorAll('table');
tables.forEach((table, index) => {
originalStyles.set(`table-${index}`, table.style.cssText);
table.style.borderCollapse = 'collapse';
});
// Find all th and td elements and apply print styles
const cells = element.querySelectorAll('th, td');
cells.forEach((cell, index) => {
originalStyles.set(`cell-${index}`, cell.style.cssText);
cell.style.verticalAlign = 'middle';
cell.style.padding = '12px 16px';
cell.style.lineHeight = '1.4';
});
// Find all totals rows and apply print styles
const totalsRows = element.querySelectorAll('.invoice-totals-row, .invoice-total-final');
totalsRows.forEach((row, index) => {
originalStyles.set(`totals-${index}`, row.style.cssText);
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.justifyContent = 'space-between';
row.style.padding = '12px 16px';
row.style.minHeight = '44px';
});
return originalStyles;
};
// Utility function to restore original styles
const restoreOriginalStyles = (originalStyles) => {
if (!originalStyles) return;
const element = document.getElementById('minimal-invoice-content');
if (!element) return;
// Restore table styles
const tables = element.querySelectorAll('table');
tables.forEach((table, index) => {
const originalStyle = originalStyles.get(`table-${index}`);
if (originalStyle !== undefined) {
table.style.cssText = originalStyle;
}
});
// Restore cell styles
const cells = element.querySelectorAll('th, td');
cells.forEach((cell, index) => {
const originalStyle = originalStyles.get(`cell-${index}`);
if (originalStyle !== undefined) {
cell.style.cssText = originalStyle;
}
});
// Restore totals row styles
const totalsRows = element.querySelectorAll('.invoice-totals-row, .invoice-total-final');
totalsRows.forEach((row, index) => {
const originalStyle = originalStyles.get(`totals-${index}`);
if (originalStyle !== undefined) {
row.style.cssText = originalStyle;
}
});
};
// Generate PDF from the visible invoice
const handleDownloadPDF = async () => {
if (!invoiceData) return;
setIsGenerating(true);
let originalStyles = null;
try {
const element = document.getElementById('minimal-invoice-content');
@@ -60,8 +136,14 @@ const InvoicePreviewMinimal = () => {
throw new Error('Invoice content not found');
}
// Apply print-optimized styles temporarily
originalStyles = applyPrintStyles();
// Small delay to ensure styles are applied
await new Promise(resolve => setTimeout(resolve, 100));
const opt = {
margin: [15, 15, 15, 15],
margin: [20, 20, 40, 20], // Increased bottom margin to prevent elements dropping
filename: `Invoice-${invoiceData.invoiceNumber || new Date().toISOString().split('T')[0]}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
@@ -85,6 +167,11 @@ const InvoicePreviewMinimal = () => {
console.error('PDF generation failed:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
// Restore original styles after a short delay
setTimeout(() => {
restoreOriginalStyles(originalStyles);
}, 500);
setIsGenerating(false);
}
};

View File

@@ -5,6 +5,7 @@ import StructuredEditor from '../components/StructuredEditor';
import MindmapView from '../components/MindmapView';
import PostmanTable from '../components/PostmanTable';
import CodeEditor from '../components/CodeEditor';
import CodeMirrorEditor from '../components/CodeMirrorEditor';
// Hook to detect dark mode
const useDarkMode = () => {
@@ -785,14 +786,14 @@ const ObjectEditor = () => {
</span>
)}
</div>
<CodeEditor
<CodeMirrorEditor
value={inputText}
onChange={(value) => handleInputChange(value)}
language={inputFormat === 'JSON' ? 'json' : 'javascript'}
placeholder="Paste JSON or PHP serialized data here..."
height="200px"
maxLines={12}
showToggle={true}
className="w-full"
theme={isDark ? 'dark' : 'light'}
/>
{error && (
<p className="text-sm text-red-600 dark:text-red-400">

View File

@@ -299,7 +299,7 @@ const ReleaseNotes = () => {
const typeConfig = getTypeConfig(release.type);
return (
<div key={release.hash} className={`p-6 ${index !== dayReleases.length - 1 ? 'border-b border-gray-100 dark:border-gray-700' : ''}`}>
<div key={release.id} className={`p-6 ${index !== dayReleases.length - 1 ? 'border-b border-gray-100 dark:border-gray-700' : ''}`}>
<div className="flex flex-col md:flex-row items-start md:space-x-4">
<div className={`flex-shrink-0 p-2 rounded-lg ${typeConfig.bgColor} flex items-center space-x-2 mb-2`}>
<div className={typeConfig.color}>

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Plus, Upload, FileText, Globe, Download, X, Table, Trash2, Database, Braces, Code, Eye, Minimize2, Maximize2, Search, ArrowUpDown, AlertTriangle, Edit3 } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CodeEditor from '../components/CodeEditor';
import CodeMirrorEditor from '../components/CodeMirrorEditor';
import StructuredEditor from "../components/StructuredEditor";
import Papa from "papaparse";
@@ -2006,13 +2007,14 @@ const TableEditor = () => {
{activeTab === "paste" && (
<div className="space-y-3">
<CodeEditor
<CodeMirrorEditor
value={inputText}
onChange={(value) => setInputText(value)}
language="javascript"
onChange={setInputText}
language="json"
placeholder="Paste CSV, TSV, JSON, or SQL INSERT statements here..."
height="128px"
theme={isDark ? 'dark' : 'light'}
maxLines={12}
showToggle={true}
className="w-full"
/>
<div className="flex items-center justify-between">
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">