Commit Graph

220 Commits

Author SHA1 Message Date
Darko Gjorgjijoski
345bfde306 feat(modules): translated display names and inline settings modal
CompanyModulesController attaches a translated display_name to each module before returning the list. ModuleSettingsController gains a translateSchema() helper that resolves section titles and field labels against the host app's i18n store before sending the schema to the frontend, so module authors can keep their 'my_module::settings.field' keys and users still see localized strings.

Per-module settings now open in an inline ModuleSettingsModal rather than routing to a standalone page. The modal reuses BaseSchemaForm for rendering, so the whole interaction takes place in-context next to the module card the user clicked — no navigation, no loss of place.

CompanyModuleCard displays the translated display_name instead of the raw slug and emits open-settings with the module payload; the parent view hands that to the modal store.
2026-04-10 21:30:00 +02:00
Darko Gjorgjijoski
23d1476870 refactor(modules): marketplace install flow with checksum validation
Rewires module installation to use slug + version + checksum_sha256 instead of the opaque module identifier. ModuleInstaller splits marketplace token handling out of install() into helpers, adopts structured error responses, and validates the downloaded archive's SHA-256 against the marketplace manifest before unpacking.

ModuleResource is simplified to accept an already-loaded installed-module instance rather than fetching it from state, exposes access_tier and checksum fields, and drops the auto-disable-on-unpurchased side effect that was bleeding write logic into a read resource. UnzipUpdateRequest accepts a nullable module with a conditional module_name field so the same endpoint serves both app and module updates.

ModulesPolicy::manageModules now short-circuits for super-admins so administration flows (token validation, store state) are not blocked on a company-scoped ability. Two new feature tests cover both the authorization bypass and ModuleResource serialization.
2026-04-10 17:30:00 +02:00
Darko Gjorgjijoski
42ce99eeba Show common currencies first in dropdowns and default to USD in install wizard
Currency dropdowns now display the most-traded currencies (USD, EUR, GBP,
JPY, CAD, AUD, CHF, CNY, INR, BRL) at the top, followed by the rest
alphabetically. The install wizard defaults to USD instead of EUR and
formats currency names as "USD - US Dollar" for consistency with the
rest of the app.
2026-04-10 15:55:46 +02:00
Darko Gjorgjijoski
9174254165 Refactor install wizard and mail configuration 2026-04-09 10:06:27 +02:00
Darko Gjorgjijoski
7743c2e126 feat(modules): dynamic sidebar group rendering active modules
The sidebar gains a new section that lists each currently-activated module
as a direct shortcut to its settings page. This is the always-visible
companion to the company-context Active Modules index — both surface the
same set of modules, but the index is the catalog landing page and the
sidebar group is the per-module quick access.

- BootstrapController returns module_menu populated from
  \InvoiceShelf\Modules\Registry::allMenu(), but only on the company-context
  branch — not on the super-admin branch (lines 53-69), since super admins
  don't see the dynamic group. Because nwidart only boots service providers
  for currently-activated modules, the registry naturally contains only
  active modules at request time, no extra filtering needed.

- bootstrap.service.ts BootstrapResponse type extended with
  module_menu?: ModuleMenuItem[]; new ModuleMenuItem interface
  (title/link/icon) — shaped distinctly from MenuItem because module entries
  use namespaced i18n keys and don't carry group/ability metadata.

- global.store.ts exposes a moduleMenu ref + a hasActiveModules computed.

- SiteSidebar.vue appends a new "Modules" section after the existing
  menuGroups output, in both the mobile (Dialog) and desktop branches. The
  section is hidden when hasActiveModules is false. Uses the
  modules.sidebar.section_title i18n key added in the previous commit.
2026-04-09 00:29:56 +02:00
Darko Gjorgjijoski
e6eeacb6d4 feat(modules): company-context module surfaces and schema-driven settings
Adds the read-only company "Active Modules" index page (lists every
instance-activated module with a Settings shortcut) and the schema-driven
settings framework (generic BaseSchemaForm.vue renderer + per-company
persistence in CompanySetting). Bundled because they share the same
routes/api.php edit and the index page's Settings button targets the
settings page.

