diff --git a/package-lock.json b/package-lock.json index b4dfbd9d..45ffafb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index aff8dfd5..922df5ea 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/data/commits.json b/public/data/commits.json index 8dcd636c..e4d447e5 100644 --- a/public/data/commits.json +++ b/public/data/commits.json @@ -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", diff --git a/src/components/CodeMirrorEditor.js b/src/components/CodeMirrorEditor.js new file mode 100644 index 00000000..e15e3749 --- /dev/null +++ b/src/components/CodeMirrorEditor.js @@ -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 ( +
+
+ {showToggle && ( + + )} +
+ ); +}; + +export default CodeMirrorEditor; diff --git a/src/components/MindmapView.js b/src/components/MindmapView.js index 21f026f2..af1a736e 100644 --- a/src/components/MindmapView.js +++ b/src/components/MindmapView.js @@ -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 }) => { )}
- {/* Actions - Third flex item */} -
- {data.value && ( - - )} - {/* Future action buttons can be added here */} -
{/* Output handle (right side) */} diff --git a/src/components/PostmanTreeTable.js b/src/components/PostmanTreeTable.js index 40c7ca8e..37dc8770 100644 --- a/src/components/PostmanTreeTable.js +++ b/src/components/PostmanTreeTable.js @@ -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; }; diff --git a/src/components/invoice-templates/MinimalTemplate.js b/src/components/invoice-templates/MinimalTemplate.js index 47dba0f5..e4580851 100644 --- a/src/components/invoice-templates/MinimalTemplate.js +++ b/src/components/invoice-templates/MinimalTemplate.js @@ -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 */}
{ {/* FROM Section */} {(invoiceData.settings?.showFromSection ?? true) && ( -
{ )} {/* TO Section */} -
{
{invoiceData.client.name || 'Acme Corporation'}
-
{invoiceData.client.address || '456 Business Ave'}
-
{invoiceData.client.city || 'New York, NY 10001'}
+ {invoiceData.client.address &&
{invoiceData.client.address}
} + {invoiceData.client.city &&
{invoiceData.client.city}
} {invoiceData.client.phone &&
{invoiceData.client.phone}
} {invoiceData.client.email &&
{invoiceData.client.email}
}
@@ -312,7 +314,7 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => { className={pageBreaks.beforeItemsTable ? 'page-break-before' : ''} style={{ marginBottom: '20px', marginTop: spacingMap[sectionSpacing] }} > - +
{/* Table Header */} { backgroundColor: `${accentColor}15` // 15 = ~8% opacity }}> @@ -360,38 +366,42 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => { borderBottom: '1px solid #E5E5E5' }}> @@ -439,7 +449,11 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => { {invoiceData.paymentMethod.bankDetails?.iban && (
IBAN: {invoiceData.paymentMethod.bankDetails.iban}
)} - {invoiceData.dueDate &&
Pay by: {invoiceData.dueDate}
} + {invoiceData.dueDate && ( +
+ Pay by: {invoiceData.dueDate} +
+ )} )} @@ -484,17 +498,73 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
{invoiceData.paymentMethod.qrCode.label || 'Scan to Pay'}
- {invoiceData.dueDate &&
Pay by: {invoiceData.dueDate}
} + {invoiceData.dueDate && ( +
+ Pay by: {invoiceData.dueDate} + {invoiceData.settings?.paymentStatus === 'PAID' && invoiceData.settings?.paymentDate && ( +
+ Paid at: {invoiceData.settings.paymentDate} +
+ )} +
+ )} + + )} + + {/* Payment Status Stamp */} + {invoiceData.settings?.paymentStatus && ( +
+
+ {invoiceData.settings.paymentStatus} +
+ + {/* Payment Date for PAID status */} + {invoiceData.settings?.paymentStatus === 'PAID' && invoiceData.settings?.paymentDate && ( +
+ Paid at: {invoiceData.settings.paymentDate} +
+ )}
)} )} {/* Totals */} -
-
- Subtotal - +
+
+ Subtotal + {formatCurrency(invoiceData.subtotal, true)}
@@ -502,11 +572,18 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => { {/* Dynamic Fees */} {invoiceData.fees && invoiceData.fees.map((fee) => ( -
- +
+ {fee.label || 'Fee'} {fee.type === 'percentage' ? `(${fee.value}%)` : ''} - + +{formatCurrency(fee.type === 'percentage' ? (invoiceData.subtotal * fee.value) / 100 : fee.value, true)}
@@ -514,40 +591,38 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => { {/* Dynamic Discounts */} {invoiceData.discounts && invoiceData.discounts.map((discount) => ( -
- +
+ {discount.label || 'Discount'} {discount.type === 'percentage' ? `(${discount.value}%)` : ''} - + -{formatCurrency(discount.type === 'percentage' ? (invoiceData.subtotal * discount.value) / 100 : discount.value, true)}
))} - {/* Legacy Discount */} - {invoiceData.discount > 0 && ( -
- Discount - - -{formatCurrency(invoiceData.discount, true)} - -
- )} - -
- Total + Total {formatCurrency(invoiceData.total, true)} diff --git a/src/pages/InvoiceEditor.js b/src/pages/InvoiceEditor.js index 3a304965..f71073b4 100644 --- a/src/pages/InvoiceEditor.js +++ b/src/pages/InvoiceEditor.js @@ -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) && (
{activeTab === 'create' && ( -
-
- -

- Start Building Your Invoice -

-

- Choose how you'd like to begin creating your professional invoice -

+
+ +

+ Start Building Your Invoice +

+

+ Choose how you'd like to begin creating your professional invoice +

+ +
+ -
- - - -
+
)} @@ -864,11 +882,14 @@ const InvoiceEditor = () => { -
Item Quantity Unit Price Total
{item.description} {item.quantity} {formatCurrency(item.rate, true)} {formatCurrency(item.amount, true)}