Commit Graph

126 Commits

Author SHA1 Message Date
Darko Gjorgjijoski
7885bf9d11 feat(menu): priority-sorted menu groups, user-menu items, sidebar appearance toggle
Every main_menu entry moves from numeric group (1/2/3) to string-based group + group_label + priority. Groups now carry their own i18n label and child entries are sorted by an explicit priority field instead of config-array order, so module-contributed menu items can slot into any existing group at any position.

BootstrapController merges module-registered menu items into main_menu (previously they lived in a separate module_menu response key) and introduces a user_menu response key for items modules want to place in the avatar dropdown. The global store follows suit: moduleMenu becomes userMenu, menuGroups is a computed that sorts by priority, and hasActiveModules drops out.

New admin Appearance setting page with a single toggle for whether sidebar group labels render — so instances that prefer a compact sidebar can hide the Documents/Administration/Modules headings without losing the grouping itself. CompanyLayout watches route meta and re-bootstraps when the admin-mode flag flips so the sidebar repaints with the right menu on navigation across the admin boundary.

Test suites updated: module menu merging is asserted against main_menu (name: 'module-{slug}') rather than the old module_menu response; HelloWorldIntegrationTest verifies the schema translation path; CompanyModulesIndexTest covers the display_name attachment.
2026-04-11 00:30:00 +02:00
Darko Gjorgjijoski
9174254165 Refactor install wizard and mail configuration 2026-04-09 10:06:27 +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
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
f5e876122d New translations en.json (Hindi) 2026-04-07 17:12:41 +02:00
Darko Gjorgjijoski
f83ec6e78f Pluralize Macedonian status labels for filter tabs and column headers
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).
2026-04-07 12:43:06 +02:00
Darko Gjorgjijoski
9345d3e525 Translate Macedonian (mk) locale to ~99% coverage
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.
2026-04-07 12:35:18 +02:00
Darko Gjorgjijoski
04952d91ed Add Hebrew/Arabic/Devanagari/Sarabun font packages and unify Noto Sans into the package array
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.
2026-04-07 11:50:34 +02:00
Darko Gjorgjijoski
53932e9e16 Pluralize admin settings menu items and clarify Font Packages copy
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.
2026-04-07 10:53:45 +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
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
827c5c8938 Add collapsible sidebar with icon-only mode and tooltips
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>
2026-04-04 03:45:00 +02:00
Darko Gjorgjijoski
7a1e2cd2c3 Rename Notes to Record Notes in company settings menu
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 02:48:00 +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
c85051161b Improve NoCompanyView design and fix header for no-company state
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>
2026-04-04 00:48:00 +02:00
Darko Gjorgjijoski
6343b4a17f Add invitation frontend: invite modal, pending invitations, no-company view
Members Index:
- "Invite Member" button opens InviteMemberModal (email + role dropdown)
- Pending invitations section shows below members table with cancel buttons
- Members store gains inviteMember, fetchPendingInvitations, cancelInvitation

CompanySwitcher:
- Shows pending invitations greyed out below active companies
- Each with Accept/Decline mini-buttons
- Accepting refreshes bootstrap and switches to new company

NoCompanyView:
- Standalone page for users with zero accepted companies
- Shows pending invitations with Accept/Decline or "no companies" message
- Route: /admin/no-company

Invitation Pinia store:
- Manages user's own pending invitations (fetchPending, accept, decline)
- Bootstrap populates invitations from API response