Backend:

- CompanyModulesController::index() returns every Module::enabled = true row
  with a kebab-case slug (via Str::kebab()) and a has_settings flag computed
  from \InvoiceShelf\Modules\Registry::settingsFor(). nwidart stores module
  names in PascalCase ("HelloWorld") but URLs and registry keys use kebab
  ("hello-world") — the controller normalizes so module authors can call
  Registry::registerSettings('hello-world') naturally without thinking
  about the storage format.

- ModuleSettingsController::show(\$slug) returns the registered Schema +
  per-company values from CompanySetting (defaults flow through when nothing
  has been saved yet). update(\$slug) builds Laravel validator rules from
  the Schema's per-field rules arrays — with type-rule fallbacks for
  switch -> boolean, number -> numeric, multiselect -> array — silently
  drops unknown keys, and persists via CompanySetting::setSettings() under
  the module.{slug}.{key} prefix. Activation is instance-global, but
  settings are per-company: two companies on the same instance can
  configure the same activated module differently.

- routes/api.php mounts GET /api/v1/company-modules at the root of the
  company API group and GET/PUT /api/v1/modules/{slug}/settings inside the
  existing modules prefix.

Frontend:

- BaseSchemaForm.vue is the central new component — a generic schema-driven
  form renderer that maps schema fields to BaseInput / BaseTextarea /
  BaseSwitch / BaseMultiselect by type, and builds Vuelidate rules
  dynamically from each field's rules array (supports required, email, url,
  numeric, min:N, max:N). New fields are added by extending the type ->
  component map.

- CompanyModulesIndexView.vue fetches /company-modules and renders a card
  grid (with empty/loading states); CompanyModuleCard.vue is the per-row
  component with the Settings button. ModuleSettingsView.vue fetches
  /modules/{slug}/settings, hands {schema, values} to BaseSchemaForm, and
  posts back on submit.

- Company-context routes.ts is rebuilt after the previous commit relocated
  the marketplace browser away. It now declares modules.index +
  modules.settings, both gated by manage-module ability.

- New api/services/{companyModules,moduleSettings}.service.ts thin clients.

- lang/en.json adds modules.index.{description,empty_title,empty_description},
  modules.settings.{title,open,saved,not_found,none}, and
  modules.sidebar.section_title. The sidebar key is added here even though
  the dynamic sidebar rendering lands in the next commit — keeping all i18n
  additions in one file edit avoids hunk-splitting lang/en.json.
2026-04-09 00:29:36 +02:00
Darko Gjorgjijoski
b2b7a07e0c refactor(modules): migrate asset registry from app/Services to invoiceshelf/modules package
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.
2026-04-09 00:27:44 +02:00
Darko Gjorgjijoski
119a1712b0 Port expense report grouped itemized view + i18n + return types from master
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:".
2026-04-07 17:28:34 +02:00
Darko Gjorgjijoski
78ed332d06 Add per-user language preference with company default fallback
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.
2026-04-07 04:41:00 +02:00
Darko Gjorgjijoski
ba5c6c39ba Add multilingual PDF font system with Noto Sans and on-demand CJK packages
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.
2026-04-06 23:32:00 +02:00
Darko Gjorgjijoski
20085cab5d Refactor FileDisk system with per-disk unique names and disk assignments UI
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.
2026-04-07 02:04:57 +02:00
Darko Gjorgjijoski
67268ac2b7 Secure expense receipts by wiring Media Library to FileDisk
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
2026-04-07 01:01:59 +02:00
Darko Gjorgjijoski
9638e02eb8 Fix customer portal not reflecting company default currency
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
2026-04-06 23:37:56 +02:00
Darko Gjorgjijoski
25b61b73a0 Fix case-sensitive email login
Email comparison on login now uses LOWER() for case-insensitive
matching. Applied to both admin and customer portal login controllers.

