The module marketplace browser UI (ModuleIndexView, ModuleDetailView,
ModuleCard, the four-step installer store) was filed under
features/company/modules/ only by historical accident — it's authorized via
the manage modules ability (super-admin-only) and conceptually belongs in the
admin context, not the company context.
- Move features/company/modules/{store.ts, views/ModuleIndexView.vue,
views/ModuleDetailView.vue, components/ModuleCard.vue} to
features/admin/modules/.
- Update hardcoded /admin/modules/... paths in the moved files to
/admin/administration/modules/... so the breadcrumbs and ModuleCard
navigation target the new admin-context routes.
- Tighten the four-step installer's silent catch {} blocks in the moved
store.ts: errors were being swallowed, now they dispatch through the
global notification store instead.
- New features/admin/modules/routes.ts declares admin.modules.index +
admin.modules.view as children of /admin/administration with
meta.isSuperAdmin: true.
- features/admin/{index,routes}.ts re-export and mount the relocated routes.
- config/invoiceshelf.php gains a new AdminModules entry in admin_menu
pointing at /admin/administration/modules with super_admin_only: true.
- The dev-gated navigation.modules entry in main_menu is replaced (not
deleted) with a non-gated entry pointing at the new company-context
Active Modules index page that lands in the next commit. The
ability is set to manage modules so non-owners can't see it.
The new company-context Active Modules index, schema-driven settings page,
and dynamic sidebar group are introduced in subsequent commits.
The vestigial App\Services\Module\Module static class — with its unused
\$scripts / \$styles / \$settings registries — never had any of its helpers
wired up. The new InvoiceShelf\Modules\Registry shipped from the
invoiceshelf/modules package supersedes it cleanly: same static-array surface
(\$menu, \$settings, \$scripts, \$styles), but lives outside the host app so
third-party modules can depend on it without importing v3-app internals.
Three consumers in the host app are migrated to the new namespace:
- ScriptController and StyleController (the HTTP endpoints that serve
module-registered JS/CSS assets at /modules/scripts/{name} and
/modules/styles/{name}) now look up paths via Registry::scriptFor() and
Registry::styleFor() instead of Arr::get(ModuleFacade::all*(), \$name).
Also tightens type hints — Request import + Response return type.
- resources/views/app.blade.php iterates Registry::allStyles() /
Registry::allScripts() to inject module-supplied <link>/<script> tags into
the main layout. Same Akaunting-style asset injection mechanism, just
reading from the new namespace.
Both Module and ModuleFacade are deleted — they had no remaining callers
after this migration.
Pulls invoiceshelf/modules ^3.0 from packagist — a thin extension package on
top of current upstream nwidart/laravel-modules ^13.0 — replacing the stale
2021-era invoiceshelf/modules ^1.0 fork that bundled its own copy of nwidart.
The host app's autoloader now resolves Nwidart\Modules\* from
vendor/nwidart/laravel-modules and InvoiceShelf\Modules\* from
vendor/invoiceshelf/modules. Existing imports of Nwidart\Modules\Facades\Module
keep working unchanged.
config/modules.php is republished from upstream v13 with two
InvoiceShelf-specific overrides:
- activators.file.statuses-file kept at storage/app/modules_statuses.json so
existing installations don't lose track of which modules are enabled when
the config is republished (upstream v13 defaults to base_path()).
- New lang/menu and lang/settings entries in stubs.files / stubs.replacements
that pair with the custom module:make stubs shipped from the package.
Wires wikimedia/composer-merge-plugin (a transitive dependency of nwidart) so
each module's nested composer.json autoload mapping is merged into the host
autoloader at composer dump-autoload time. This is what makes a module
generated via php artisan module:make MyModule actually loadable. The plugin
is added to allow-plugins and configured via extra.merge-plugin.include.
Drops the stale Modules\\: Modules/ PSR-4 fallback root from autoload — it
didn't match nwidart's app/-prefixed module layout and was always broken for
generated modules.
Closes the residual surface from the three published SSRF advisories (GHSA-pc5v-8xwc-v9xq, GHSA-38hf-fq8x-q49r, GHSA-q9wx-ggwq-mcgh / CVE-2026-34365 to 34367) that the original 2.2.0 fix only covered for the Notes field. The same blade templates render company/billing/shipping address fields with {!! !!} via Invoice/Estimate/Payment::getCompanyAddress(), getCustomerBillingAddress(), getCustomerShippingAddress() — and those flow through GeneratesPdfTrait::getFormattedString() which did not call PdfHtmlSanitizer.
Customer-controlled fields (name, street, phone, custom-field values) are substituted into address templates via getFieldsArray() without HTML-escaping, so a malicious customer name like "Acme <img src='http://attacker/probe'>" reaches Dompdf as raw HTML through the address path. Today this is blocked only by the secondary defense of dompdf's enable_remote=false; if a self-hoster sets DOMPDF_ENABLE_REMOTE=true for legitimate remote logos, the address surface immediately re-opens.
Move PdfHtmlSanitizer::sanitize() into the chokepoint at GeneratesPdfTrait::getFormattedString() so all four sinks — notes plus the three address fields, on all three models — get the same treatment via a single call site. v3.0's models (Invoice, Estimate, Payment) already had the simpler getNotes() shape (no per-method PdfHtmlSanitizer wrapper), so the trait edit alone is sufficient — no model edits required on this branch. Verified getFormattedString() is only called from PDF code paths (no email body callers, which use strtr() directly).
This is the v3.0 counterpart to master's f387e751. Re-implemented directly on v3.0 instead of cherry-picked because the import-block divergence from the larger v3.0 refactor produced four merge conflicts that were noisier than just porting the chokepoint change manually.
Extends tests/Unit/PdfHtmlSanitizerTest.php with three new cases covering the address-template scenario, iframe/link tag stripping, and on* event handler removal. All 8 tests pass via vendor/bin/pest tests/Unit/PdfHtmlSanitizerTest.php.
Polishes the intro wording, removes the Mobile Apps section (the source repo is referenced from invoiceshelf.com instead) and the Credits section, and rebuilds the Roadmap as a single flat checklist that mixes shipped items with in-flight v3.0 work.
Shipped v3.0 items added: decoupled system settings from company settings, proper multi-tenancy system, company member invitations with custom roles, dark mode, full TypeScript refactor of the frontend, improved backend architecture, security hardening.
In-flight v3.0 items (bold + (v3.0) marker so they jump out as the active milestone): reworked installation wizard, Module Directory, rewritten Payments module. The two longer-term items (Stripe integration, improved template system) stay at the bottom unchanged.
Port of master's 241ec092 (Feat: Automatically set due date when invoice date is changed). The original commit was a JavaScript Options-API watcher in the deleted v1 InvoiceCreate.vue; v3.0's equivalent is a Composition-API TypeScript view at resources/scripts/features/company/invoices/views/InvoiceCreateView.vue, so this is a re-implementation rather than a cherry-pick.
Behaviour: if the company setting invoice_set_due_date_automatically is 'YES', a watcher on invoiceStore.newInvoice.invoice_date recomputes the due date as invoice_date + invoice_due_date_days whenever the invoice date changes. A second watcher on due_date tracks whether the user has manually edited it; if the manual value is still valid (>= the new invoice date) it is left alone, otherwise the auto value takes over. An isAutoUpdatingDueDate guard avoids a feedback loop when the watcher writes back to the store.
Uses moment for the date math, matching the original master commit and several other v3.0 features (reports, CreateCustomFields) that already import moment. companyStore is newly imported in this view to read selectedCompanySettings.
CompanyRequest::getCompanyPayload() accepted 'slug' from the client but never generated it, so the installation wizard (which PUTs /api/v1/company) left the slug empty when setting up the first company. Match the sibling CompaniesRequest (which already does Str::slug($this->name)) and generate the slug from the name server-side; drop the now-unused 'slug' validation rule.
Fixes the same bug that master's ed7af3fc tried to fix client-side with a lodash deburr + regex workaround in Step7CompanyInfo.vue. v3.0's installation wizard is a rewrite under resources/scripts/features/installation/CompanyView.vue and doesn't carry that workaround, so the cleaner fix is to make the backend authoritative like CompaniesRequest already is.
Ports the net behaviour from three master commits into v3.0 as a single change, because v3.0 has already diverged structurally (controller moved from V1/Admin/Report to Company/Report, blade has its own CSS rework using the bundled fonts partial, and v3.0's App\Facades\Pdf replaces Barryvdh\DomPDF\Facade\Pdf). The three source commits are: 834b53ea (grouped itemized expenses), e22050bc (DomPDF facade + Pint — adapted to v3.0's App\Facades\Pdf), 0e9f18d4 (expenses.uncategorized + pdf_expense_group_total_label i18n keys + View|Response return type).
Controller: replaces the expenseCategories aggregate fetch with an itemized Expense query ordered by date, groups by category name with expenses.uncategorized fallback, and shares an expenseGroups collection of {name, expenses, total} plus the overall totalExpense. Adds expense_category_id to applyFilters. Updates the docblock return type from JsonResponse to View|Response. Keeps v3.0's App\Facades\Pdf.
Blade: replaces the single expenseCategories aggregate table with a per-group itemized table (date / note / amount columns + per-group total line using the new pdf_expense_group_total_label i18n key). Adds the item-table-* CSS classes and removes the old expense-total-table bottom block.
lang/en.json: adds expenses.uncategorized = "Uncategorized" and pdf_expense_group_total_label = "Group total:".
Updates CLAUDE.md to reflect the actual current state of the codebase: rewrites the Frontend section to point at main.ts / features-folder layout / @ alias (the previous text still pointed at the deleted v1 main.js / admin-router.js / admin/stores paths), expands Backend Patterns with the FileDisk + disk-assignments model and the per-user 'default' language sentinel, and adds two new sections — PDF Font System (the on-demand Font Packages mechanism plus the two non-obvious dompdf constraints around variable fonts and font-family fallback) and CSS Theme Tokens (the @theme inline registration model, all currently-defined token categories, the [data-theme=dark] attribute switch, the two-step ritual for adding a new token, and the no-hardcoded-values convention).
Cleans up two scripts-v2 leftovers from the rename refactor: resources/css/invoiceshelf.css had four @source directives — two pointing at the long-deleted scripts-v2/ directory and one pointing at scripts/**/*.js (the new directory has zero .js files, only .vue and .ts), collapsed down to the two correct ones. CONTRIBUTING.md still pointed contributors at 'patterns in resources/scripts-v2/' — fixed to resources/scripts/.
Rewrites resources/scripts/layouts/AuthLayout.vue from scratch using only the @theme tokens defined in themes.css and registered via @theme inline in invoiceshelf.css. The new layout is a centered card on the existing bg-glass-gradient utility, using the same visual vocabulary as BaseCard (bg-surface, rounded-xl, border-line-default, shadow-sm) so the auth pages read as a smaller, simpler version of the admin's existing card pattern. Both light and dark mode work automatically because every color references a theme token rather than a hardcoded hex/rgb.
Drops the previous attempt's hardcoded #0a0e1a / #fbbf24 / #f5efe5 palette, the imported Google Fonts (Fraunces / Manrope / JetBrains Mono — replaced with the project default Poppins via font-base), the local --ink / --brass / --cream CSS variables that ignored [data-theme=dark], and the :deep() overrides that forced BaseInput / BaseButton into a custom underline style. The form components now render in the auth card identically to how they render anywhere else in the admin — same components, same theme tokens, no overrides.
Removes four legacy SVG decorations from the original two-panel design: LoginPlanetCrater, LoginBackground, LoginBackgroundOverlay, LoginBottomVector. The page now has no decorative imagery — the bg-glass-gradient utility carries the visual mood.
Adds w-full justify-center to the four auth-form submit buttons (LoginView, ForgotPasswordView, ResetPasswordView, RegisterWithInvitationView) so they fill the auth card width with their labels centered. Done at the call site rather than via :deep() so BaseButton stays untouched and the rest of the admin keeps its inline button style. Route-aware heading/subheading copy is preserved for all four auth views, and the four window.* admin customization hooks (login_page_logo, login_page_heading, login_page_description, copyright_text) still work.
Now that the legacy v1 frontend (commit 064bdf53) is gone, the v2 directory is the only frontend and the v2 suffix is just noise. Renames resources/scripts-v2 to resources/scripts via git mv (so git records the move as renames, preserving blame and log --follow), then bulk-rewrites the 152 files that imported via @v2/... to use @/scripts/... instead. The existing @ alias (resources/) covers the new path with no extra config needed.
Drops the now-unused @v2 alias from vite.config.js and points the laravel-vite-plugin entry at resources/scripts/main.ts. Updates the only blade reference (resources/views/app.blade.php) to match. The package.json test script (eslint ./resources/scripts) automatically targets the right place after the rename without any edit.
Verified: npm run build exits clean and the Vite warning lines now reference resources/scripts/plugins/i18n.ts, confirming every import resolved through the new path. git log --follow on any moved file walks back through its scripts-v2 history.
The resources/scripts/ directory was the original Vue 2 / Pinia v1 admin and customer-portal SPA. It has been fully orphaned for some time — vite.config.js has zero entry points pointing at it and the only blade @vite() reference in resources/views/app.blade.php loads scripts-v2/main.ts. The directory was pure dead code.
Removes 424 .vue / .js / store / router / helper files (~2.7 MB) so that resources/scripts-v2/ can be renamed back to resources/scripts/ in a follow-up commit, dropping the v2 suffix now that there is no v1 left.
Status labels in lang/mk.json shipped as neuter singular adjectives in commit 9345d3e5 (Платено, Активно, Повторливо, Прегледано, …) but in the actual UI these strings appear primarily as filter tab labels at the top of list views and as column headers — both contexts read more naturally in plural in Macedonian. Going plural also sidesteps the gender-mismatch problem we'd otherwise hit (Фактура fem., Плаќање neut., Корисник masc.) since Macedonian plural is gender-neutral.
Pluralizes 27 status keys across invoices, estimates, recurring_invoices and settings.preferences: paid → Платени, viewed → Прегледани, overdue → Задоцнети, completed → Завршени, accepted → Прифатени, rejected → Одбиени, expired → Истечени, active → Активни, recurring → Повторливи, one_time → Еднократни, partially_paid → Делумно платени, etc.
Deliberately preserved as singular: general.draft / general.sent and their estimates.* aliases (inherited by single-item header badges), recurring_invoices.on_hold / settings.preferences.on_hold (prepositional phrase, doesn't decline), and settings.exchange_rate.active (per-row is_active flag on a single provider entry).
lang/mk.json was an empty Crowdin stub with only 16 of 1591 keys translated (~1%), and several of those were stale English variants from older en.json revisions ('Roles' instead of 'Company Roles', 'Notes' instead of 'Record Notes', 'Backup' instead of 'Backups', etc.) that survived as pseudo-translations. Replace the stub with a full Cyrillic translation covering 1578 of 1591 keys (~99%), with structural parity to en.json (zero missing keys, identical 1769-line shape, valid JSON).
The 13 deliberately untranslated entries are brand names and technical conventions that should not be localized: CC, BCC, 404, EXP-001, Slug, Placeholder, URL, dropbox, and the four exchange-rate provider proper nouns (Currency Freak, Currency Layer, Open Exchange Rate, Currency Converter). Industry-standard transliterations are used where Macedonian doesn't have a settled term (Драјвер for driver, S3 / AWS / Mailgun / Dropbox kept as proper nouns, ДДВ for VAT). Vue-i18n pluralization syntax preserved (e.g. 'Фактура | Фактури', 'Корисник | Корисници').
All sidebar menu titles, form labels, validation messages, settings pages, wizard flow, error states, PDF labels and email templates are translated. Re-rendering the SPA after a hard refresh shows the full admin UI in Cyrillic when the company language is set to Macedonian.
Both lang/he.json and lang/ur.json shipped as empty Crowdin stubs but never made it into config('invoiceshelf.languages'), so admins could not pick either locale even after the matching font packages (noto-sans-hebrew, noto-naskh-arabic) landed in commit 04952d91. Add the two entries with native names (עברית, اردو) following the existing Svenska / ไทย / Tiếng Việt convention for non-Latin scripts.
Inserted alphabetically by English name — he between Greek and Hindi, ur after Ukrainian. The first PDF render for either locale will trigger ensureFontsForLocale() and synchronously install the corresponding font package; the UI itself stays mostly English until the Crowdin translations catch up, which is the intended trade-off — these locales are useful primarily as language tags for PDF rendering of Hebrew / Arabic-script customer data.
Closes the audit gaps from the original font system commit. The bundled NotoSans only covered Latin/Greek/Cyrillic but the descriptions claimed Arabic, Thai and Hindi too — that was false. DejaVu Sans, the prior dompdf default, did cover Hebrew, Arabic, Armenian and Georgian, so swapping it for NotoSans had silently regressed those scripts. The Thai conditional include was also dropped from every PDF template in that commit, leaving th locales rendering boxes despite THSarabunNew still sitting in resources/static/fonts/.
Adds four on-demand Font Packages — Noto Sans Hebrew, Noto Naskh Arabic (covering Arabic, Persian, Urdu, Sorani Kurdish), Noto Sans Devanagari (Hindi, Marathi, Sanskrit, Nepali) and Sarabun (Thai) — sourced from openmaptiles/fonts and google/fonts as static TTF. Static is mandatory because dompdf's PHP-Font-Lib does not parse variable fonts. Sarabun replaces THSarabunNew as the Thai face: same designer, OFL-licensed, maintained on a stable upstream URL, and surfaces through the same install flow as every other non-Latin script. The bundled THSarabunNew TTF files and the dead app/pdf/locale/th.blade.php legacy partial are removed as part of the migration.
Unifies the bundled Noto Sans into FONT_PACKAGES as a noto-sans entry with bundled => true and files served from resources/static/fonts/ instead of storage/fonts/. FontService::isInstalled, downloadPackage, getInstalledFontFaces and getPackageStatuses honor the flag through a new packageDir() helper. The hardcoded @font-face block in the PDF partial is gone — fonts.blade.php collapses to a single getInstalledFontFaces() call so the package array is the only source of truth for every face, bundled or on-demand. Admin → Font Packages now lists Noto Sans at the top with a primary-colored Bundled pill (new settings.fonts.bundled string) alongside the existing Installed badge / Install button states.
Also fixes the misleading settings.fonts.description and settings.fonts.bundled_info copy to actually describe what ships out of the box vs. what's optional, and rebuilds the en locale chunk.
Switch the BaseModal panel container from overflow-hidden to overflow-visible so multiselect dropdowns, autocompletes and tooltips can render outside the modal bounds. Same fix as the settings layout panel earlier — Headless UI's dialog still clips at the viewport, and the modal panel keeps its rounded-xl corners through its own bg-surface background, so removing the clip exposes no square-corner regression on existing modals.
Sidebar entries Backup and File Disk now read Backups and File Disks to match how the underlying pages actually behave (each one lists/manages multiple entries) and to align with the recently added Font Packages entry. The settings.{backup,disk}.title pluralization slots are collapsed to plural-only so the existing $t(..., 1) call sites in AdminBackupView, AdminFileDiskView and the legacy v1 BackupSetting view render the plural form on the page header without touching any Vue code.
Also finishes the Fonts → Font Packages rename: menu_title.fonts and fonts.title both read Font Packages, and fonts.description leads with 'used exclusively when generating PDF documents' so the PDF-only scope is unmissable.
Switch the settings layout content wrapper from overflow-hidden to overflow-visible in SettingsLayoutView, UserSettingsLayoutView and AdminSettingsView so multiselect dropdowns and tooltips can render outside the panel without being clipped.
Existing accounts inherited the company language at creation time and there was no way to change UI language per user. Add a 'Default (Company Language)' entry to the language selector in UserGeneralView, persist the choice through userStore.updateUserSettings and reload the i18n bundle via window.loadLanguage. The 'default' sentinel keeps the user opted in to the company-wide setting.
Bootstrap (global.store) now syncs userForm from current_user data and resolves the active UI language as user > company > 'en'. RegisterController, InvitationRegistrationController and MemberService seed new users with language=default instead of copying the current company setting, so promoting/inviting members no longer leaks the inviter's frozen language.
Adds AdminFontView with package list, install buttons, status indicators and toast notifications backed by /api/v1/fonts/status and /api/v1/fonts/{package}/install. Wires the new admin.settings.fonts lazy route and a Languages-icon menu entry under Admin → Settings.
Bundle Noto Sans (Regular/Bold/Italic/BoldItalic) under resources/static/fonts/ as the default PDF face — it covers Latin, Cyrillic, Greek, Arabic, Thai and Hindi out of the box, replacing the limited DejaVu Sans fallback. Move all @font-face declarations into app.pdf.partials.fonts and include it from every invoice/estimate/payment/report template, dropping per-template font-family hardcodes and the conditional Thai locale include.
Introduce FontService + FontController to download static Noto Sans CJK packages (zh, zh_CN, ja, ko) from life888888/cjk-fonts-ttf on demand. GeneratesPdfTrait::ensureFontsForLocale primes the family before rendering and the partial emits @font-face rules for installed packages so dompdf resolves them through standard CSS — no separate registerFont() instance required. Static TTFs are mandatory because dompdf's PHP-Font-Lib does not parse variable fonts (fvar/gvar tables), which is why Google Fonts' NotoSansTC[wght].ttf rendered empty boxes.
Expose status/install via /api/v1/fonts/status and /api/v1/fonts/{package}/install with matching FONTS_STATUS / FONTS_INSTALL constants in scripts-v2/api/endpoints.ts. Flip DOMPDF_ENABLE_REMOTE default to true for remote asset loading.
The @font-face URLs in resources/views/app/pdf/locale/th.blade.php pointed to resource_path('static/static/fonts/THSarabunNew*.ttf'), which does not exist on disk, so dompdf fell back to a default face when rendering Thai PDFs. Drop the duplicated segment so the bundled THSarabunNew TTFs resolve correctly.
Clears config and application cache on every container start to
prevent stale provider references after image updates. Creates the
storage symlink and runs pending migrations if the app is already
installed.
Fixes#614
Migrates media disk references from the old temp_{driver} naming to
the new disk_{id} scheme. System local disks map to 'local', remote
disks map to 'disk_{id}'. Structured as the single v3.0 upgrade
migration for future additions.
Major changes to the file disk subsystem:
- Each FileDisk now gets a unique Laravel disk name (disk_{id}) instead
of temp_{driver}, fixing the bug where multiple local disks with
different roots overwrote each other's config.
- Move disk registration logic from FileDisk model to FileDiskService
(registerDisk, getDiskName). Model keeps only getDecodedCredentials
and a deprecated setConfig() wrapper.
- Add Disk Assignments admin UI (File Disk tab) with three purpose
dropdowns: Media Storage, PDF Storage, Backup Storage. Stored as
settings (media_disk_id, pdf_disk_id, backup_disk_id).
- Backup tab now uses the assigned backup disk instead of a per-backup
dropdown. BackupsController refactored to use BackupService which
centralizes disk resolution. Removed stale 4-second cache.
- Add local_public disk to config/filesystems.php so system disks
are properly defined.
- Local disk roots stored relative to storage/app/ with hint text
in the admin modal explaining the convention.
- Fix BaseModal watchEffect -> watch to prevent infinite request
loops on the File Disk page.
- Fix string/number comparison for disk purpose IDs from settings.
- Add safeguards: prevent deleting disks with files, warn on
purpose change, prevent deleting system disks.
Remove duplicate configureMediaDisk() from AppServiceProvider — all
FileDisk and media-library config is now in AppConfigProvider's
configureFileSystemFromDatabase().
Replace setConfig() calls with inline config registration everywhere
to avoid mutating filesystems.default, which caused infinite request
loops on the File Disk admin page.
configureMediaDisk() was calling FileDisk::setConfig() which mutates
the global filesystems.default config on every request. This caused
cascading requests on the File Disk admin page.
Now registers the media disk config directly without changing the
global default filesystem.
Spatie Media Library now uses the default FileDisk (local_private) for
new uploads instead of the public disk. Expense receipts are no longer
directly web-accessible.
- AppServiceProvider configures media-library disk from FileDisk on boot
- Change media-library fallback from 'public' to 'local'
- Expense receipt URL accessor returns authenticated route instead of
direct file URL
- Add registerMediaCollections() to Expense model
- Prevent deleting FileDisk that contains files or is a system disk
- Add media:secure command to migrate existing receipts to private disk
Fixes#187
Add IdnEmail validation rule that converts IDN domains to Punycode
via idn_to_ascii() before validating with FILTER_VALIDATE_EMAIL.
Applied to all email fields: customers, members, profiles, admin
users, customer portal profiles, and mail configuration.
Includes unit tests for standard emails, IDN emails, and invalid
inputs.
Fixes#388
Pass the app's configured timezone to CronExpression::getNextRunDate()
so the next invoice date is calculated in the correct timezone instead
of defaulting to UTC.
Fixes#491
The customer portal bootstrap now returns current_company_currency
alongside the customer's own currency. The store falls back to the
company currency when the customer has no currency assigned.
Fixes#142
Custom fields defined on an estimate are now carried over to the
invoice when using Convert to Invoice. Uses the same pattern as
the clone method.
Fixes#282
CompanyResource now includes user_role — the authenticated user's
Bouncer role title scoped to that company (e.g. "Owner"). Displayed
as a subtitle under each company name in the switcher dropdown.
New backend endpoint POST /invoices/{id}/convert-to-estimate that
creates a draft estimate from an invoice, copying items, taxes,
custom fields, and financial data. Frontend wired with dropdown
action, store method, and API service call.
SendInvoiceModal and SendEstimateModal were only mounted on detail
views. Resend from table dropdowns did nothing because the modal
component was not in the DOM. Added to index views and dashboard.
Pass canCreatePayment and canCreateEstimate props to InvoiceDropdown
from detail view and dashboard.
Copy PDF URL now checks window.isSecureContext before using
navigator.clipboard, falls back to textarea+execCommand on HTTP,
and shows a success notification.
Invoice dropdown: Mark as Sent uses its own condition instead of
reusing Send, Resend hidden in detail view.
Estimate dropdown: Mark as Accepted/Rejected hidden when already in
the other terminal state, Convert to Invoice hidden on rejected
estimates. Added Convert to Estimate action for invoices.
fetchBasicMailConfig() was calling the wrong endpoint (company-config
instead of default config). Also, the response has no .data wrapper so
from_mail was never extracted. Fixed in all three send modals.
Estimate and payment preview blob construction now falls back to the
raw response when .data is undefined, matching the invoice modal.
Global percentage taxes are now recalculated when items or discount
change, preventing stale tax amounts. Math.round() applied to
sub_total, total, and tax in invoice/estimate submit payloads to
ensure the backend always receives whole-cent integers.
Read default_invoice_template and default_estimate_template from
useUserStore().currentUserSettings instead of relying on a parameter
that was never passed from the create views.
- Fix exchange-rate service types to match actual backend response shapes
(exchangeRate array, activeProvider success/error, used currencies as strings)
- Add ExchangeRateConverter to payments, expenses, and recurring invoices
- Set currency_id from customer currency in invoice/estimate selectCustomer()
- Load globalStore.currencies in ExchangeRateConverter on mount
- Pass driver/key/driver_config params to getSupportedCurrencies in provider modal
- Fix OpenExchangeRateDriver validateConnection to use base=USD (free plan compat)
- Fix checkActiveCurrencies SQLite whereJsonContains with array values
- Remove broken currency/companyCurrency props from ExpenseCreateView, use stores
- Show base currency equivalent in document line items and totals when exchange
rate is active
- Add manifest.json generation script (scripts/generate-manifest.php)
- Add Updater::cleanStaleFiles() that removes files not in manifest
- Add /api/v1/update/clean endpoint with backward compatibility
- Add configurable update_protected_paths in config/invoiceshelf.php
- Update frontend to use clean step instead of delete step
- Add GitHub Actions release workflow triggered on version tags
- Add .github/release.yml for auto-generated changelog categories
- Update Makefile to include manifest generation and scripts directory
Create BaseCustomTag (dynamic tag render), BaseFormatMoney,
BaseHeading, BaseScrollPane, BaseDescriptionList/Item, BaseLabel,
BaseCustomerSelectInput, BaseSpinner, BaseRating, and status label
components. Register all renamed v2 components under their old
Base* names (BaseInputGroup->FormGroup, BasePage->Page,
BaseTable->DataTable, etc.) so templates resolve correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
from eager glob, clean up dynamic import conflicts
- Add missing isImageFile() to format-money utils
- Exclude BaseMultiselect, InvoicePublicPage, InvoiceInformationCard
from eager glob in global-components.ts using negative patterns
- Short-circuit English locale in i18n to avoid redundant dynamic import
- Only en.json warning remains (intentional: English bundled inline)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LOGIN was posting to /api/v1/auth/login but the actual route is
POST /login (session-based web route). LOGOUT was /api/v1/auth/logout
but the actual is POST /auth/logout. All 76+ other endpoints verified
correct against routes/api.php.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>