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
}}>
| Item |
Quantity |
Unit Price |
Total |
@@ -360,38 +366,42 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
borderBottom: '1px solid #E5E5E5'
}}>
{item.description}
|
{item.quantity}
|
{formatCurrency(item.rate, true)}
|
{formatCurrency(item.amount, true)}
|
@@ -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 = () => {
-