Fixes #424
2026-04-06 23:22:16 +02:00
Darko Gjorgjijoski
9ca998e64a Add Convert to Estimate feature for invoices
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.
2026-04-06 22:57:03 +02:00
Darko Gjorgjijoski
e64529468c Replace deleted_files with manifest-based updater cleanup, add release workflow
- 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
2026-04-06 19:27:33 +02:00
Darko Gjorgjijoski
74b4b2df4e Finalize Typescript restructure 2026-04-06 17:59:15 +02:00
Darko Gjorgjijoski
eb0a588164 Refactor Administration entrypoint
We moved the administration item to the company switcher in the header
2026-04-04 01:36:28 +02:00
Darko Gjorgjijoski
fae59221d3 Generate admin menus for super admins without a company
Super admin users with no company associations now receive their
administration menu items in the bootstrap response instead of
empty arrays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 00:24:00 +02:00
Darko Gjorgjijoski
c1994887ef Support invitations for unregistered users
When inviting an email without an InvoiceShelf account, the email now
links to a registration page (/register?invitation={token}) instead of
login. After registering, the invitation is auto-accepted.

Backend:
- InvitationRegistrationController: public details() and register()
  endpoints. Registration validates token + email match, creates account,
  auto-accepts invitation, returns Sanctum token.
- AuthController: login now accepts optional invitation_token param to
  auto-accept invitation for existing users clicking the email link.
- CompanyInvitationMail: conditional URL based on user existence.
- Web route for /invitations/{token}/decline (email decline link).

Frontend:
- RegisterWithInvitation.vue: fetches invitation details, shows company
  name + role, registration form with pre-filled email.
- Router: /register route added.

Tests: 3 new tests (invitation details, register + accept, email mismatch).
2026-04-03 23:26:58 +02:00
Darko Gjorgjijoski
8a6c085288 Rename company-scoped Users to Members throughout
Complete rename across backend and frontend:
- Controller: Company/Users/UsersController -> Company/Members/MembersController
- Service: UserService -> MemberService
- Requests: UserRequest -> MemberRequest, DeleteUserRequest -> DeleteMemberRequest
- API routes: /api/v1/users -> /api/v1/members (company-scoped only)
- Sidebar menu: "Users" -> "Members"
- Frontend: views/users -> views/members, stores/users -> stores/members
- Router: users.index -> members.index, /admin/users -> /admin/members
- i18n: new "members" section with invitation-related keys
- Tests: UserTest -> MemberTest

Admin/super-admin Users (system-wide user management) remains unchanged.
2026-04-03 23:12:30 +02:00
Darko Gjorgjijoski
92a1baced4 Add company invitation system (backend)
New feature allowing company owners/admins to invite users by email with
a specific company-scoped role.

Database:
- New company_invitations table (company_id, email, role_id, token,
  status, invited_by, expires_at)

Backend:
- CompanyInvitation model with pending/forUser scopes
- InvitationService: invite, accept, decline, getPendingForUser
- CompanyInvitationMail with markdown email template
- InvitationController (company-scoped): list, send, cancel invitations
- InvitationResponseController (user-scoped): pending, accept, decline
- BootstrapController returns pending_invitations in response
- CompanyMiddleware handles zero-company users gracefully

Tests: 9 feature tests covering invite, accept, decline, cancel, expire,
duplicate prevention, and bootstrap integration.
2026-04-03 22:58:55 +02:00
Darko Gjorgjijoski
00d5abae5f Eliminate Company\CompaniesController, introduce owner role
Redistribute methods:
- show() -> BootstrapController::currentCompany()
- store(), destroy(), userCompanies() -> Admin\CompaniesController
- transferOwnership() -> CompanySettingsController

