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:
133
package-lock.json
generated
133
package-lock.json
generated
@@ -8,14 +8,16 @@
|
|||||||
"name": "developer-tools-mvp",
|
"name": "developer-tools-mvp",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/basic-setup": "^0.20.0",
|
||||||
"@codemirror/commands": "^6.8.1",
|
"@codemirror/commands": "^6.8.1",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
"@codemirror/lang-html": "^6.4.9",
|
"@codemirror/lang-html": "^6.4.9",
|
||||||
"@codemirror/lang-javascript": "^6.2.4",
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
"@codemirror/lang-json": "^6.0.2",
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
"@codemirror/search": "^6.5.11",
|
"@codemirror/search": "^6.5.11",
|
||||||
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@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/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
@@ -1994,6 +1996,123 @@
|
|||||||
"@lezer/common": "^1.0.0"
|
"@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": {
|
"node_modules/@codemirror/commands": {
|
||||||
"version": "6.8.1",
|
"version": "6.8.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -2045,6 +2164,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@codemirror/lang-json": {
|
"node_modules/@codemirror/lang-json": {
|
||||||
"version": "6.0.2",
|
"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",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/language": "^6.0.0",
|
"@codemirror/language": "^6.0.0",
|
||||||
@@ -2083,6 +2204,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@codemirror/state": {
|
"node_modules/@codemirror/state": {
|
||||||
"version": "6.5.2",
|
"version": "6.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||||
|
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@marijn/find-cluster-break": "^1.0.0"
|
"@marijn/find-cluster-break": "^1.0.0"
|
||||||
@@ -2090,6 +2213,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@codemirror/theme-one-dark": {
|
"node_modules/@codemirror/theme-one-dark": {
|
||||||
"version": "6.1.3",
|
"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",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/language": "^6.0.0",
|
"@codemirror/language": "^6.0.0",
|
||||||
@@ -2099,7 +2224,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@codemirror/view": {
|
"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",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.5.0",
|
"@codemirror/state": "^6.5.0",
|
||||||
@@ -6115,6 +6242,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/codemirror": {
|
"node_modules/codemirror": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.0.0",
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
|||||||
@@ -4,14 +4,16 @@
|
|||||||
"description": "Web Developer Tools MVP - Utilities Toolkit",
|
"description": "Web Developer Tools MVP - Utilities Toolkit",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/basic-setup": "^0.20.0",
|
||||||
"@codemirror/commands": "^6.8.1",
|
"@codemirror/commands": "^6.8.1",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
"@codemirror/lang-html": "^6.4.9",
|
"@codemirror/lang-html": "^6.4.9",
|
||||||
"@codemirror/lang-javascript": "^6.2.4",
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
"@codemirror/lang-json": "^6.0.2",
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
"@codemirror/search": "^6.5.11",
|
"@codemirror/search": "^6.5.11",
|
||||||
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@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/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
|||||||
@@ -3,6 +3,30 @@
|
|||||||
{
|
{
|
||||||
"date": "2025-09-28",
|
"date": "2025-09-28",
|
||||||
"changes": [
|
"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",
|
"datetime": "2025-09-28T17:11:19+07:00",
|
||||||
"type": "enhancement",
|
"type": "enhancement",
|
||||||
|
|||||||
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,
|
ToggleLeft,
|
||||||
FileText,
|
FileText,
|
||||||
Zap,
|
Zap,
|
||||||
Copy,
|
|
||||||
Check,
|
|
||||||
Eye,
|
Eye,
|
||||||
Code,
|
Code,
|
||||||
Maximize,
|
Maximize,
|
||||||
@@ -35,27 +33,10 @@ import {
|
|||||||
// Custom node component for different data types
|
// Custom node component for different data types
|
||||||
const CustomNode = ({ data, selected }) => {
|
const CustomNode = ({ data, selected }) => {
|
||||||
const [renderHtml, setRenderHtml] = React.useState(true);
|
const [renderHtml, setRenderHtml] = React.useState(true);
|
||||||
const [isCopied, setIsCopied] = React.useState(false);
|
|
||||||
|
|
||||||
// Check if value contains HTML
|
// Check if value contains HTML
|
||||||
const isHtmlContent = data.value && typeof data.value === 'string' &&
|
const isHtmlContent = data.value && typeof data.value === 'string' &&
|
||||||
(data.value.includes('<') && data.value.includes('>'));
|
(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 = () => {
|
const getIcon = () => {
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'object':
|
case 'object':
|
||||||
@@ -169,27 +150,6 @@ const CustomNode = ({ data, selected }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Output handle (right side) */}
|
{/* 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 { ChevronRight, ChevronDown, Copy, Search, Filter } from 'lucide-react';
|
||||||
import CopyButton from './CopyButton';
|
import CopyButton from './CopyButton';
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
|
|||||||
const [filterType, setFilterType] = useState('all');
|
const [filterType, setFilterType] = useState('all');
|
||||||
|
|
||||||
// Flatten the data structure for table display
|
// Flatten the data structure for table display
|
||||||
const flattenData = (obj, path = '', level = 0) => {
|
const flattenData = useCallback((obj, path = '', level = 0) => {
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
if (obj === null || obj === undefined) {
|
if (obj === null || obj === undefined) {
|
||||||
@@ -74,7 +74,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
}, [expandedPaths]);
|
||||||
|
|
||||||
const toggleExpanded = (path) => {
|
const toggleExpanded = (path) => {
|
||||||
const newExpanded = new Set(expandedPaths);
|
const newExpanded = new Set(expandedPaths);
|
||||||
@@ -86,7 +86,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
|
|||||||
setExpandedPaths(newExpanded);
|
setExpandedPaths(newExpanded);
|
||||||
};
|
};
|
||||||
|
|
||||||
const flatData = useMemo(() => flattenData(data), [data, expandedPaths]);
|
const flatData = useMemo(() => flattenData(data), [data, flattenData]);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
return flatData.filter(item => {
|
return flatData.filter(item => {
|
||||||
@@ -129,7 +129,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getKeyDisplay = (key, level) => {
|
const getKeyDisplay = (key, level) => {
|
||||||
const parts = key.split(/[.\[\]]+/).filter(Boolean);
|
const parts = key.split(/[.[\]]+/).filter(Boolean);
|
||||||
return parts[parts.length - 1] || key;
|
return parts[parts.length - 1] || key;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
|
||||||
const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
||||||
// Get the chosen color scheme or default to golden
|
// Get the chosen color scheme or default to golden
|
||||||
const accentColor = invoiceData.settings?.colorScheme || '#D4AF37';
|
const accentColor = invoiceData.settings?.colorScheme || '#D4AF37';
|
||||||
@@ -21,7 +22,8 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
|||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
lineHeight: '1.4',
|
lineHeight: '1.4',
|
||||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||||
color: '#000000'
|
color: '#000000',
|
||||||
|
position: 'relative'
|
||||||
}}>
|
}}>
|
||||||
{/* Header Section - 2 Column Layout */}
|
{/* Header Section - 2 Column Layout */}
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -118,7 +120,7 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
|||||||
|
|
||||||
{/* FROM Section */}
|
{/* FROM Section */}
|
||||||
{(invoiceData.settings?.showFromSection ?? true) && (
|
{(invoiceData.settings?.showFromSection ?? true) && (
|
||||||
<div style={{
|
<div className={'invoice-from-to-card'} style={{
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
background: `${accentColor}08`,
|
background: `${accentColor}08`,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
@@ -147,7 +149,7 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* TO Section */}
|
{/* TO Section */}
|
||||||
<div style={{
|
<div className={'invoice-from-to-card'} style={{
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
background: `${accentColor}08`,
|
background: `${accentColor}08`,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
@@ -167,8 +169,8 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
|||||||
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
|
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
|
||||||
{invoiceData.client.name || 'Acme Corporation'}
|
{invoiceData.client.name || 'Acme Corporation'}
|
||||||
</div>
|
</div>
|
||||||
<div>{invoiceData.client.address || '456 Business Ave'}</div>
|
{invoiceData.client.address && <div>{invoiceData.client.address}</div>}
|
||||||
<div>{invoiceData.client.city || 'New York, NY 10001'}</div>
|
{invoiceData.client.city && <div>{invoiceData.client.city}</div>}
|
||||||
{invoiceData.client.phone && <div>{invoiceData.client.phone}</div>}
|
{invoiceData.client.phone && <div>{invoiceData.client.phone}</div>}
|
||||||
{invoiceData.client.email && <div>{invoiceData.client.email}</div>}
|
{invoiceData.client.email && <div>{invoiceData.client.email}</div>}
|
||||||
</div>
|
</div>
|
||||||
@@ -312,7 +314,7 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
|||||||
className={pageBreaks.beforeItemsTable ? 'page-break-before' : ''}
|
className={pageBreaks.beforeItemsTable ? 'page-break-before' : ''}
|
||||||
style={{ marginBottom: '20px', marginTop: spacingMap[sectionSpacing] }}
|
style={{ marginBottom: '20px', marginTop: spacingMap[sectionSpacing] }}
|
||||||
>
|
>
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
<table className="invoice-table" style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
{/* Table Header */}
|
{/* Table Header */}
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{
|
<tr style={{
|
||||||
@@ -320,36 +322,40 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
|||||||
backgroundColor: `${accentColor}15` // 15 = ~8% opacity
|
backgroundColor: `${accentColor}15` // 15 = ~8% opacity
|
||||||
}}>
|
}}>
|
||||||
<th style={{
|
<th style={{
|
||||||
padding: '12px 0 12px 16px',
|
padding: '12px 0px 12px 16px',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: accentColor,
|
color: accentColor,
|
||||||
verticalAlign: 'middle'
|
verticalAlign: 'middle',
|
||||||
|
height: '48px'
|
||||||
}}>Item</th>
|
}}>Item</th>
|
||||||
<th style={{
|
<th style={{
|
||||||
padding: '12px 0',
|
padding: '12px 0px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: accentColor,
|
color: accentColor,
|
||||||
verticalAlign: 'middle'
|
verticalAlign: 'middle',
|
||||||
|
height: '48px'
|
||||||
}}>Quantity</th>
|
}}>Quantity</th>
|
||||||
<th style={{
|
<th style={{
|
||||||
padding: '12px 0',
|
padding: '12px 0px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: accentColor,
|
color: accentColor,
|
||||||
verticalAlign: 'middle'
|
verticalAlign: 'middle',
|
||||||
|
height: '48px'
|
||||||
}}>Unit Price</th>
|
}}>Unit Price</th>
|
||||||
<th style={{
|
<th style={{
|
||||||
padding: '12px 16px 12px 0',
|
padding: '12px 16px 12px 0px',
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: accentColor,
|
color: accentColor,
|
||||||
verticalAlign: 'middle'
|
verticalAlign: 'middle',
|
||||||
|
height: '48px'
|
||||||
}}>Total</th>
|
}}>Total</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -360,38 +366,42 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
|||||||
borderBottom: '1px solid #E5E5E5'
|
borderBottom: '1px solid #E5E5E5'
|
||||||
}}>
|
}}>
|
||||||
<td style={{
|
<td style={{
|
||||||
padding: '16px 0 16px 16px',
|
padding: '16px 0px 16px 16px',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
verticalAlign: 'middle'
|
verticalAlign: 'middle',
|
||||||
|
height: '52px'
|
||||||
}}>
|
}}>
|
||||||
{item.description}
|
{item.description}
|
||||||
</td>
|
</td>
|
||||||
<td style={{
|
<td style={{
|
||||||
padding: '16px 0',
|
padding: '16px 0px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
verticalAlign: 'middle'
|
verticalAlign: 'middle',
|
||||||
|
height: '52px'
|
||||||
}}>
|
}}>
|
||||||
{item.quantity}
|
{item.quantity}
|
||||||
</td>
|
</td>
|
||||||
<td style={{
|
<td style={{
|
||||||
padding: '16px 0',
|
padding: '16px 0px',
|
||||||
textAlign: 'center',
|
textAlign: 'right',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
verticalAlign: 'middle'
|
verticalAlign: 'middle',
|
||||||
|
height: '52px'
|
||||||
}}>
|
}}>
|
||||||
{formatCurrency(item.rate, true)}
|
{formatCurrency(item.rate, true)}
|
||||||
</td>
|
</td>
|
||||||
<td style={{
|
<td style={{
|
||||||
padding: '16px 16px 16px 0',
|
padding: '16px 16px 16px 0px',
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
verticalAlign: 'middle'
|
verticalAlign: 'middle',
|
||||||
|
height: '52px'
|
||||||
}}>
|
}}>
|
||||||
{formatCurrency(item.amount, true)}
|
{formatCurrency(item.amount, true)}
|
||||||
</td>
|
</td>
|
||||||
@@ -439,7 +449,11 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
|||||||
{invoiceData.paymentMethod.bankDetails?.iban && (
|
{invoiceData.paymentMethod.bankDetails?.iban && (
|
||||||
<div style={{ marginBottom: '4px' }}>IBAN: {invoiceData.paymentMethod.bankDetails.iban}</div>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -484,17 +498,73 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
|||||||
<div style={{ fontSize: '12px', color: '#666666', marginBottom: '4px' }}>
|
<div style={{ fontSize: '12px', color: '#666666', marginBottom: '4px' }}>
|
||||||
{invoiceData.paymentMethod.qrCode.label || 'Scan to Pay'}
|
{invoiceData.paymentMethod.qrCode.label || 'Scan to Pay'}
|
||||||
</div>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Totals */}
|
{/* Totals */}
|
||||||
<div style={{ textAlign: 'right', minWidth: '200px' }}>
|
<div style={{ textAlign: 'right', minWidth: '300px', width: '100%', maxWidth: '400px' }}>
|
||||||
<div style={{ marginBottom: '12px' }}>
|
<div className="invoice-totals-row" style={{
|
||||||
<span style={{ fontSize: '14px', color: '#000000' }}>Subtotal</span>
|
display: 'flex',
|
||||||
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
|
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)}
|
{formatCurrency(invoiceData.subtotal, true)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -502,11 +572,18 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
|||||||
|
|
||||||
{/* Dynamic Fees */}
|
{/* Dynamic Fees */}
|
||||||
{invoiceData.fees && invoiceData.fees.map((fee) => (
|
{invoiceData.fees && invoiceData.fees.map((fee) => (
|
||||||
<div key={fee.id} style={{ marginBottom: '12px' }}>
|
<div key={fee.id} className="invoice-totals-row" style={{
|
||||||
<span style={{ fontSize: '14px', color: '#000000' }}>
|
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}%)` : ''}
|
{fee.label || 'Fee'} {fee.type === 'percentage' ? `(${fee.value}%)` : ''}
|
||||||
</span>
|
</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)}
|
+{formatCurrency(fee.type === 'percentage' ? (invoiceData.subtotal * fee.value) / 100 : fee.value, true)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -514,40 +591,38 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
|||||||
|
|
||||||
{/* Dynamic Discounts */}
|
{/* Dynamic Discounts */}
|
||||||
{invoiceData.discounts && invoiceData.discounts.map((discount) => (
|
{invoiceData.discounts && invoiceData.discounts.map((discount) => (
|
||||||
<div key={discount.id} style={{ marginBottom: '12px' }}>
|
<div key={discount.id} className="invoice-totals-row" style={{
|
||||||
<span style={{ fontSize: '14px', color: '#000000' }}>
|
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}%)` : ''}
|
{discount.label || 'Discount'} {discount.type === 'percentage' ? `(${discount.value}%)` : ''}
|
||||||
</span>
|
</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)}
|
-{formatCurrency(discount.type === 'percentage' ? (invoiceData.subtotal * discount.value) / 100 : discount.value, true)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Legacy Discount */}
|
<div className="invoice-total-final" style={{
|
||||||
{invoiceData.discount > 0 && (
|
padding: '16px',
|
||||||
<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',
|
|
||||||
backgroundColor: `${accentColor}10`, // 10 = ~6% opacity
|
backgroundColor: `${accentColor}10`, // 10 = ~6% opacity
|
||||||
borderTop: `2px solid ${accentColor}`,
|
borderTop: `2px solid ${accentColor}`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
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={{
|
<span style={{
|
||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: accentColor
|
color: accentColor,
|
||||||
|
lineHeight: '1.4'
|
||||||
}}>
|
}}>
|
||||||
{formatCurrency(invoiceData.total, true)}
|
{formatCurrency(invoiceData.total, true)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Globe, AlertTriangle, ChevronUp, ChevronDown
|
Globe, AlertTriangle, ChevronUp, ChevronDown
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import ToolLayout from '../components/ToolLayout';
|
import ToolLayout from '../components/ToolLayout';
|
||||||
|
import CodeMirrorEditor from '../components/CodeMirrorEditor';
|
||||||
|
|
||||||
const InvoiceEditor = () => {
|
const InvoiceEditor = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -276,12 +277,13 @@ const InvoiceEditor = () => {
|
|||||||
// Utility functions for formatting
|
// Utility functions for formatting
|
||||||
const formatNumber = (num, useThousandSeparator = invoiceData.settings?.thousandSeparator) => {
|
const formatNumber = (num, useThousandSeparator = invoiceData.settings?.thousandSeparator) => {
|
||||||
if (!useThousandSeparator) return num.toString();
|
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 formatCurrency = (amount, showSymbol = true) => {
|
||||||
const currency = invoiceData.settings?.currency || { code: 'USD', symbol: '$' };
|
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) {
|
if (showSymbol && currency.symbol) {
|
||||||
return `${currency.symbol} ${formattedAmount}`;
|
return `${currency.symbol} ${formattedAmount}`;
|
||||||
@@ -777,39 +779,55 @@ const InvoiceEditor = () => {
|
|||||||
{(activeTab !== 'create' || !createNewCompleted) && (
|
{(activeTab !== 'create' || !createNewCompleted) && (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{activeTab === 'create' && (
|
{activeTab === 'create' && (
|
||||||
<div className="space-y-4">
|
<div className="text-center py-12">
|
||||||
<div className="text-center py-8">
|
<FileText className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500 mb-4" />
|
||||||
<FileText className="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
Start Building Your Invoice
|
||||||
Start Building Your Invoice
|
</h3>
|
||||||
</h3>
|
<p className="text-gray-500 dark:text-gray-400 mb-8 max-w-md mx-auto">
|
||||||
<p className="text-gray-500 dark:text-gray-400 mb-6">
|
Choose how you'd like to begin creating your professional invoice
|
||||||
Choose how you'd like to begin creating your professional invoice
|
</p>
|
||||||
</p>
|
|
||||||
|
<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>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 max-w-2xl mx-auto">
|
<button
|
||||||
<button
|
onClick={() => {
|
||||||
onClick={handleStartEmpty}
|
if (hasModifiedData()) {
|
||||||
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"
|
setPendingTabChange('create_sample');
|
||||||
>
|
setShowInputChangeModal(true);
|
||||||
<div className="flex flex-col items-center">
|
} else {
|
||||||
<Plus className="h-8 w-8 text-blue-500 mb-3 group-hover:scale-110 transition-transform" />
|
handleLoadSample();
|
||||||
<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>
|
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"
|
||||||
</button>
|
>
|
||||||
|
<FileText className="h-8 w-8 text-gray-400 group-hover:text-green-500 dark:group-hover:text-green-400 mb-2" />
|
||||||
<button
|
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-green-600 dark:group-hover:text-green-400">
|
||||||
onClick={handleLoadSample}
|
Load Sample
|
||||||
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"
|
</span>
|
||||||
>
|
<span className="text-xs text-gray-500 dark:text-gray-400 text-center mt-1">
|
||||||
<div className="flex flex-col items-center">
|
Start with example invoice
|
||||||
<FileText className="h-8 w-8 text-green-500 mb-3 group-hover:scale-110 transition-transform" />
|
</span>
|
||||||
<span className="text-sm font-medium text-green-600 dark:text-green-400">Load Sample</span>
|
</button>
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400 mt-1">Start with example invoice</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -864,11 +882,14 @@ const InvoiceEditor = () => {
|
|||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Paste Invoice JSON Data
|
Paste Invoice JSON Data
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<CodeMirrorEditor
|
||||||
value={inputText}
|
value={inputText}
|
||||||
onChange={(e) => setInputText(e.target.value)}
|
onChange={setInputText}
|
||||||
placeholder="Paste your invoice JSON data here..."
|
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>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -1194,7 +1215,7 @@ const InvoiceEditor = () => {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-3 text-center" style={{ width: 'auto' }}>
|
<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>
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -1203,7 +1224,7 @@ const InvoiceEditor = () => {
|
|||||||
const numValue = parseFloat(e.target.value.replace(/,/g, '')) || 0;
|
const numValue = parseFloat(e.target.value.replace(/,/g, '')) || 0;
|
||||||
updateLineItem(item.id, 'rate', numValue);
|
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` }}
|
style={{ width: `${Math.max(formatNumber(item.rate).length * 8 + 20, 40)}px` }}
|
||||||
onFocus={(e) => {
|
onFocus={(e) => {
|
||||||
// Show raw number on focus
|
// 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'
|
: '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' && (
|
{activeTab === 'payment' && (
|
||||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500 dark:bg-blue-400"></div>
|
<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>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -2601,6 +2643,46 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -101,22 +101,130 @@ const InvoicePreview = () => {
|
|||||||
|
|
||||||
// Format number with thousand separator
|
// Format number with thousand separator
|
||||||
const formatNumber = (num) => {
|
const formatNumber = (num) => {
|
||||||
if (!invoiceData?.settings?.thousandSeparator) return num.toString();
|
const decimalDigits = invoiceData?.settings?.decimalDigits ?? 2;
|
||||||
return num.toLocaleString('en-US', { minimumFractionDigits: 2 });
|
if (!invoiceData?.settings?.thousandSeparator) return num.toFixed(decimalDigits);
|
||||||
|
return num.toLocaleString('en-US', { minimumFractionDigits: decimalDigits, maximumFractionDigits: decimalDigits });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format currency
|
// Format currency
|
||||||
const formatCurrency = (amount, useThousandSeparator = false) => {
|
const formatCurrency = (amount, useThousandSeparator = false) => {
|
||||||
const symbol = invoiceData?.settings?.currency?.symbol || '$';
|
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}`;
|
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
|
// Generate PDF from the visible invoice
|
||||||
const handleDownloadPDF = async () => {
|
const handleDownloadPDF = async () => {
|
||||||
if (!invoiceData) return;
|
if (!invoiceData) return;
|
||||||
|
|
||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
|
let originalStyles = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const element = document.getElementById('invoice-content');
|
const element = document.getElementById('invoice-content');
|
||||||
@@ -124,8 +232,14 @@ const InvoicePreview = () => {
|
|||||||
throw new Error('Invoice content not found');
|
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 = {
|
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`,
|
filename: `invoice-${invoiceData.invoiceNumber || 'draft'}.pdf`,
|
||||||
image: { type: 'png', quality: 0.98 },
|
image: { type: 'png', quality: 0.98 },
|
||||||
html2canvas: {
|
html2canvas: {
|
||||||
@@ -151,6 +265,11 @@ const InvoicePreview = () => {
|
|||||||
console.error('PDF generation failed:', error);
|
console.error('PDF generation failed:', error);
|
||||||
alert('Failed to generate PDF. Please try again.');
|
alert('Failed to generate PDF. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
|
// Restore original styles after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
restoreOriginalStyles(originalStyles);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,22 +36,98 @@ const InvoicePreviewMinimal = () => {
|
|||||||
|
|
||||||
// Format number with thousand separator
|
// Format number with thousand separator
|
||||||
const formatNumber = (num) => {
|
const formatNumber = (num) => {
|
||||||
if (!invoiceData?.settings?.thousandSeparator) return num.toString();
|
const decimalDigits = invoiceData?.settings?.decimalDigits ?? 2;
|
||||||
return num.toLocaleString();
|
if (!invoiceData?.settings?.thousandSeparator) return num.toFixed(decimalDigits);
|
||||||
|
return num.toLocaleString('en-US', { minimumFractionDigits: decimalDigits, maximumFractionDigits: decimalDigits });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format currency
|
// Format currency
|
||||||
const formatCurrency = (amount, useThousandSeparator = false) => {
|
const formatCurrency = (amount, useThousandSeparator = false) => {
|
||||||
const symbol = invoiceData?.settings?.currency?.symbol || '$';
|
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}`;
|
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
|
// Generate PDF from the visible invoice
|
||||||
const handleDownloadPDF = async () => {
|
const handleDownloadPDF = async () => {
|
||||||
if (!invoiceData) return;
|
if (!invoiceData) return;
|
||||||
|
|
||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
|
let originalStyles = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const element = document.getElementById('minimal-invoice-content');
|
const element = document.getElementById('minimal-invoice-content');
|
||||||
@@ -60,8 +136,14 @@ const InvoicePreviewMinimal = () => {
|
|||||||
throw new Error('Invoice content not found');
|
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 = {
|
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`,
|
filename: `Invoice-${invoiceData.invoiceNumber || new Date().toISOString().split('T')[0]}.pdf`,
|
||||||
image: { type: 'jpeg', quality: 0.98 },
|
image: { type: 'jpeg', quality: 0.98 },
|
||||||
html2canvas: {
|
html2canvas: {
|
||||||
@@ -85,6 +167,11 @@ const InvoicePreviewMinimal = () => {
|
|||||||
console.error('PDF generation failed:', error);
|
console.error('PDF generation failed:', error);
|
||||||
alert('Failed to generate PDF. Please try again.');
|
alert('Failed to generate PDF. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
|
// Restore original styles after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
restoreOriginalStyles(originalStyles);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import StructuredEditor from '../components/StructuredEditor';
|
|||||||
import MindmapView from '../components/MindmapView';
|
import MindmapView from '../components/MindmapView';
|
||||||
import PostmanTable from '../components/PostmanTable';
|
import PostmanTable from '../components/PostmanTable';
|
||||||
import CodeEditor from '../components/CodeEditor';
|
import CodeEditor from '../components/CodeEditor';
|
||||||
|
import CodeMirrorEditor from '../components/CodeMirrorEditor';
|
||||||
|
|
||||||
// Hook to detect dark mode
|
// Hook to detect dark mode
|
||||||
const useDarkMode = () => {
|
const useDarkMode = () => {
|
||||||
@@ -785,14 +786,14 @@ const ObjectEditor = () => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CodeEditor
|
<CodeMirrorEditor
|
||||||
value={inputText}
|
value={inputText}
|
||||||
onChange={(value) => handleInputChange(value)}
|
onChange={(value) => handleInputChange(value)}
|
||||||
language={inputFormat === 'JSON' ? 'json' : 'javascript'}
|
language={inputFormat === 'JSON' ? 'json' : 'javascript'}
|
||||||
placeholder="Paste JSON or PHP serialized data here..."
|
placeholder="Paste JSON or PHP serialized data here..."
|
||||||
height="200px"
|
maxLines={12}
|
||||||
|
showToggle={true}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
theme={isDark ? 'dark' : 'light'}
|
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-sm text-red-600 dark:text-red-400">
|
<p className="text-sm text-red-600 dark:text-red-400">
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ const ReleaseNotes = () => {
|
|||||||
const typeConfig = getTypeConfig(release.type);
|
const typeConfig = getTypeConfig(release.type);
|
||||||
|
|
||||||
return (
|
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 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={`flex-shrink-0 p-2 rounded-lg ${typeConfig.bgColor} flex items-center space-x-2 mb-2`}>
|
||||||
<div className={typeConfig.color}>
|
<div className={typeConfig.color}>
|
||||||
|
|||||||
@@ -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 { 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 ToolLayout from '../components/ToolLayout';
|
||||||
import CodeEditor from '../components/CodeEditor';
|
import CodeEditor from '../components/CodeEditor';
|
||||||
|
import CodeMirrorEditor from '../components/CodeMirrorEditor';
|
||||||
import StructuredEditor from "../components/StructuredEditor";
|
import StructuredEditor from "../components/StructuredEditor";
|
||||||
import Papa from "papaparse";
|
import Papa from "papaparse";
|
||||||
|
|
||||||
@@ -2006,13 +2007,14 @@ const TableEditor = () => {
|
|||||||
|
|
||||||
{activeTab === "paste" && (
|
{activeTab === "paste" && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<CodeEditor
|
<CodeMirrorEditor
|
||||||
value={inputText}
|
value={inputText}
|
||||||
onChange={(value) => setInputText(value)}
|
onChange={setInputText}
|
||||||
language="javascript"
|
language="json"
|
||||||
placeholder="Paste CSV, TSV, JSON, or SQL INSERT statements here..."
|
placeholder="Paste CSV, TSV, JSON, or SQL INSERT statements here..."
|
||||||
height="128px"
|
maxLines={12}
|
||||||
theme={isDark ? 'dark' : 'light'}
|
showToggle={true}
|
||||||
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
|||||||
Reference in New Issue
Block a user