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>
Rewrite all 7 global stores from JS options API to TypeScript
composition API. 8 files, 1005 lines, zero any types.
- auth.store.ts: login/logout with authService
- global.store.ts: bootstrap, menus, sidebar, config fetching
- company.store.ts: company selection, admin mode, settings
- user.store.ts: current user, abilities, settings
- notification.store.ts: typed toast notifications
- dialog.store.ts: confirm dialog returning Promise<boolean>
- modal.store.ts: modal state with isEdit getter
All use async/await, typed API services, localStore utility,
and explicit ref<T> generics.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create the complete TypeScript foundation for the Vue 3 migration
in a parallel scripts-v2/ directory. 72 files, 5430 lines, zero
any types, strict mode.
- types/ (21 files): Domain interfaces for all 17 entities derived
from actual Laravel models and API resources. Enums for all
statuses. Generic API response wrappers.
- api/ (29 files): Typed axios client with interceptors, endpoint
constants from routes/api.php, 25 typed service classes covering
every API endpoint.
- composables/ (14 files): Vue 3 composition functions for auth,
notifications, dialogs, modals, pagination, filters, currency,
dates, theme, sidebar, company context, and permissions.
- utils/ (5 files): Pure typed utilities for money formatting,
date formatting (date-fns), localStorage, and error handling.
- config/ (3 files): Typed ability constants, app constants.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move collapse button from header into a sticky bottom toolbar in
the sidebar. Chevron double-left/right icon aligned to sidebar edge.
Toolbar area uses mt-auto with border-t separator and glass backdrop,
ready for additional action buttons in the future.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Desktop sidebar can be toggled between full width (w-56/w-64) and
collapsed icon-only mode (w-16). Collapsed state persists in
localStorage. Icons enlarge to w-6 h-6 when collapsed, labels
hidden, v-tooltip shows item names on hover. Group labels replaced
with thin dividers when collapsed. Toggle button pinned at bottom
with ChevronLeft/Right icon. Content area padding animates smoothly
with transition-all duration-300.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Apply glassmorphism to sidebar, cards, tables, modals, dropdowns,
and dialogs with semi-transparent backgrounds, backdrop-blur, and
white/15 borders. Add subtle gradient body background for the blur
to work against. Add dedicated btn-primary color tokens so primary
buttons stay bold in dark mode instead of using the brightened text
palette. Use shadow-sm to avoid heavy halos in light mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add gap-8 to user settings (was missing), and sticky top-20
self-start to all three settings sidebars (company, admin, user
profile) so the menu stays fixed while the content area scrolls.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add global webkit and Firefox scrollbar styling using semantic
color tokens. Fix component scrollbar classes in GlobalSearchBar
and CompanySwitcher from hardcoded gray to theme-aware colors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add dedicated header-from/header-to color tokens that are independent
of the primary palette dark mode overrides. Dark mode header uses a
deeper indigo gradient instead of the brightened primary colors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The bulk sed migration changed the PlusIcon from text-gray-600 to
text-body, but it sits on the gradient header and should be white.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace solid bg-surface background with bg-white/20 translucent
style matching the + button and company switcher. Use white text
and placeholder with opacity for consistency on the gradient header.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show a green check icon with tinted background when company is
using the global mail configuration, replacing the plain gray text.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change BaseDivider from text-subtle (which left the hr with a dark
default border) to border-line-light for a gentle themed separator.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change SENT status from yellow to green in both invoice and estimate
badges. Make PAID badge more noticeable with stronger green background
(40% opacity) and semibold text. Use consistent text-status-green
token for PAID across all badge components.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Define 13 semantic color tokens (surface, text, border, hover) with
light/dark values in themes.css. Register with Tailwind via @theme inline.
Migrate all 335 Vue files from hardcoded gray/white classes to semantic
tokens. Add theme toggle (sun/moon/system) in user avatar dropdown.
Replace @tailwindcss/forms with custom form reset using theme vars.
Add status badge and alert tokens for dark mode. Theme-aware chart
grid/labels, skeleton placeholders, and editor. Inline script in
<head> prevents flash of wrong theme on load.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Display the Bouncer role title in the members list table. Move
Update App to the last position in administration settings menu.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Personalize welcome heading with user name, add descriptive subtitle,
improve invitation card styling, remove redundant logout button. Fix
hasCreateAbilities check in header to actually call the function.
Widen company switcher dropdown and improve invitation row layout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make setSelectedCompany null-safe and clear stale localStorage.
Conditionally initialize company store state in bootstrap. Add
router guard to redirect no-company users to NoCompanyView while
allowing super admins through. Hide sidebar when no company.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>