Security fix: introduce 'owner' role for company-level admin, distinct
from 'super admin' which is now global platform admin only.
- CompanyService::setupRoles() creates 'owner' role per company
- Company creation assigns scoped 'owner' role instead of global 'super admin'
- Seeders updated to assign 'owner'

Migration renames all existing company-scoped 'super admin' roles to
'owner' and ensures every company owner has the role assigned.
2026-04-03 22:33:56 +02:00
Darko Gjorgjijoski
5912995164 Move CompaniesController from Company/Company/ to Company/ to eliminate namespace stutter 2026-04-03 22:20:04 +02:00
Darko Gjorgjijoski
6f095210d6 Consolidate Pdf controllers: 6 -> 1 DocumentPdfController
Merge InvoicePdfController, EstimatePdfController, PaymentPdfController
into DocumentPdfController with invoice(), estimate(), payment() methods.

Delete DownloadInvoicePdfController and DownloadPaymentPdfController
(dead code — not mapped in any routes).

Move DownloadReceiptController logic to ExpensesController::downloadReceipt()
(expense receipts, not PDF documents).
2026-04-03 22:16:20 +02:00
Darko Gjorgjijoski
b9e34ff25c Consolidate Company/Settings: 7 controllers -> 5
Merge CompanyCurrencyCheckTransactionsController into
CompanySettingsController as checkTransactions() method.

Merge UserSettingsController into UserProfileController as
showSettings() and updateSettings() methods — both operate on
the authenticated user (/me routes).
2026-04-03 22:11:16 +02:00
Darko Gjorgjijoski
8e7c48f532 Move BackupsController and UpdateController to Admin/ namespace directly
Remove single-file Backup/ and Update/ subdirectories. These controllers
now sit alongside CompaniesController, UsersController, etc. in Admin/.
2026-04-03 21:49:30 +02:00
Darko Gjorgjijoski
20ace694fe Fix UpdateController auth: use Bouncer ability instead of company owner check
ensureOwner() checked isOwner() which only verifies company ownership,
not super admin status. Replace with authorize('manage update app')
which uses the proper Bouncer ability gate for platform administration.
2026-04-03 21:45:40 +02:00
Darko Gjorgjijoski
3f5accc0f0 Consolidate Admin/Update: 8 controllers into 1 UpdateController
Merge 7 single-action pipeline controllers (checkVersion, download,
unzip, copy, delete, migrate, finish) into UpdateController with named
methods. Remove dead UpdateController that duplicated the same logic
but wasn't referenced in routes. Extract shared owner check into
private ensureOwner() helper. Route URLs unchanged.
2026-04-03 21:42:45 +02:00
Darko Gjorgjijoski
7bb6d9bcc3 Consolidate Admin/Settings: merge GetSettingsController + UpdateSettingsController into SettingsController 2026-04-03 21:21:13 +02:00
Darko Gjorgjijoski
142899cfd7 Consolidate Admin/Backup: merge ApiController and DownloadBackupController into BackupsController
Inline the respondSuccess() helper, add download() method. Remove the
unnecessary ApiController base class and DownloadBackupController.
2026-04-03 21:18:45 +02:00
Darko Gjorgjijoski
d505677a74 Consolidate Admin/Modules: 10 single-action controllers into 2
ModulesController: index, show, checkToken, enable, disable
ModuleInstallationController: download, upload, unzip, copy, complete
2026-04-03 21:16:18 +02:00
Darko Gjorgjijoski
85b62dfdf8 Refactor exchange rate providers into driver-based architecture
Replace duplicated switch/case blocks across 4 methods with a clean
abstract driver pattern:

- ExchangeRateDriver (abstract): defines getExchangeRate(),
  getSupportedCurrencies(), validateConnection()
- CurrencyFreakDriver, CurrencyLayerDriver, OpenExchangeRateDriver,
  CurrencyConverterDriver: concrete implementations
- ExchangeRateDriverFactory: resolves driver name to class, with
  register() method for module extensibility