Global store:
- Bootstrap action stores pending_invitations from response
2026-04-03 23:20:41 +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
dee17a1da8 Rename Roles to Company Roles in settings menu 2026-04-03 22:35:50 +02:00
Darko Gjorgjijoski
4bfec37a53 Switch User Settings from horizontal tabs to sidebar layout
Match the Company Settings design pattern: sidebar navigation on desktop
with dropdown on mobile, child routes rendered via RouterView. Each tab
(General, Profile Photo, Security) is now a BaseSettingCard with its own
route under /admin/user-settings/{general,profile-photo,security}.
2026-04-03 17:41:18 +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
9432da467e Add super-admin Administration section and restructure global vs company settings
- Add Administration sidebar section (super-admin only) with Companies, Users, and Global Settings pages
- Add super-admin middleware, controllers, and API routes under /api/v1/super-admin/
- Allow super-admins to manage all companies and users across tenants
- Add user impersonation with short-lived tokens, audit logging, and UI banner
- Move system-level settings (Mail, PDF, Backup, Update, File Disk) from per-company to Administration > Global Settings
- Convert save_pdf_to_disk from CompanySetting to global Setting
- Add per-company mail configuration overrides (optional, falls back to global)
- Add CompanyMailConfigService to apply company mail config before sending emails
2026-04-03 10:35:40 +02:00
Darko Gjorgjijoski
11024ddc38 Update source file en.json 2026-03-24 09:30:45 +01:00
Darko Gjorgjijoski
3af0e83d26 New translations en.json (Serbian (Latin)) 2026-03-24 07:44:53 +01:00
Darko Gjorgjijoski
a866a26e9f New translations en.json (Swahili) 2026-03-24 07:44:52 +01:00
Darko Gjorgjijoski
154a4dc076 New translations en.json (Malay) 2026-03-24 07:44:51 +01:00
Darko Gjorgjijoski
7900e020f5 New translations en.json (Hindi) 2026-03-24 07:44:50 +01:00
Darko Gjorgjijoski
974efb36da New translations en.json (Latvian) 2026-03-24 07:44:49 +01:00
Darko Gjorgjijoski
45d84b8b3c New translations en.json (Estonian) 2026-03-24 07:44:48 +01:00
Darko Gjorgjijoski
fdfb46ed79 New translations en.json (Croatian) 2026-03-24 07:44:46 +01:00
Darko Gjorgjijoski
ddcef0fb2c New translations en.json (Thai) 2026-03-24 07:44:45 +01:00
Darko Gjorgjijoski
0254b16556 New translations en.json (Bengali) 2026-03-24 07:44:44 +01:00
Darko Gjorgjijoski
a9a52ca85f New translations en.json (Persian) 2026-03-24 07:44:43 +01:00
Darko Gjorgjijoski
708d880b52 New translations en.json (Indonesian) 2026-03-24 07:44:42 +01:00
Darko Gjorgjijoski
57406989d4 New translations en.json (Portuguese, Brazilian) 2026-03-24 07:44:41 +01:00
Darko Gjorgjijoski
5d7e46b026 New translations en.json (Vietnamese) 2026-03-24 07:44:39 +01:00
Darko Gjorgjijoski
8e034e59f8 New translations en.json (Urdu (Pakistan)) 2026-03-24 07:44:38 +01:00
Darko Gjorgjijoski
43951d4542 New translations en.json (Chinese Traditional) 2026-03-24 07:44:37 +01:00
Darko Gjorgjijoski
3cc0f41d8c New translations en.json (Chinese Simplified) 2026-03-24 07:44:36 +01:00
Darko Gjorgjijoski
5f34ae558c New translations en.json (Ukrainian) 2026-03-24 07:44:35 +01:00
Darko Gjorgjijoski
70d51d580d New translations en.json (Turkish) 2026-03-24 07:44:34 +01:00
Darko Gjorgjijoski
b7929672fb New translations en.json (Swedish) 2026-03-24 07:44:32 +01:00
Darko Gjorgjijoski
bd28849a9f New translations en.json (Albanian) 2026-03-24 07:44:31 +01:00
Darko Gjorgjijoski
4d21aadcd4 New translations en.json (Slovenian) 2026-03-24 07:44:30 +01:00
Darko Gjorgjijoski
373a824fdb New translations en.json (Slovak) 2026-03-24 07:44:29 +01:00
Darko Gjorgjijoski
a5d9a60d5c New translations en.json (Russian) 2026-03-24 07:44:28 +01:00
Darko Gjorgjijoski
2537b53506 New translations en.json (Portuguese) 2026-03-24 07:44:27 +01:00