Delete ExchangeRateProvidersTrait — all logic now lives in driver
classes and ExchangeRateProviderService. Adding a new exchange rate
provider only requires implementing ExchangeRateDriver and calling
ExchangeRateDriverFactory::register() in a module service provider.
2026-04-03 20:24:03 +02:00
Darko Gjorgjijoski
8f29e8f5de Extract business logic from remaining models to services
New services:
- ExchangeRateProviderService: CRUD, API status checks, currency converter
  URL resolution (extracted 122 lines from ExchangeRateProvider model)
- FileDiskService: create, update, setAsDefault, validateCredentials
  (extracted 97 lines from FileDisk model)
- ItemService: create/update with tax handling (extracted from Item model)
- TransactionService: create/complete/fail (extracted from Transaction model)
- CustomFieldService: create/update with slug generation (extracted from
  CustomField model)

Controllers updated to use constructor-injected services:
ExchangeRateProviderController, DiskController, ItemsController,
CustomFieldsController.
2026-04-03 19:32:37 +02:00
Darko Gjorgjijoski
ece6ce737b Rename Services/Installation to Services/Setup to match controllers 2026-04-03 19:23:32 +02:00
Darko Gjorgjijoski
4f47db9258 Move Mobile/AuthController to Company/Auth and remove Mobile namespace
The Mobile namespace only contained an API auth controller (Sanctum token
login/logout/check) that is not mobile-specific. Relocated to
Company/Auth/AuthController alongside the other auth controllers.
2026-04-03 19:19:09 +02:00
Darko Gjorgjijoski
64c481e963 Rename controller namespaces: drop V1 prefix, clarify roles
V1/Admin     -> Company       (company-scoped controllers)
V1/SuperAdmin -> Admin        (platform-wide admin controllers)
V1/Customer  -> CustomerPortal (customer-facing portal)
V1/Installation -> Setup      (installation wizard)
V1/PDF       -> Pdf           (consistent casing)
V1/Modules   -> Modules       (drop V1 prefix)
V1/Webhook   -> Webhook       (drop V1 prefix)

The V1 prefix served no purpose - API versioning is in the route prefix
(/api/v1/), not the controller namespace. "Admin" was misleading for
company-scoped controllers. "SuperAdmin" is now simply "Admin" for
platform administration.
2026-04-03 19:15:20 +02:00
Darko Gjorgjijoski
0aaf0419c3 Reorganize Admin/General: 14 controllers down to 6
Move global reference data to SuperAdmin:
- CountriesController, CurrenciesController (not company-scoped)

Merge exchange rate operations into ExchangeRateProviderController:
- GetAllUsedCurrenciesController -> usedCurrenciesWithoutRate()
- BulkExchangeRateController -> bulkUpdate()

Consolidate single-action controllers:
- DateFormatsController + TimeFormatsController + TimezonesController -> FormatsController
- NextNumberController + NumberPlaceholdersController -> SerialNumberController
- SearchUsersController merged into SearchController::users()
2026-04-03 19:03:57 +02:00
Darko Gjorgjijoski
c0454613a8 Move customer stats logic from CustomerStatsController to CustomerService 2026-04-03 18:10:59 +02:00
Darko Gjorgjijoski
92872e7e1c Merge ShowReceiptController and UploadReceiptController into ExpensesController 2026-04-03 18:07:07 +02:00
Darko Gjorgjijoski
2191417151 Consolidate ExchangeRate single-action controllers into ExchangeRateProviderController
Merge 4 invocable controllers (GetActiveProvider, GetExchangeRate,
GetSupportedCurrencies, GetUsedCurrencies) as methods on the existing
resource controller: activeProvider(), getRate(), supportedCurrencies(),
usedCurrencies().
2026-04-03 18:03:24 +02:00
Darko Gjorgjijoski
5f389ea3b0 Consolidate single-action controllers into resource controllers
Merge 11 single-action controllers into their parent resource controllers:
- Invoice: send, sendPreview, clone, changeStatus -> InvoicesController
- Estimate: send, sendPreview, clone, convertToInvoice, changeStatus -> EstimatesController
- Payment: send, sendPreview -> PaymentsController

Extract clone and convert business logic from controllers into services:
- InvoiceService: add clone(), changeStatus()
- EstimateService: add clone(), convertToInvoice(), changeStatus()

Previously this logic was inlined in controllers (~80-90 lines each).
2026-04-03 17:55:46 +02:00
Darko Gjorgjijoski
f76f351244 Merge CompanyController into CompaniesController as show() method 2026-04-03 17:45:20 +02:00
Darko Gjorgjijoski
1ca915a0a3 Split CompanyController and introduce standalone User Settings page
Backend:
- Extract user profile methods (show, update, uploadAvatar) from
  CompanyController into new UserProfileController
- CompanyController now only handles company concerns (updateCompany,
  uploadCompanyLogo)
- Remove Account Settings from setting_menu config

Frontend:
- New /admin/user-settings page with 3 tabs: General, Profile Photo,
  Security (password change)
- User dropdown now links to /admin/user-settings instead of
  /admin/settings/account-settings
- Settings sidebar defaults to Company Information as first item
- Remove old monolithic AccountSetting.vue
2026-04-03 17:35:41 +02:00
Darko Gjorgjijoski
6b5e4878fb Consolidate Admin Settings controllers: merge Get/Update pairs
Merge GetCompanySettingsController + UpdateCompanySettingsController into
CompanySettingsController with show() and update() methods.

Merge GetUserSettingsController + UpdateUserSettingsController into
UserSettingsController with show() and update() methods.

Absorb GetCompanyMailConfigurationController into
CompanyMailConfigurationController as getDefaultConfig() method.

Removes 5 single-action controllers, down to 4 Settings controllers.
2026-04-03 17:18:48 +02:00
Darko Gjorgjijoski
bbf46577dc Move global admin controllers from Admin to SuperAdmin namespace
Backup, Update, Modules, and global Settings controllers (mail config,
PDF config, disk management, global settings) are application-wide features
not scoped to a company. Move them from Admin/ to SuperAdmin/ to match the
v3.0 UI structure where these live under Administration.

Company-scoped settings controllers remain in Admin/Settings/.
2026-04-03 16:52:18 +02:00
Darko Gjorgjijoski
129028518d Consolidate PDF classes under app/Services/Pdf with consistent naming
Split PDFService.php (3 classes + 2 interfaces in one file) into separate
files. Move GotenbergPDFDriver from app/Services/PDFDrivers/ into
app/Services/Pdf/. Normalize casing from ALL-CAPS PDF to Pdf throughout:
facade, provider, service, driver factory, and Gotenberg driver.

Fix PaymentService using Barryvdh DomPDF facade directly instead of the
app's PDF facade (bypassed the driver factory). Report controllers also
updated to use the app facade.
2026-04-03 16:18:25 +02:00
Darko Gjorgjijoski
e0b8b86e06 Rename SerialNumberFormatter to SerialNumberService for consistency 2026-04-03 16:09:22 +02:00
Darko Gjorgjijoski
0ce88ab817 Remove app/Space folder and extract model business logic into services
Relocate all 14 files from the catch-all app/Space namespace into proper
locations: data providers to Support/Formatters, installation utilities to
Services/Installation, PDF utils to Services/Pdf, module/update classes to
Services/Module and Services/Update, SiteApi trait to Traits, and helpers
to Support.

Extract ~1,400 lines of business logic from 8 fat models (Invoice, Payment,
Estimate, RecurringInvoice, Company, Customer, Expense, User) into 9 new
service classes with constructor injection. Controllers now depend on
services instead of calling static model methods. Shared item/tax creation
logic consolidated into DocumentItemService.
2026-04-03 15:37:22 +02:00
Darko Gjorgjijoski
23ff69026e Merge branch 'master' into v3.0 2026-04-03 14:36:24 +02:00