327 Commits

Author SHA1 Message Date
Darko Gjorgjijoski
525f432ee0 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

Backport from v3.0 (e6452946). Adapted for master's existing structure: master uses one-controller-per-action under app/Http/Controllers/V1/Admin/Update/, so the new endpoint is implemented as a dedicated CleanFilesController matching the existing DeleteFilesController/CopyFilesController/etc. pattern instead of v3.0's unified UpdateController. The legacy /update/delete route and DeleteFilesController are retained for compatibility — only the frontend (resources/scripts/admin/views/settings/UpdateAppSetting.vue) is updated to call /update/clean. Updater service lives at app/Space/Updater.php on master (not yet refactored to app/Services/Update/Updater.php like v3.0).
2026-04-07 18:13:44 +02:00
mchev
f17c7be5f0 Merge pull request #611 from klittle81/EnhanceExpenseReport
Enhance Expense Report - Grouped itemized Expenses By Expense Category
2026-04-07 12:58:09 +02:00
mchev
0e9f18d4d1 fix: i18n for expense report PDF and correct report controller return types
Add expenses.uncategorized and pdf_expense_group_total_label; use the new key
in the grouped expense template; document View|Response instead of JsonResponse.

Made-with: Cursor
2026-04-07 10:43:02 +02:00
mchev
e22050bc71 fix: use DomPDF Pdf facade and Pint style in expense report
Replace legacy PDF facade alias with Barryvdh\DomPDF\Facade\Pdf so CI Pint passes.

Made-with: Cursor
2026-04-07 10:31:48 +02:00
mchev
af9d672574 Bump version from 2.3.1 to 2.3.2 2026-04-06 11:02:23 +02:00
mchev
7606f8ece8 Merge pull request #585 from InvoiceShelf/translations
New Crowdin updates
2026-04-06 11:00:58 +02:00
mchev
9b0498a2e5 Merge pull request #583 from sirlupusdev/fix-setup-wizard
Fix: Set Slug when creating/updating first company
2026-04-06 10:54:57 +02:00
mchev
04c7682e73 Merge pull request #584 from sirlupusdev/feat-auto-due-date
Feat: Automatically set due date when invoice date is changed
2026-04-06 10:50:05 +02:00
Darko Gjorgjijoski
88650c2f3e Bump version 2026-04-05 12:34:24 +02:00
Darko Gjorgjijoski
ee76f31138 Add log mail driver support to frontend
The default mail driver in config/mail.php is 'log', which had no
matching Vue component, causing the mail configuration step in the
install wizard (and settings page) to render empty.
2026-04-05 12:33:14 +02:00
Darko Gjorgjijoski
e1af9f56c4 Docker optimizations 2026-04-05 12:07:47 +02:00
mchev
fdd860c381 Merge pull request #612 from mchev/master
Ensure public/storage symlink exists in Docker production entrypoint
2026-04-04 18:57:09 +02:00
klittle81
834b53ea40 Enhance Expense Report - Grouped itemized expenses 2026-04-04 11:22:32 -04:00
Darko Gjorgjijoski
5ff051fbb5 Bump version from 2.2.1 to 2.3.0 2026-04-04 17:12:13 +02:00
Darko Gjorgjijoski
0d7059fcf6 Fix logout/re-login CSRF mismatch and stale token issues
Cherry-picked from v3.0 branch. Three fixes:
1. Refresh CSRF cookie after logout (auth.js)
2. Clear auth.token and selectedCompany from localStorage on logout (auth.js)
3. Invalidate session and regenerate CSRF token on server-side logout (web.php)

Without these, logging out and back in as a different user would fail
with CSRF token mismatch and 401 Unauthenticated errors because the
browser held stale session cookies and localStorage tokens.
2026-04-03 23:53:56 +02:00
Darko Gjorgjijoski
7d9fdb79cc Scope users listing and search to current company (#607)
Add scopeWhereCompany() to User model using whereHas through the
user_company pivot table. Apply it in UsersController::index() and
SearchController so users only see members of their current company.

Previously, the users page showed ALL users across all companies.

Ref #574
2026-04-03 14:34:33 +02:00
Darko Gjorgjijoski
3d871604ae Add company ownership check to clone endpoints (#606)
Verify the source record belongs to the current company before cloning.
Previously, users could clone invoices/estimates from other companies,
leaking sensitive data (amounts, customer details, items, taxes, notes).

The view policy already includes hasCompany() check, so authorizing
view on the source record gates both ability and company ownership.

Ref #574
2026-04-03 14:32:12 +02:00
Darko Gjorgjijoski
1adebe85b9 Scope all bulk deletes to current company and fix inverted ownership transfer (#605)
Bulk delete: filter IDs through whereCompany() before deleting in all
controllers (Invoices, Payments, Items, Expenses, Estimates, Recurring
Invoices). Previously, any user could delete records from other companies
by providing cross-company IDs.

Transfer ownership: fix inverted hasCompany() check that allowed
transferring company ownership to users who do NOT belong to the company,
while blocking users who DO belong.

Ref #567
2026-04-03 14:16:42 +02:00
Darko Gjorgjijoski
defbfc6406 Fix CustomerPolicy missing hasCompany() check (IDOR) (#604)
* Fix CustomerPolicy missing hasCompany() check (cross-company IDOR)

Add $user->hasCompany($customer->company_id) check to view, update,
delete, restore, and forceDelete methods in CustomerPolicy, matching
the pattern used by all other policies (InvoicePolicy, PaymentPolicy,
EstimatePolicy, etc.).

Without this check, a user in Company A with view-customer ability
could access customers belonging to Company B by providing the target
customer's ID.

Add cross-company authorization tests to verify the fix.

Closes #565

* Scope bulk delete to current company to prevent cross-company deletion

Filter customer IDs through whereCompany() before passing to
deleteCustomers(), ensuring users cannot delete customers belonging
to other companies via the bulk delete endpoint.
2026-04-03 13:56:34 +02:00
Darko Gjorgjijoski
25986b7bd5 Update IDE helpers, Tailwind skill to v4, and refresh dependencies 2026-04-02 21:03:55 +02:00
Darko Gjorgjijoski
751bd4a1c8 Upgrade ChartJS from v2 to v4 (#603) 2026-04-02 20:02:49 +02:00
Darko Gjorgjijoski
0e313b80ca Upgrade @vueuse/core from v12 to v14 (#602) 2026-04-02 18:09:43 +02:00
Darko Gjorgjijoski
5014a75fbc Upgrade eslint tooling to v10 and fix linting bugs (#601)
- Upgrade eslint 9→10, eslint-config-prettier 9→10, eslint-plugin-vue 9→10
- Upgrade @types/node 20→24
- Migrate from legacy .eslintrc.mjs to flat config eslint.config.mjs
- Remove --ext flag from npm test script (dropped in eslint 10)
- Fix vue/no-ref-as-operand: add missing .value to ref assignments (5 files)
- Fix vue/return-in-computed-property: add default returns (2 files)
- Fix vue/no-side-effects-in-computed-properties: move mutation to watcher
- Fix vue/no-dupe-keys: remove ref shadowing prop in DomPDFDriver
- Fix vue/no-deprecated-slot-attribute: migrate to v-slot syntax (3 files)
- Fix vue/require-valid-default-prop: use factory function for array default
- Fix vue/no-unused-vars: remove unused slot destructure
- Disable vue/no-mutating-props (false positive for Pinia store props)
2026-04-02 17:33:18 +02:00
Darko Gjorgjijoski
9e5b9fdaad Upgrade vue-flatpickr-component from v11 to v12 (#600) 2026-04-02 17:17:17 +02:00
Darko Gjorgjijoski
08dfe62312 Standardize Node.js version to 24 (#599)
Update Node.js from 20 to 24 across CI workflows, Dockerfiles,
package.json engines field, and add .node-version file for consistent
local development.
2026-04-02 17:08:39 +02:00
Darko Gjorgjijoski
f623cd0179 Upgrade vue-router from v4 to v5 (#598)
- Migrate beforeEach navigation guard from next() callback to return-based
  API, preparing for vue-router v6 where next() is removed
2026-04-02 16:47:16 +02:00
mchev
77fd96d499 Merge branch 'master' of https://github.com/mchev/InvoiceShelf 2026-04-02 16:43:57 +02:00
mchev
2e4e19dfc5 Remove testing image 2026-04-02 16:43:22 +02:00
mchev
76c02be219 Merge branch 'InvoiceShelf:master' into master 2026-04-02 16:42:13 +02:00
Darko Gjorgjijoski
414531524c Remove unused cross-env dependency 2026-04-02 16:39:10 +02:00
Darko Gjorgjijoski
d75a957183 Upgrade Tiptap from v2 to v3 (#597)
- Upgrade @tiptap/core, starter-kit, vue-3, pm, extension-text-align to v3
- Remove @tiptap/extension-link (now bundled in StarterKit v3)
- Move Link config into StarterKit.configure()
2026-04-02 16:35:43 +02:00
Darko Gjorgjijoski
63d3a7fc8e Skip PHP CI jobs when only non-PHP files change
Replace workflow-level paths-ignore with per-job filtering using
dorny/paths-filter. PHP lint and test jobs now only run when PHP-related
files (app/, config/, database/, routes/, tests/, composer.*, phpunit.xml)
are modified.
2026-04-02 16:35:07 +02:00
Darko Gjorgjijoski
0be747a483 Pin axios to 1.14.0
Avoid possible supply chain attacks in future caused by compromised
author account or even rogue author.
2026-04-02 16:14:47 +02:00
Darko Gjorgjijoski
3ceb08bc31 Upgrade Pinia from v2 to v3 (#596)
Migrate all 37 store definitions from the deprecated object-with-id
signature to the string-id-first signature required by Pinia 3:

  defineStore({ id: 'name', ... }) → defineStore('name', { ... })
2026-04-02 16:12:11 +02:00
Darko Gjorgjijoski
ad5a7e51b9 Upgrade to Vite 8 and Tailwind CSS 4 (#595)
- Vite 6 → 8 (Rolldown bundler), laravel-vite-plugin 1 → 3, @vitejs/plugin-vue 5 → 6
- Tailwind CSS 3 → 4 with CSS-based config (@theme, @plugin, @utility)
- Add @tailwindcss/vite plugin, remove postcss/autoprefixer/sass
- Convert SCSS files to plain CSS (resources/sass → resources/css)
- Migrate tailwind.config.js to CSS @theme directives
- Rename deprecated utility classes (shadow-sm→shadow-xs, outline-none→outline-hidden,
  rounded-sm→rounded-xs, bg-gradient-to→bg-linear-to, ring→ring-3)
- Migrate opacity utilities to color modifiers (bg-opacity, text-opacity,
  border-opacity, ring-opacity → color/N syntax)
- Update primary color CSS vars to full rgb() values for TW4 color-mix()
- Fix border-l color specificity for sidebar navigation (TW4 default border
  color changed from gray-200 to currentColor)
- Fix invalid border color classes (border-grey-light, border-modal-bg, border--200)
- Add @reference directive for @apply in Vue component style blocks
- Convert Vue component <style lang="scss"> blocks to plain CSS
2026-04-02 15:59:15 +02:00
Darko Gjorgjijoski
691178857f Add HTTP client wrapper and upgrade Axios to v1 (#594)
* refactor: add HTTP client wrapper and upgrade axios to v1

Introduce a thin HTTP wrapper (resources/scripts/http) that centralizes
axios configuration, interceptors, and auth header injection. All 43
files now import from the wrapper instead of axios directly, making
future library swaps a single-file change. Upgrade axios from 0.30.0
to 1.14.0.

* fix: restore window.Ls assignment removed during axios refactor

company.js uses window.Ls.set() to persist selected company,
which broke after the axios plugin (that set window.Ls) was deleted.
2026-04-02 15:08:23 +02:00
Darko Gjorgjijoski
a38f09cf7b Installer reliability improvements (#593)
* docs: add CLAUDE.md for Claude Code guidance

* fix: handle missing settings table in installation middlewares

RedirectIfInstalled crashed with "no such table: settings" when the
database_created marker file existed but the database was empty.
Changed to use isDbCreated() which verifies actual tables, and added
try-catch around Setting queries in both middlewares.

* feat: pre-select database driver from env in installation wizard

The database step now reads DB_CONNECTION from the environment and
pre-selects the matching driver on load, including correct defaults
for hostname and port.

* feat: pre-select mail driver and config from env in installation wizard

The email step now fetches the current mail configuration on load
instead of hardcoding the driver to 'mail'. SMTP fields fall back
to Laravel config values from the environment.

* refactor: remove file-based DB marker in favor of direct DB checks

The database_created marker file was a second source of truth that
could drift out of sync with the actual database. InstallUtils now
checks the database directly via Schema::hasTable which is cached
per-request and handles all error cases gracefully.
2026-04-02 14:48:08 +02:00
mchev
de4ba6bba0 Fix storage link on docker 2026-04-02 11:56:16 +02:00
mchev
375cfc6b18 Merge pull request #591 from mchev/588
Fix PDF notes line breaks
2026-04-01 21:38:14 +02:00
mchev
aa88dc340d Closes #588 2026-04-01 21:30:32 +02:00
mchev
7004bf375e Merge pull request #587 from rihards-simanovics/rihards-simanovics/issue586
Fix docker containers failing to create SQL backups due to missing OS dependencies
2026-03-27 08:15:43 +01:00
mchev
80889293bf Merge pull request #508 from alexdev01012020/email-backup
Overrite the email notification for backup
2026-03-27 08:04:02 +01:00
Rihards Simanovics
d754c4d29e fix: return missing development docker sql dependencies
Fixes #586
2026-03-27 00:18:15 +00:00
Rihards Simanovics
8de32e27d3 fix: return missing production docker sql dependencies
Fixes #586
2026-03-27 00:17:56 +00:00
Darko Gjorgjijoski
0f933b6217 New translations en.json (Hindi) 2026-03-26 19:47:36 +01:00
mchev
3bae2c282c Merge pull request #576 from csalzano/fix/remote-disk-backup-listing
Fix remote disk backups never appear in backup listing
2026-03-26 09:16:13 +01:00
Corey Salzano
14b5aaa0c9 Runs pint 2026-03-25 09:09:33 -04:00
lupus0802
241ec09220 Feat: Automatically set due date when invoice date is changed 2026-03-25 13:06:39 +01:00
lupus0802
ed7af3fc3c Fix: Set Slug when creating/updating first company 2026-03-25 13:05:23 +01:00
Corey Salzano
aafcf147cf Updates the "create backup" test to handle the disk prefix. 2026-03-24 23:00:47 -04:00
mchev
7e8f9e65fb Merge pull request #580 from InvoiceShelf/translations
New Crowdin updates
2026-03-24 12:27:36 +01:00
mchev
67739750ca Merge pull request #582 from mchev/master
Hot fix  #280
2026-03-24 12:26:27 +01:00
mchev
5f6a7b92bf Merge pull request #581 from swiffer/master
Import File facade in UpdateCommand
2026-03-24 12:26:00 +01:00
mchev
ff3cab570a Hot fix #280 2026-03-24 12:13:40 +01:00
Darko Gjorgjijoski
11024ddc38 Update source file en.json 2026-03-24 09:30:45 +01:00
Matthias Wirtz
71303a1050 Import File facade in UpdateCommand
Added use statement for File facade.
2026-03-24 08:03:53 +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
Darko Gjorgjijoski
bbc2d2088a New translations en.json (Polish) 2026-03-24 07:44:25 +01:00
Darko Gjorgjijoski
5734a06ab3 New translations en.json (Norwegian) 2026-03-24 07:44:24 +01:00
Darko Gjorgjijoski
75a1038245 New translations en.json (Dutch) 2026-03-24 07:44:23 +01:00
Darko Gjorgjijoski
017691b7b1 New translations en.json (Macedonian) 2026-03-24 07:44:22 +01:00
Darko Gjorgjijoski
d4f8a40982 New translations en.json (Lithuanian) 2026-03-24 07:44:21 +01:00
Darko Gjorgjijoski
ebdee1e3f0 New translations en.json (Georgian) 2026-03-24 07:44:20 +01:00
Darko Gjorgjijoski
ce97a474f9 New translations en.json (Japanese) 2026-03-24 07:44:18 +01:00
Darko Gjorgjijoski
4ee3d0d129 New translations en.json (Italian) 2026-03-24 07:44:17 +01:00
Darko Gjorgjijoski
bd5abdec0e New translations en.json (Hungarian) 2026-03-24 07:44:16 +01:00
Darko Gjorgjijoski
995533974e New translations en.json (Hebrew) 2026-03-24 07:44:15 +01:00
Darko Gjorgjijoski
156e94f931 New translations en.json (Finnish) 2026-03-24 07:44:14 +01:00
Darko Gjorgjijoski
e38c4963ef New translations en.json (Greek) 2026-03-24 07:44:13 +01:00
Darko Gjorgjijoski
d7efbb5c47 New translations en.json (German) 2026-03-24 07:44:11 +01:00
Darko Gjorgjijoski
3cd219e7b3 New translations en.json (Danish) 2026-03-24 07:44:10 +01:00
Darko Gjorgjijoski
3d65a09038 New translations en.json (Czech) 2026-03-24 07:44:09 +01:00
Darko Gjorgjijoski
f52c376a49 New translations en.json (Catalan) 2026-03-24 07:44:08 +01:00
Darko Gjorgjijoski
5a7c1363ee New translations en.json (Bulgarian) 2026-03-24 07:44:07 +01:00
Darko Gjorgjijoski
2c40c2ef06 New translations en.json (Arabic) 2026-03-24 07:44:06 +01:00
Darko Gjorgjijoski
b5ff16c318 New translations en.json (Spanish) 2026-03-24 07:44:04 +01:00
Darko Gjorgjijoski
4d7b818d23 New translations en.json (French) 2026-03-24 07:44:03 +01:00
Darko Gjorgjijoski
b314ebc07f New translations en.json (Romanian) 2026-03-24 07:44:02 +01:00
mchev
254ed5ade0 Merge pull request #579 from mchev/upgrade_requirements
PHP8.4 requirement
2026-03-24 07:15:33 +01:00
mchev
40e4ea931e PHP8.4 requirement 2026-03-24 07:07:41 +01:00
mchev
030c13b67a Merge pull request #578 from mchev/updates
Laravel 13 upgrade, security updates and fixes
2026-03-24 06:36:47 +01:00
mchev
28fedead6c Merge pull request #566 from InvoiceShelf/translations
New Crowdin updates
2026-03-24 06:14:11 +01:00
mchev
14ccce1934 Merge pull request #572 from klittle81/payment-pdf-invoice-details
Update receipt PDF to include Invoice Total, Balance Due, and Invoice Status
2026-03-24 06:13:15 +01:00
mchev
2d808f17bb Merge pull request #573 from csalzano/fix/dropbox-file-disk-creation
Bug fix in Dropbox file disk creation. Applies disk path.
2026-03-24 05:32:44 +01:00
mchev
6d227fa368 Merge pull request #554 from csalzano/fix/paginated-table-stale-response
Fix wrong customer names on invoice list page 2+
2026-03-23 20:18:26 +01:00
mchev
07757e747e Addresses SSRF risk 2026-03-21 19:14:51 +01:00
mchev
d4e19646ee fix(ci): install deps on PHP 8.4 (Symfony 8 requires >=8.4) 2026-03-21 19:03:37 +01:00
mchev
c901114fc0 Pint 2026-03-21 18:59:53 +01:00
mchev
186ab35fd4 Laravel 13 upgrade, updates and fixes 2026-03-21 18:53:33 +01:00
Corey Salzano
3c31baf20d fix(backup): remote disk backups never appear in backup listing
Three bugs prevented backups stored on remote disks (Dropbox, S3, etc.)
from ever appearing in the Settings > Backup listing:

1. Typo in BackupSetting.vue: `filed_disk_id` was sent to the API
   instead of `file_disk_id`, so the backend never received the selected
   disk ID and always fell back to the local filesystem.

2. Wrong default disk selection in loadDisksData(): `set_as_default == 0`
   selected the first disk that is NOT default (local_public), instead of
   the disk that IS default. Changed to `set_as_default == 1` with a
   fallback to the first disk.

3. BackupsController::index() did not call setConfig() on the FileDisk
   before querying backups, so even when the correct file_disk_id arrived
   it still read from the default local filesystem. Added the same disk
   bootstrap logic already present in CreateBackupJob and destroy().

Made-with: Cursor
2026-03-17 19:32:40 -04:00
Darko Gjorgjijoski
c7485930a8 New translations en.json (Romanian) 2026-03-15 18:17:11 +01:00
Darko Gjorgjijoski
c7d06671d6 New translations en.json (Romanian) 2026-03-15 17:11:09 +01:00
Corey Salzano
b35bd0880a Bug fix in Dropbox file disk creation. Applies disk path. 2026-03-03 11:47:55 -05:00
klittle81
c70e2abf8a Apply suggestion from @klittle81
remove the <br> at end as its not needed
2026-02-27 22:43:40 -05:00
klittle81
fc05cf61fa Patch to update reciept PDF generated by payment.blade.php to include Invoice Total, Balance Due, and Invoice Status 2026-02-27 22:06:27 -05:00
Rihards Simanovičs
48abd9020d docs(SECURITY.md): revise security vulnerability reporting instructions
Updated contact information to include GWS security email and added Discord outreach option for reporting vulnerabilities.
2026-02-27 16:35:06 +00:00
Darko Gjorgjijoski
9afd4011a6 New translations en.json (Hindi) 2026-02-25 12:16:09 +01:00
Corey Salzano
01528d9ebe Fix wrong customer names on invoice list page 2+
On the invoices list, when viewing page 2 or later, customer names in the
table could be wrong—opening an invoice showed a different customer than
the one displayed in that row.

Ensure we only apply API responses that match the currently requested page.
This prevents stale or out-of-order responses from overwriting the displayed
data. Also use row id as v-for key for correct Vue reconciliation.
2026-02-16 11:57:36 -05:00
Alex
a386fd19fc Merge remote-tracking branch 'upstream/master' into email-backup 2026-02-15 12:37:13 +02:00
Darko Gjorgjijoski
d20f1aa105 New Crowdin updates (#539)
* Update source file en.json

* New translations en.json (Slovak)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Georgian)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovenian)

* New translations en.json (Albanian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Urdu (Pakistan))

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Estonian)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Malay)

* New translations en.json (Swahili)

* New translations en.json (Serbian (Latin))

* Update source file en.json
2026-02-08 17:34:06 +01:00
Alex
e5f6984078 Apply review feedback 2026-02-06 16:17:57 +02:00
Abdulrazzaq Alhendi
65d1fdd3f0 feat(mail): add CC and BCC fields to email requests and forms (#466)
* feat(mail): add CC and BCC fields to email requests and forms

* chore: fmt
2026-02-06 01:59:38 +01:00
Darko Gjorgjijoski
796f6f364a New Crowdin updates (#481)
* New translations en.json (Norwegian)

* New translations en.json (Croatian)

* New translations en.json (French)

* New translations en.json (Arabic)

* New translations en.json (Dutch)

* New translations en.json (Dutch)

* New translations en.json (Dutch)

* New translations en.json (Chinese Simplified)

* New translations en.json (Dutch)

* New translations en.json (Spanish)

* New translations en.json (German)

* New translations en.json (German)

* New translations en.json (Romanian)

* New translations en.json (Dutch)

* New translations en.json (Romanian)

* New translations en.json (Romanian)

* New translations en.json (Swedish)

* New translations en.json (Bulgarian)

* New translations en.json (Bulgarian)

* New translations en.json (Bulgarian)

* New translations en.json (Bulgarian)

* New translations en.json (Slovenian)

* New translations en.json (Slovenian)

* New translations en.json (Italian)

* New translations en.json (Bengali)

* New translations en.json (Slovak)
2026-02-06 01:53:49 +01:00
Radoš
61b345d473 chore: lang - cs.json (#490)
Wrong translation for tax_id
2026-02-06 01:44:03 +01:00
Darko Gjorgjijoski
a30b2e751b Pin PHP version to 8.4 in development Dockerfile 2026-02-06 00:57:13 +01:00
Devstack
af205acb75 Fix aggregates for customers using withSum() (SQL-portable, no DB mode tweaks) (#486)
* Update CustomersController.php

Fix: replace GROUP BY + SUM join with withSum() to avoid ONLY_FULL_GROUP_BY; no API changes (same aliases)

* Update CustomersController.php

style: apply Laravel Pint formatting
2026-01-01 22:43:52 +01:00
Ahmed Ashraf
24546aea3c New currency: Qatari Riyal (#476)
* add qatari riyal currency to migration and seeder

* run pint
2026-01-01 17:50:31 +01:00
Alex
d2953e9409 Overrite the email notification for backup 2025-11-05 19:14:55 +02:00
Darko
935fe06f9e Bump version 2025-09-19 15:50:54 +02:00
Darko Gjorgjijoski
18d63a3375 Configurations cleanup & database configurations for mail and pdfs (#479)
* Move Mail, PDF configuration to Database, standardize configurations

* Set default currency to USD on install

* Pint code
2025-09-19 15:42:53 +02:00
Darko Gjorgjijoski
3da86965e1 New Crowdin updates (#454)
* New translations en.json (French)

* New translations en.json (Norwegian)

* New translations en.json (Norwegian)

* New translations en.json (Norwegian)

* New translations en.json (Norwegian)

* New translations en.json (Norwegian)

* New translations en.json (Norwegian)

* New translations en.json (Italian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Japanese)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Danish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Georgian)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))

* New translations en.json (Urdu (Pakistan))

* New translations en.json (Bengali)

* New translations en.json (Estonian)

* New translations en.json (Malay)

* New translations en.json (Swahili)

* New translations en.json (Portuguese)

* New translations en.json (Albanian)

* Update source file en.json
2025-09-13 16:42:20 +02:00
Darko Gjorgjijoski
5aa54dbd86 Fix docker asset url (#472) 2025-09-08 17:52:33 +02:00
Darko Gjorgjijoski
58b262fe32 Fix path to sqlite.empty.db in setup script 2025-09-08 14:19:59 +02:00
lupus
f8c4e63b3f Fix: broken file links for development files (#469)
* FIx broken file links for development files

* Fix: Compose Files Context / Paths / Networks
2025-09-07 14:16:39 +02:00
Darko Gjorgjijoski
78dfdbe162 Devenv subcommands for test / format (#462)
* Add devenv subcommands for test / format

* Rename dev-env-config to devenvconfig for consitency sake
2025-09-02 03:32:21 +02:00
Darko Gjorgjijoski
770da45dbf Production docker improvements (#463)
* Update production dockerfiles for testing

* Add bash, nano and remove unecessary sqlite3 alpine package
2025-09-02 03:27:26 +02:00
Christos Yiakoumettis
3e96297699 Add expense number at Expenses (#406)
* add expense number at expenses

* Re-order expense fields

* Rename expense_number migration

* Add expense_number to tests

---------

Co-authored-by: Darko Gjorgjijoski <dg@darkog.com>
2025-09-02 03:20:27 +02:00
Rihards Simanovičs
f3e49d3044 Temperately hide modules tab while Module Management is developed (#443)
* fix(navigation): temperately hide modules tab while Module Management is developed

* chore run pint
2025-09-02 01:30:53 +02:00
Darko Gjorgjijoski
05ab78942c Update JS dependencies 2025-09-01 20:58:43 +02:00
Rihards Simanovičs
827436af15 chore: Ignore devcontainer dir (#460) 2025-09-01 13:00:38 +02:00
Darko Gjorgjijoski
233d3cb989 Update readme.md 2025-09-01 12:07:15 +02:00
Darko Gjorgjijoski
8842d6a626 Development Environment (#459)
* Fix SQLite docker build related issues

* Add devenv for development
2025-09-01 11:47:58 +02:00
Darko Gjorgjijoski
f1635bcef8 Fix SQLite docker build related issues (#458) 2025-09-01 02:42:07 +02:00
Darko Gjorgjijoski
3d327a1735 Add SQLite command line utility to docker images 2025-08-31 21:35:15 +02:00
Darko Gjorgjijoski
a6ce294497 Update sqlite path 2025-08-31 20:57:45 +02:00
Darko Gjorgjijoski
1998d15b25 Fix repository name 2025-08-31 16:18:29 +02:00
Darko Gjorgjijoski
f47b6d51f2 GitHub Actions Tweaks (#457)
* Remove PHP 8.2 from tests

* Fix docker hub repository name

* Improve action labels

* Ignore .github folder from check CI
2025-08-31 16:13:18 +02:00
Darko Gjorgjijoski
23f6b1877f 🚢 Simplified docker builds (#456)
* Simplify docker builds

* Ignore docker and frontend scripts from PHP related checks

* Update docker development setup
2025-08-31 15:07:22 +02:00
Darko Gjorgjijoski
3ed91545d1 Bump version 2025-08-31 03:27:33 +02:00
Calcen
c035c834d4 Fix: Weird gap in invoice items description (#439)
- Ensure invoice item display is block for better layout. 
- Gets rid of the weird extra gap between last line and the line above it.
2025-08-31 03:23:58 +02:00
lupus
4f34ca783b Fix: Actually display company currency symbol / total receipts for customer charts (#453)
* Fix: Actually display company currency symbol for customer charts

Related to draft #403.
The mentioned pull request was incomplete, the current state would show amounts in base currency but not use the base/company currency symbols/notation. This change addresses the issue.

* Fix: Use totalReceipts for "Receipts" value
2025-08-31 03:13:16 +02:00
Darko Gjorgjijoski
bae8dbe083 Upgrade mail configuration (#455)
* Upgrade the mail configuration

* Update mail configuration to match Laravel 12

* Update mail configuration to properly set none or null

* Pint code

* Upgrade Symfony Mailers
2025-08-31 03:04:31 +02:00
Tim van Osch
d1bca362de chore: Improve .dev dockerfile for development and production (#183)
* chore: update dockerfile and dev env

* chore(dockerfile): fix user/group id args

* chore(docker): use php-fpm w/ separate nginx

* chore(docker): add nginx image w/ static files

* chore(docker): build vite resources only once, bump vite minor version,
add watch yarn command.
By using --buildplatform tag in the dockerfile we can have the vite step
be built only on the host platform, which significantly speeds it up.
This is possible since the build assets aren't platform dependant.

* Move dockerfiles to .dev
2025-08-31 00:46:56 +02:00
lupus
d5137e393d Fix: Use amounts in base currency for customer charts (#403)
Previously, the customer chart used the total/amount fields to calculate net profits/expenses/etc.
If the currency the expense (for example) was created in differed from the base currency of the company, the chart would display wrong amounts.
This change addresses the issue by always using the base currency field.
2025-08-30 12:30:49 +02:00
Darko Gjorgjijoski
20caf7ef5b Add missing mailgun-mailer package for Mailgun email driver (#452) 2025-08-30 12:14:20 +02:00
Fabio Ribeiro
e8e01a706e Fix: Create item with tax (ItemModal) (#385)
The issue was found during an Item creation inside the Invoice,
Estimates or Recurring Invoice, the same fix that was applied into the
Item creation view, now is needed into ItemModal. The root cause is that
price + tax returns an amount as float making the database fail.

Relates #377
2025-08-30 11:38:04 +02:00
Darko Gjorgjijoski
2f8c98003d Fix language file conflicts (#451) 2025-08-30 01:36:13 +02:00
Darko Gjorgjijoski
02701db815 New Crowdin updates (#368)
* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Turkish)

* New translations en.json (Indonesian)

* New translations en.json (Russian)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))

* New translations en.json (Indonesian)

* New translations en.json (Catalan)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (German)

* New translations en.json (Persian)

* New translations en.json (Persian)

* New translations en.json (Persian)

* New translations en.json (Slovak)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Indonesian)

* New translations en.json (Indonesian)

* New translations en.json (Indonesian)

* New translations en.json (Indonesian)

* New translations en.json (German)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Arabic)

* New translations en.json (German)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Hindi)

* New translations en.json (Hindi)

* New translations en.json (Slovenian)

* New translations en.json (Italian)

* New translations en.json (German)

* New translations en.json (German)

* New translations en.json (Italian)

* New translations en.json (Italian)

* New translations en.json (Italian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Japanese)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))

* Update source file en.json

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Croatian)

* New translations en.json (Danish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Georgian)

* New translations en.json (Urdu (Pakistan))

* New translations en.json (Bengali)

* New translations en.json (Estonian)

* New translations en.json (Malay)

* New translations en.json (Swahili)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Portuguese)

* New translations en.json (Albanian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Portuguese)

* New translations en.json (Chinese Simplified)
2025-08-30 01:24:36 +02:00
Darko Gjorgjijoski
b7f17f2d14 Add common languages (#448)
* Update languages

* Update language list
2025-08-28 15:35:10 +02:00
Honza Raclavský
cf1d5e7324 Fix deprecated i18n api (#398) 2025-08-28 15:28:42 +02:00
Darko Gjorgjijoski
a40bf5840d Dynamically load language files (#446) 2025-08-28 15:19:51 +02:00
Darko Gjorgjijoski
32f7bc053a New currencies: Paraguayan Guaraní, Algerian Dinar (#447)
* feat(currency): add Algerian Dinar (DZD) support (#395)

* Add Paraguayan Guarany (PYG) currency  (#434)

* Adding Paraguayan currency, closes #404

* Adding the currency to the seeder too

* If the data was already seeded, don't add the entry

* Pint

---------

Co-authored-by: Darko Gjorgjijoski <5760249+gdarko@users.noreply.github.com>

* Add DZD currency to the currencies seeder

---------

Co-authored-by: Polat İnceler <inceler.polat@gmail.com>
Co-authored-by: mchev <martin.chevignard@gmail.com>
2025-08-28 14:13:03 +02:00
Honza Raclavský
a006b07be5 fixed czech translates (#399) 2025-08-28 13:10:48 +02:00
Yes-Sebastian
a44303a370 Update de.json (#414)
Grammar in the German language is a bit tricky. The past tense has been corrected here.
2025-08-28 13:10:02 +02:00
Fabio Ribeiro
d69a56e2d5 feat: Tax included (#370)
* feat: Tax included

* Added a toggle switch in tax settings to enable the feature.
* Database migration adding tax_included field into estimates, invoices
  and recurring invoices table.
* Toggle switch to enable and store the tax_included by estimates,
  invoices and recurring invoices.
* In case of tax included enabled, total taxes will be recalculated and
  the invoices, estimates and recurring invoices total won't be sum with
  taxes.
* Apply tax included when discount_per_item/tax_per_item item is enabled.
* Custom component to show the net total when tax included is enabled.
* Update invoice and estimates pdfs with net total.

* chore: Tax included by default

A switch button inside the tax settings to enable the tax included by
default in invoices, estimates and recurring invoices.
2025-08-28 10:28:24 +02:00
Darko Gjorgjijoski
08e1bb2e22 Exclude .git directory from backups (#445)
* Exclude .git directory from backups

* Fix formatting
2025-08-28 10:02:06 +02:00
Darko Gjorgjijoski
29c15116bc Fix Adminer docker build (#444) 2025-08-28 10:01:50 +02:00
Loduis Madariaga Barrios
8e96d3e972 fix(csrf-token): add leading dot to session domain cookie. (#224)
* fix(csrf-token): add leading dot to session domain cookie.

* refactor: remove generate key, upgrade axios and keep session domain in null.

* refactor: fix PSR-12 code styles for PHP 8.2 compatibility.

---------

Co-authored-by: Darko Gjorgjijoski <5760249+gdarko@users.noreply.github.com>
2025-08-28 09:44:34 +02:00
Fabio Ribeiro
bf0d98c69c fix: Backup Job (#375)
Since `laravel-backup` major version was updated 8 to 9, the backup
ability was compromised, the main reason is the change on the method
contract from `BackupFactory` that now the `createFromArray` no longer
exists.
2025-06-11 23:23:19 +02:00
Fabio Ribeiro
73d4ac1eb1 fix: Payment confirmation error (#376)
Error caused when using Payments module, when try Stripe redirects back
to InvoiceShelf, and the module calls the InvoiceShelf `generatePayment`.

Relates #369
2025-06-11 22:56:56 +02:00
Leo
e832c7661a Fix: Heroicons v1 leftovers (#374)
Change the getOrderBy's button. 
Using Heroicons v2

Change the SortAscendingIcon to BarsArrowUpIcon
Change the SortDescendingIcon to BarsArrowDownIcon
2025-06-11 22:48:01 +02:00
Fabio Ribeiro
b962bc9227 fix: Create item with tax (#377)
When the `tax-per-item` is enabled and also has a tax added, during the
item creation, the price + tax returns an amount as float making the
database fail. To fix this was necessary to round the result.
2025-05-22 10:48:34 +02:00
mchev
6d14dce668 Improving github workflow (#278)
* Improving workflow, updating dependencies and test pint with PHP 8.3, adding php 8.4 on tests

* Fix cache issue

* Not caching since it is not working
2025-05-04 15:22:43 +02:00
mchev
1ff220f0d8 Upgrading to Laravel 12 (#346)
* Upgrading to Laravel 12

* Upgrade lockfile

* Keep the old local filesystem driver base path

---------

Co-authored-by: Darko Gjorgjijoski <5760249+gdarko@users.noreply.github.com>
Co-authored-by: Darko Gjorgjijoski <dg@darkog.com>
2025-05-04 11:04:01 +02:00
Darko Gjorgjijoski
2e77a76c7b New Crowdin updates (#345)
* New translations en.json (Portuguese, Brazilian)

* Update source file en.json

* New translations en.json (Czech)

* New translations en.json (Indonesian)

* New translations en.json (Lithuanian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))

* New translations en.json (Serbian (Latin))

* New translations en.json (Ukrainian)

* New translations en.json (Ukrainian)

* New translations en.json (Ukrainian)

* New translations en.json (Ukrainian)

* New translations en.json (Ukrainian)

* New translations en.json (Ukrainian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Turkish)

* New translations en.json (Romanian)

* New translations en.json (Indonesian)

* New translations en.json (Russian)

* New translations en.json (Turkish)

* New translations en.json (Romanian)

* New translations en.json (Indonesian)

* New translations en.json (Russian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))

* Update source file en.json

* New translations en.json (Turkish)

* New translations en.json (Romanian)

* New translations en.json (Indonesian)

* New translations en.json (Russian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))
2025-05-04 02:50:56 +02:00
mchev
bf5b544ca3 Adding Flat Tax support with fixed amount (#253)
* Possibility to set a fixed amount on tax types settings

* Pint and manage flat taxes on items

* Fix display errors and handle global taxes

* Tests

* Pint with PHP 8.2 cause with PHP 8.3 version it cause workflow error

* Merging percent and fixed amount into one column

* Now display the currency on SelectTaxPopup on fixed taxes
2025-05-04 02:24:56 +02:00
Darko Gjorgjijoski
546f75d3a6 Pint updated files (#367) 2025-05-04 02:23:51 +02:00
Tim van Osch
bf40f792c2 Feat(Gotenberg): Opt-in alternative pdf generation for modern CSS (#184)
* WIP(gotenberg): add pdf generation abstraction and UI

* feat(pdf): settings validate(clien+server) & save

* fix(gotenberg): Use correct default papersize
chore(gotengberg): Remove unused GOTENBERG_MARGINS env from .env

* style(gotenberg): fix linter/styling issues

* fix(pdf): use pdf config policy

* fix: revert accidental capitalization in mail config vue

* Update composer, remove whitespace typo

* Fix small typos

* fix cookie/env issue

* Add gotenberg to .dev, move admin menu item up
2025-05-04 02:10:15 +02:00
Fabio Ribeiro
8a9392e400 Fix: AWS SES Mailer (#365)
As reported on issue #357, the aws ses configuration was not able to
store because of the missing `ses` service config. Additionally was
added a `AWS Region` field to be used by the `ses`.

closes #357
2025-05-02 11:16:31 +02:00
mchev
14bfaff30b Fix security alert on axios 0.29 (#347) 2025-05-02 10:49:16 +02:00
Yannic Inselmann
b32c334a71 feat: default notes (#263)
* feat: default notes

* feat: include default invoice note in recurring invoice

* feat: use default export in tw config

* fix: test and naming

* fix: consistent ui for switch in note modal

* feat: little text improvements
2025-04-05 12:01:06 +02:00
mchev
2aa17513e1 Check version number on version.md file (#280) 2025-04-05 10:16:23 +02:00
mchev
ba243b28a9 Upgrade to Heroicons v2 (#281) 2025-04-05 02:11:12 +02:00
mchev
1bb65f420c Fix negative values on item price (#335)
* Fix negative values on item price

* Remove console log
2025-04-05 00:43:34 +02:00
Darko Gjorgjijoski
23a99758d2 New Crowdin updates (#308)
* New translations en.json (Chinese Traditional)

* New translations en.json (Czech)

* New translations en.json (Slovak)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (French)

* New translations en.json (Czech)

* New translations en.json (Romanian)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Indonesian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Slovak)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))

* Update source file en.json

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Polish)

* New translations en.json (Romanian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Traditional)

* New translations en.json (Macedonian)

* New translations en.json (Indonesian)

* New translations en.json (Russian)

* New translations en.json (Russian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Russian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Russian)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Indonesian)

* New translations en.json (Indonesian)

* New translations en.json (Lithuanian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Czech)

* New translations en.json (Indonesian)

* New translations en.json (Lithuanian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))

* Update source file en.json

* New translations en.json (Spanish)
2025-04-05 00:42:24 +02:00
Rihards Simanovičs
139f8e2d13 Update Github issuepr templates (#341)
* chore(github): update bug report template

* chore(github): update feature request template

* chore(github): update code quality template

* chore(github): update issue selection menu config

* chore(github): update PR template

* chore(github): update PR template typo

* chore(github): remove one checklist item from PR template

* chore(github): update bug report to add a Docker check

* chore(github): update template formatting and links

* chore(github): final spell and grammar check of all issues and PR templates
2025-04-04 11:57:07 +02:00
Vid Čufar
ac1a582ecf fix: capitalise 'date' in PDF invoice due date label (#320) 2025-04-04 11:42:24 +02:00
mchev
6b168f36ec [HOTFIX] Customers names on table list issue #210 (#310)
* Fix customer dropdown

* Fix #250
2025-02-17 12:11:10 +01:00
Rihards Simanovičs
50b647e26e Merge pull request #318 from Pureball/translation
fix: en translation spelling
2025-02-17 10:45:46 +00:00
Pureball
57776b7c7d fix: en translation spelling 2025-02-17 10:40:38 +01:00
Darko Gjorgjijoski
8c343b4b92 New Crowdin updates (#304)
* New translations en.json (Spanish)

* New translations en.json (French)

* New translations en.json (Indonesian)

* New translations en.json (Indonesian)
2025-02-05 11:10:06 +01:00
Darko Gjorgjijoski
a60cac3e66 Update version.md 2025-02-05 11:04:44 +01:00
mchev
6a0d3a3bcc Fix customer dropdown (#307) 2025-02-05 11:04:17 +01:00
Darko Gjorgjijoski
3e52b84fa8 New Crowdin updates (#279)
* New translations en.json (Japanese)

* New translations en.json (Japanese)

* New translations en.json (Japanese)

* New translations en.json (Spanish)

* New translations en.json (Indonesian)

* New translations en.json (Indonesian)

* New translations en.json (Indonesian)

* New translations en.json (Japanese)

* New translations en.json (Japanese)

* New translations en.json (German)

* New translations en.json (Japanese)

* New translations en.json (Japanese)

* New translations en.json (Japanese)
2025-02-01 10:41:49 +01:00
Darko Gjorgjijoski
d1f45a790b Bump version 2025-02-01 10:41:19 +01:00
Darko Gjorgjijoski
bec0c058a8 Bump version 2025-01-13 02:14:23 +01:00
Darko Gjorgjijoski
d862ee05e9 Refactor Custom Invoice/Estimate PDF Templates (#277)
* Add utility class for managing templates

* Register custom pdf template views location

* Update the make:template command to make use of PdfTemplateUtils

* Update PDF invoice/estimate template controllers

* Register pdf_templates filesystem disk

* Remove unused leftovers

* Reformat with pint
2025-01-13 01:20:13 +01:00
Darko Gjorgjijoski
12d9d6c801 Regenerate IDE Helper Assets (#274)
* Add back laravel/ui package

* Fix wrong Closure import

* Regenerate ide-helper files
2025-01-12 20:48:06 +01:00
Darko Gjorgjijoski
e9e52c60a7 Reformat with pint 2025-01-12 18:37:08 +01:00
Darko Gjorgjijoski
34b9f52af7 Upgrade Laravel and other third-party packages 2025-01-12 18:22:51 +01:00
Darko Gjorgjijoski
fcf64c0b26 Upgrade Vite, Vue and other thrid-party packages 2025-01-12 17:38:54 +01:00
Darko Gjorgjijoski
7e2d257f7d Update caniuse-lite package 2025-01-12 17:11:50 +01:00
Darko Gjorgjijoski
c617f7d169 Fix: PDF Template command (#272)
* Fix `make:template` command

* Fix issue related to Vite assets

* Reformat code

---------

Co-authored-by: Steven Rombauts <steven@kotuha.be>
2025-01-12 16:19:54 +01:00
Darko Gjorgjijoski
1804481fc6 New Crowdin updates (#262)
* New translations en.json (Indonesian)

* New translations en.json (Turkish)

* New translations en.json (Hindi)

* New translations en.json (Hindi)

* New translations en.json (Persian)

* New translations en.json (Chinese Traditional)

* Update source file en.json

* New translations en.json (Dutch)

* New translations en.json (Czech)

* New translations en.json (Indonesian)

* New translations en.json (Turkish)

* New translations en.json (Hindi)

* New translations en.json (Persian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Serbian (Latin))

* New translations en.json (Dutch)

* New translations en.json (Czech)

* New translations en.json (Indonesian)

* New translations en.json (Turkish)

* New translations en.json (Hindi)

* New translations en.json (Persian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Serbian (Latin))

* Update source file en.json
2025-01-12 15:09:28 +01:00
mchev
9bed81fe8f Handle demo version of the app (#256) 2025-01-12 13:56:52 +01:00
Darko Gjorgjijoski
f52b73f517 Invoice time support (#269)
* Changed invoice date to datetime

* Fixed code style errors

* Update TimeFormatsController.php

* Update TimeFormatter.php

* Update TimeFormatsController namespace

* Fix missing comma in language file

* Fix formatting

---------

Co-authored-by: troky <troky2001@yahoo.com>
2025-01-12 13:32:47 +01:00
Mohamed Safouan Besrour
32e03b98a3 Update CurrenciesTableSeeder.php (#258) 2025-01-12 11:42:12 +01:00
mchev
3f560a1de2 Add Amount Paid and Amount Due to Invoice PDF (#248)
* Display payment status on invoices

* Update en.json
2025-01-12 11:36:36 +01:00
mchev
ef9adbd6c9 Adding sent and viewed field to payload (#247) 2025-01-12 11:27:29 +01:00
Loduis Madariaga Barrios
969cbe9ad8 fix: update company info on array of companies. (#227) 2025-01-12 11:02:58 +01:00
Darko Gjorgjijoski
6031f0dbdd Module paths (#268)
* refactor: Update file paths for stub files for module creation (#144)

---------

Co-authored-by: Dominik Hendrix <dominik.hendrix@g-fittings.com>

* Revert unrelated change

---------

Co-authored-by: Dominik Hendrix <all@dhendrix.de>
Co-authored-by: Dominik Hendrix <dominik.hendrix@g-fittings.com>
2025-01-12 10:49:04 +01:00
Loduis Madariaga Barrios
06a71fc7b3 fix: Corrects and simplifies password visibility icon logic. (#222) 2025-01-12 10:48:34 +01:00
Darko Gjorgjijoski
fd1c7be1f8 New Crowdin updates (#241)
* New translations en.json (Slovak)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Spanish)

* New translations en.json (Indonesian)

* New translations en.json (German)

* New translations en.json (Arabic)

* New translations en.json (Arabic)

* New translations en.json (Japanese)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Czech)

* New translations en.json (Czech)
2025-01-05 12:06:14 +01:00
Darko Gjorgjijoski
c466d51fe0 New Crowdin updates (#235)
* New translations en.json (Swedish)

* New translations en.json (Croatian)

* New translations en.json (Russian)

* New translations en.json (French)

* New translations en.json (Ukrainian)

* New translations en.json (Slovak)

* New translations en.json (Romanian)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Slovenian)

* New translations en.json (Turkish)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))
2024-12-03 18:39:00 +01:00
OniriCorpe
e8dccb178b Specifying that a custom frequency should be in the cron format (#231) 2024-12-03 15:27:21 +01:00
Darko Gjorgjijoski
f5b36d7e6c New Crowdin updates (#232)
* New translations en.json (French)

* New translations en.json (Croatian)

* New translations en.json (Ukrainian)

* New translations en.json (Ukrainian)

* New translations en.json (Ukrainian)

* New translations en.json (Slovak)
2024-12-03 15:27:09 +01:00
OniriCorpe
a32bbb6268 Fixes receipt view (#234)
The #185 modifications were also necessary here
2024-12-03 15:26:05 +01:00
Darko Gjorgjijoski
9599814ae5 New Crowdin updates (#225)
* New translations en.json (Swedish)

* New translations en.json (Croatian)

* New translations en.json (Swedish)

* New translations en.json (Swedish)

* New translations en.json (Swedish)

* New translations en.json (Croatian)

* New translations en.json (Russian)

* New translations en.json (French)

* New translations en.json (French)
2024-11-29 21:29:22 +01:00
Darko Gjorgjijoski
0d14da93d0 New Crowdin updates (#221)
* New translations en.json (Swedish)

* New translations en.json (Swedish)

* New translations en.json (Greek)

* New translations en.json (Greek)
2024-11-18 01:21:13 +01:00
Loduis Madariaga Barrios
298c170867 feat(infrastructure): improve htaccess configuration. (#214) 2024-11-17 00:18:35 +01:00
Rihards Simanovičs
b49228eabf chore: Update Issue Template (#215)
* chore: update issue template

Signed-off-by: Rihards Simanovics <rihards.s@griffin-web.studio>

* chore: correct spelling in issue template

Signed-off-by: Rihards Simanovics <rihards.s@griffin-web.studio>

* fix: adjust render for some textarea fields and add a reproduction step

* fix: remove render from markdown fields

looks like github gets confused as all textarea fields are markdown by the default

* style: add comments and correct labels

* feat(github): add code quality issue template

* refactor: remove direct references to Files

From template fields, descriptions, and placeholders

* refactor(github): convert feature request template to issue form

---------

Signed-off-by: Rihards Simanovics <rihards.s@griffin-web.studio>
2024-11-17 00:18:18 +01:00
Darko Gjorgjijoski
99ada6fbcc New Crowdin updates (#217)
* New translations en.json (Japanese)

* New translations en.json (Japanese)
2024-11-17 00:17:37 +01:00
Darko Gjorgjijoski
ed4a59574c Customer tax id validation (#220)
* Remove tax_id validation

* Remove validation leftovers
2024-11-17 00:17:25 +01:00
Darko Gjorgjijoski
be2e1df442 Add 'Database Overwrtie' during install for SQLite type (#219) 2024-11-17 00:07:44 +01:00
Darko Gjorgjijoski
ddca5a7b6d New translations en.json (Dutch) (#211) 2024-11-11 12:24:48 +01:00
Darko Gjorgjijoski
51d001aa0f New Crowdin updates (#209)
* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))

* New translations en.json (Turkish)
2024-11-09 21:39:07 +01:00
Darko Gjorgjijoski
5d817b5c45 Ignore the translations branch from CI 2024-11-09 15:22:23 +01:00
Ali Çömez | Slaweally
ac2c73829c Update tr.json (#205) 2024-11-09 15:01:57 +01:00
Loduis Madariaga Barrios
317f763a85 chore(wizard): add missing translations for Spanish version in setup wizard (#207) 2024-11-09 14:38:20 +01:00
Joël Kuijper
e112ed5875 Add missing dutch translations (#208) 2024-11-09 14:38:02 +01:00
Darko Gjorgjijoski
84c10264ed Add missing strings lost in merge (#200)
Issue #199
2024-11-02 21:47:22 +01:00
Darko Gjorgjijoski
b143642056 New Crowdin updates (#188)
* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Croatian)

* New translations en.json (Croatian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Croatian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Japanese)

* New translations en.json (German)

* New translations en.json (Japanese)

* New translations en.json (Spanish)

* New translations en.json (Croatian)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (German)

* New translations en.json (Croatian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Czech)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))

* Update source file en.json
2024-11-02 12:49:04 +01:00
mchev
967c225df9 Merge pull request #198 from mchev/invoice_cancellation
Support for Zero and Negative Item Quantities on Invoices
2024-11-02 12:20:55 +01:00
mchev
e1a0a2d8e4 Merge pull request #138 from IDerr/develop
Add VAT and Tax into PDF Trait
2024-11-02 12:19:43 +01:00
mchev
748168ffa5 Merge pull request #189 from loduis/master
enhance(wizard): update translations, refine icons, and add automated…
2024-11-02 10:36:52 +01:00
mchev
4db243e136 Merge branch 'master' into master 2024-11-02 10:31:53 +01:00
mchev
bc0c8d5348 Merge pull request #166 from mchev/customer_tax_id
Include a Tax ID field in both customer creation and invoices
2024-11-02 10:28:49 +01:00
mchev
6583569b1c Merge pull request #190 from mchev/issue_71
Refactor editor and allow links
2024-11-02 10:28:30 +01:00
mchev
134c99369e Merge pull request #186 from mchev/issue_181
Fix table data not refreshing properly (keys)
2024-11-02 10:28:12 +01:00
Loduis Madariaga
f2ae4e17c8 enhance(wizard): update translations, refine icons, and add automated requirement verification 2024-10-17 07:24:17 -05:00
Martin Chevignard
4ff62c0adf Refactor editor and allow links 2024-10-17 11:16:11 +02:00
Darko Gjorgjijoski
2604db32ce New Crowdin updates (#154)
* New translations en.json (Spanish)

* New translations en.json (Finnish)

* New translations en.json (French)

* New translations en.json (German)

* New translations en.json (Romanian)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Czech)

* New translations en.json (Greek)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))

* New translations en.json (Russian)

* New translations en.json (Spanish)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Japanese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Japanese)

* New translations en.json (Japanese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Slovak)

* New translations en.json (Swedish)

* New translations en.json (Vietnamese)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovenian)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))

* Update source file en.json

* New translations en.json (Japanese)

* New translations en.json (Japanese)
2024-10-16 15:18:26 +02:00
Darko Gjorgjijoski
01d32d50b5 Exclude crowdin branch from workflows 2024-10-16 00:54:02 +02:00
SalvaGR
0c86563375 fix: missing values in en.json translation (#179) 2024-10-15 20:53:08 +02:00
mchev
33c2949a7b Fix carbon int val (#185) 2024-10-15 20:51:17 +02:00
mchev
547a2be0a3 Update readme.md 2024-10-15 19:14:55 +02:00
Martin Chevignard
59b43fa258 Public Invoice View fix 2024-10-15 17:55:56 +02:00
Martin Chevignard
168b741936 Upadate filters with laravel best practices 2024-10-15 16:20:04 +02:00
SpeedX
022769ba38 Fix Logo render in Payment PDF (#162)
* Fix logo render in Payment PDF

* Fix CSS to make it consistent with other templates
2024-10-14 11:47:10 +02:00
mchev
068f48568e Fix carbon dates on custom fields value (#178) 2024-10-11 13:17:01 +02:00
Martin Chevignard
03b9defeb1 Customers tax id field 2024-10-04 12:07:29 +02:00
mchev
dd98df1c77 Fix long text on dropdown items (#157) 2024-09-30 20:15:56 +02:00
Darko Gjorgjijoski
c8c75a6aa7 New Crowdin updates (#85)
* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Dutch)

* New translations en.json (Swedish)

* New translations en.json (Swedish)

* New translations en.json (Swedish)

* New translations en.json (Swedish)

* New translations en.json (Swedish)

* New translations en.json (Swedish)

* New translations en.json (Swedish)

* New translations en.json (Swedish)

* New translations en.json (Swedish)

* New translations en.json (Swedish)

* New translations en.json (Swedish)

* New translations en.json (Hindi)

* New translations en.json (Hindi)

* New translations en.json (Dutch)

* New translations en.json (Swedish)

* New translations en.json (Spanish)

* New translations en.json (French)

* New translations en.json (Indonesian)

* New translations en.json (Indonesian)

* New translations en.json (Vietnamese)

* New translations en.json (Vietnamese)

* New translations en.json (Vietnamese)

* New translations en.json (Vietnamese)

* New translations en.json (Vietnamese)

* New translations en.json (Vietnamese)

* New translations en.json (Vietnamese)

* New translations en.json (Slovak)

* New translations en.json (Vietnamese)

* New translations en.json (Slovak)

* New translations en.json (Slovak)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (German)

* New translations en.json (Spanish)

* New translations en.json (Spanish)

* New translations en.json (Finnish)

* New translations en.json (French)

* New translations en.json (German)

* Update source file en.json

* New translations en.json (Spanish)

* New translations en.json (Finnish)

* New translations en.json (French)

* New translations en.json (German)

* New translations en.json (Romanian)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Czech)

* New translations en.json (Greek)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Lithuanian)

* New translations en.json (Macedonian)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Thai)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Serbian (Latin))
2024-09-24 19:51:47 +02:00
Darko Gjorgjijoski
5ece0f43c0 Bump version 2024-09-24 19:05:06 +02:00
Darko Gjorgjijoski
91962f43b2 Add additional language and bump sqlite version 2024-08-04 21:40:15 +02:00
Darko Gjorgjijoski
50613fcff0 Remove unecessary debug calls 2024-08-04 19:50:14 +02:00
Darko Gjorgjijoski
468aec6bc1 Fix partially paid status appearing after invoice update 2024-08-04 19:45:08 +02:00
Darko Gjorgjijoski
c799149d2d Improved logo display in Invoice/Estimate PDFs 2024-08-04 16:08:22 +02:00
Darko Gjorgjijoski
c9e85f18da Merge branch 'develop' into pdf-templates-logo 2024-08-04 15:17:09 +02:00
Darko Gjorgjijoski
9a46f892ab Add support for release channels (insider release channel) in Updater 2024-08-04 03:04:10 +02:00
Darko Gjorgjijoski
bcb89bc9ae Ignore .dev/docker-compose.yml to allow devs to customize one of those 2024-08-04 02:39:20 +02:00
Darko Gjorgjijoski
fbac9abbc5 Expose port 5173 for the Dev environment 2024-08-04 02:38:02 +02:00
Darko Gjorgjijoski
f16b4df44a Remove extensions from composer 2024-08-01 20:48:12 +02:00
Darko Gjorgjijoski
f82937e85e Set app version on install and updates 2024-08-01 19:39:47 +02:00
jacobi petrucciani
f10154539e fix logo rendering in pdf output for invoices 2024-07-31 14:22:48 -04:00
Darko Gjorgjijoski
a64701bda5 Set SESSION_DRIVER to file by default 2024-07-29 14:24:48 +02:00
Darko Gjorgjijoski
da600d0144 Add database overwrite checkbox on Install wizard
Allows overwriting the existing database when installing InvoiceShelf
2024-07-29 14:21:34 +02:00
Darko Gjorgjijoski
56a555bc4a Fix installer wizard step highlighting 2024-07-29 14:06:21 +02:00
Darko Gjorgjijoski
6422da193b Improve .dev/php/entrypoint permissions setting 2024-07-29 13:01:43 +02:00
Darko Gjorgjijoski
cc39f37b12 Update axios and vite 2024-07-29 13:00:54 +02:00
Darko Gjorgjijoski
f95d03f281 Fix "declarations that appear after nested rules" deprecation warning 2024-07-29 12:55:29 +02:00
Darko Gjorgjijoski
4b9052079f Update docker context for the php image 2024-07-28 17:45:06 -07:00
Darko Gjorgjijoski
bf5f95d29d Update file line ending format 2024-07-28 17:44:22 -07:00
Darko Gjorgjijoski
53343c64eb Update migrations formatting & namespace 2024-07-28 23:14:34 +02:00
Darko Gjorgjijoski
dfc4b1982e Update namespace on 1.x migration 2024-07-28 23:13:53 +02:00
Darko Gjorgjijoski
8dd9d0c414 Consolidate migrations v2.0 should go after v1.x 2024-07-28 22:46:45 +02:00
Darko Gjorgjijoski
84a6181240 Merge master 2024-07-28 22:43:02 +02:00
Darko Gjorgjijoski
25fb260cd7 Update local links 2024-07-28 21:07:48 +02:00
Darko Gjorgjijoski
17082ea8c8 Improvements to SQLite dev setup 2024-07-28 21:02:49 +02:00
Darko Gjorgjijoski
c4ade42a8f Update dev readme 2024-07-28 21:02:00 +02:00
Darko Gjorgjijoski
314abe4cc6 Add node and exif & update entrypoint 2024-07-28 19:54:07 +02:00
Darko Gjorgjijoski
45ff9c89a8 Add development environment 2024-07-28 17:25:32 +02:00
Darko Gjorgjijoski
9df5b306a2 Make artisan file executable 2024-07-28 17:24:57 +02:00
Darko Gjorgjijoski
310d05eab1 Set backup to point to the default database connection 2024-07-28 17:24:44 +02:00
Darko Gjorgjijoski
f986454084 Set backup to point to the default database connection 2024-07-28 17:22:40 +02:00
mchev
0c0de64549 Updated download link 2024-07-27 09:49:57 +02:00
Darko Gjorgjijoski
19bf467068 Add "none" as choice for MAIL_ENCRYPTION settings/install that translates to =NULL 2024-07-21 10:42:00 +02:00
Darko Gjorgjijoski
532a044bb9 Add APP_NAME in .env.example 2024-07-18 09:00:00 +02:00
mchev
948e9ea814 Delete CONTRIBUTING.md 2024-07-17 15:55:34 +02:00
mchev
1bd33b8b1b Create CONTRIBUTING.md 2024-07-17 15:50:22 +02:00
mchev
b06cc46eaf Create CONTRIBUTING.md 2024-07-17 12:37:33 +02:00
mchev
c8439d859d Add cache and queue tables migrations (#121)
* Add cache and queue tables migrations
2024-07-17 12:06:04 +02:00
Darko Gjorgjijoski
a866187f71 Add default cache and queue configs 2024-07-17 11:32:42 +02:00
Darko Gjorgjijoski
e212714c63 Pint 2024-07-13 15:19:00 +02:00
Darko Gjorgjijoski
429a8ae826 Add sqlite3 into the php requirements 2024-07-13 15:18:30 +02:00
Darko Gjorgjijoski
94cfe55555 Add sqlite3 validation 2024-07-13 15:18:10 +02:00
Darko Gjorgjijoski
ad9d08c43c Create empty SQLite database if it doesn't exist 2024-07-13 15:12:54 +02:00
Darko Gjorgjijoski
27a511bc09 Update version
[ci-skip]
2024-07-13 03:11:14 +02:00
Darko Gjorgjijoski
1317d57ef8 Update readme.md 2024-07-13 03:07:18 +02:00
Darko Gjorgjijoski
ba85f407d6 Revert "Add version.md file"
This reverts commit 75b0c264dc.
2024-07-13 02:49:02 +02:00
Darko Gjorgjijoski
b37019f845 Set the default database to SQLite 2024-07-13 02:29:20 +02:00
Darko Gjorgjijoski
75b0c264dc Add version.md file 2024-07-13 02:14:41 +02:00
Darko Gjorgjijoski
98852686c7 Add doctrine/dbal and run pint 2024-07-13 02:03:22 +02:00
Darko Gjorgjijoski
a907f8c932 Set the default database to be SQLite 2024-07-12 17:13:54 +02:00
Darko Gjorgjijoski
53866f721c Require dbal as dependency for migrations that changing columns 2024-07-12 12:37:04 +02:00
Darko Gjorgjijoski
9c0d79bf59 Add app version 2024-07-12 03:08:07 +02:00
Darko Gjorgjijoski
33076dc240 Update makefile 2024-07-11 14:10:40 +02:00
Darko Gjorgjijoski
de126465a8 Update code style (pint) 2024-07-11 13:42:43 +02:00
Darko Gjorgjijoski
9317751de9 Remove duplicate SESSION_DOMAIN 2024-07-11 13:37:31 +02:00
Darko Gjorgjijoski
24ba33fd02 Update /installation/languages controller namespace 2024-07-11 13:36:57 +02:00
mchev
9fcf3792c7 Translate recurring invoice subject (#110)
* Translate recurring invoice subject
2024-06-25 19:44:23 +02:00
mchev
2c9f81c571 Merge pull request #109 from mchev/customer-portal
Fix CompanySetting not being imported in BootstrapController
2024-06-20 14:56:56 +02:00
mchev
0af379909d Merge pull request #108 from mchev/updating_other_stored_namespaces
Adding new field to update #107
2024-06-20 14:56:45 +02:00
mchev
680e105f1b Fix CompanySetting not imported in BootstrapController 2024-06-16 21:58:16 +02:00
mchev
210f735e40 Adding new field to update #107 2024-06-16 21:35:55 +02:00
mchev
a0bf6f7f59 Replacing model_type in database (#100)
* Replacing model_type in database

* Also update old Crater namespace
2024-06-07 12:10:57 +02:00
mchev
bb8258036a Clone estimates (#97)
* Clone estimates

* Clone estimate test feature

* Resolve namespace

* Fix string to int for Carbon

* Fix homes routes and default queue key

* Move dropdown item below View and use the propper translation key
2024-06-06 12:16:41 +02:00
mchev
14c599ed4f Allow decimals on items quantities (#94)
* Allow decimals on items quantities #80

* Updating all requests validation

* Revert discount_val
2024-06-05 15:10:36 +02:00
mchev
592a537379 Replace fixed text length with css line-clamp (#96) 2024-06-05 14:36:32 +02:00
mchev
bf5a8cdb28 Increasing base total validation limit for IDR currency (#98) 2024-06-05 12:16:09 +02:00
Darko Gjorgjijoski
96e2f6cf0f Run pint 2024-06-05 12:09:04 +02:00
agencetwogether
3b61440e1f Complete dashboard translations & small UI improvements (#69)
* fix dropdown action Estimate Dashboard and fix translating full Dasboard page

* Update app.php

* fix locale in app.php config

* Wizard install with translation, customer portal with translation, and fixing hardcoding strings to get translation

* fixes asked to review

* fixes pint

---------

Co-authored-by: Max <contact@agencetwogether.fr>
Co-authored-by: Darko Gjorgjijoski <5760249+gdarko@users.noreply.github.com>
2024-06-05 12:07:46 +02:00
mchev
3259173066 Laravel 11 (#84)
* Convert string references to `::class`

PHP 5.5.9 adds the new static `class` property which provides the fully qualified class name. This is preferred over using strings for class names since the `class` property references are checked by PHP.

* Use Faker methods

Accessing Faker properties was deprecated in Faker 1.14.

* Convert route options to fluent methods

Laravel 8 adopts the tuple syntax for controller actions. Since the old options array is incompatible with this syntax, Shift converted them to use modern, fluent methods.

* Adopt class based routes

* Remove default `app` files

* Shift core files

* Streamline config files

* Set new `ENV` variables

* Default new `bootstrap/app.php`

* Re-register HTTP middleware

* Consolidate service providers

* Re-register service providers

* Re-register routes

* Re-register scheduled commands

* Bump Composer dependencies

* Use `<env>` tags for configuration

`<env>` tags have a lower precedence than system environment variables making it easier to overwrite PHPUnit configuration values in additional environments, such a CI.

Review this blog post for more details on configuration precedence when testing Laravel: https://jasonmccreary.me/articles/laravel-testing-configuration-precedence/

* Adopt anonymous migrations

* Rename `password_resets` table

* Convert `$casts` property to method

* Adopt Laravel type hints

* Mark base controller as `abstract`

* Remove `CreatesApplication` testing trait

* Shift cleanup

* Fix shift first issues

* Updating Rules for laravel 11, sanctum config and pint

* Fix Carbon issue on dashboard

* Temporary fix for tests while migration is issue fixed on laravel side

* Carbon needs numerical values, not strings

* Minimum php version

* Fix domain installation step not fetching the correct company_id

* Fix Role Policy wasn't properly registered

---------
2024-06-05 11:33:52 +02:00
Jay Muntz
72311db1bd Fix SES configuration process to set correct SES_KEY and SES_SECRET (#74)
* Fix SES configuration process to set correct SES_KEY and SES_SECRET

* Fixed SES from name

---------

Co-authored-by: Jay Muntz <jaymuntz@jaymuntz.com>
2024-05-31 12:46:06 +02:00
1075 changed files with 90554 additions and 27214 deletions

11
.cursor/mcp.json Normal file
View File

@@ -0,0 +1,11 @@
{
"mcpServers": {
"laravel-boost": {
"command": "php",
"args": [
"artisan",
"boost:mcp"
]
}
}
}

View File

@@ -0,0 +1,106 @@
---
name: medialibrary-development
description: Build and work with spatie/laravel-medialibrary features including associating files with Eloquent models, defining media collections and conversions, generating responsive images, and retrieving media URLs and paths.
license: MIT
metadata:
author: Spatie
---
# Media Library Development
## Overview
Use spatie/laravel-medialibrary to associate files with Eloquent models. Supports image/video conversions, responsive images, multiple collections, and various storage disks.
## When to Activate
- Activate when working with file uploads, media attachments, or image processing in Laravel.
- Activate when code references `HasMedia`, `InteractsWithMedia`, the `Media` model, or media collections/conversions.
- Activate when the user wants to add, retrieve, convert, or manage files attached to Eloquent models.
## Scope
- In scope: media uploads, collections, conversions, responsive images, custom properties, file retrieval, path/URL generation.
- Out of scope: general file storage without Eloquent association, non-Laravel frameworks.
## Workflow
1. Identify the task (model setup, adding media, defining conversions, retrieving files, etc.).
2. Read `references/medialibrary-guide.md` and focus on the relevant section.
3. Apply the patterns from the reference, keeping code minimal and Laravel-native.
## Core Concepts
### Model Setup
Every model that should have media must implement `HasMedia` and use the `InteractsWithMedia` trait:
```php
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
class BlogPost extends Model implements HasMedia
{
use InteractsWithMedia;
}
```
### Adding Media
```php
$blogPost->addMedia($file)->toMediaCollection('images');
$blogPost->addMediaFromUrl($url)->toMediaCollection('images');
$blogPost->addMediaFromRequest('file')->toMediaCollection('images');
```
### Defining Collections
```php
public function registerMediaCollections(): void
{
$this->addMediaCollection('avatar')->singleFile();
$this->addMediaCollection('downloads')->useDisk('s3');
}
```
### Defining Conversions
```php
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\Image\Enums\Fit;
public function registerMediaConversions(?Media $media = null): void
{
$this->addMediaConversion('thumb')
->fit(Fit::Contain, 300, 300)
->nonQueued();
}
```
### Retrieving Media
```php
$url = $model->getFirstMediaUrl('images');
$thumbUrl = $model->getFirstMediaUrl('images', 'thumb');
$allMedia = $model->getMedia('images');
```
## Do and Don't
Do:
- Always implement the `HasMedia` interface alongside the `InteractsWithMedia` trait.
- Use `?Media $media = null` as the parameter for `registerMediaConversions()`.
- Call `->toMediaCollection()` to finalize adding media.
- Use `->nonQueued()` for conversions that should run synchronously.
- Use `->singleFile()` on collections that should only hold one file.
- Use `Spatie\Image\Enums\Fit` enum values for fit methods.
Don't:
- Don't forget to run `php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"` before migrating.
- Don't use `env()` for disk configuration; use `config()` or set it in `config/media-library.php`.
- Don't call `addMedia()` without calling `toMediaCollection()` — the media won't be saved.
- Don't reference conversion names that aren't registered in `registerMediaConversions()`.
## References
- `references/medialibrary-guide.md`

View File

@@ -0,0 +1,577 @@
# Laravel Media Library Reference
Complete reference for `spatie/laravel-medialibrary`. Full documentation: https://spatie.be/docs/laravel-medialibrary
## Model Setup
Implement `HasMedia` and use `InteractsWithMedia`:
```php
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
class BlogPost extends Model implements HasMedia
{
use InteractsWithMedia;
public function registerMediaCollections(): void
{
$this->addMediaCollection('images');
}
public function registerMediaConversions(?Media $media = null): void
{
$this->addMediaConversion('thumb')
->fit(Fit::Contain, 300, 300);
}
}
```
## Adding Media
### From uploaded file
```php
$model->addMedia($request->file('image'))->toMediaCollection('images');
```
### From request (shorthand)
```php
$model->addMediaFromRequest('image')->toMediaCollection('images');
```
### From URL
```php
$model->addMediaFromUrl('https://example.com/image.jpg')->toMediaCollection('images');
```
### From string content
```php
$model->addMediaFromString('raw content')->usingFileName('file.txt')->toMediaCollection('files');
```
### From base64
```php
$model->addMediaFromBase64($base64Data)->usingFileName('photo.jpg')->toMediaCollection('images');
```
### From stream
```php
$model->addMediaFromStream($stream)->usingFileName('file.pdf')->toMediaCollection('files');
```
### From existing disk
```php
$model->addMediaFromDisk('path/to/file.jpg', 's3')->toMediaCollection('images');
```
### Multiple files from request
```php
$model->addMultipleMediaFromRequest(['images'])->each(function ($fileAdder) {
$fileAdder->toMediaCollection('images');
});
$model->addAllMediaFromRequest()->each(function ($fileAdder) {
$fileAdder->toMediaCollection('images');
});
```
### Copy instead of move
```php
$model->copyMedia($pathToFile)->toMediaCollection('images');
// or
$model->addMedia($pathToFile)->preservingOriginal()->toMediaCollection('images');
```
## FileAdder Options
All methods are chainable before calling `toMediaCollection()`:
```php
$model->addMedia($file)
->usingName('Custom Name') // display name
->usingFileName('custom-name.jpg') // filename on disk
->setOrder(3) // order within collection
->withCustomProperties(['alt' => 'A landscape photo'])
->withManipulations(['thumb' => ['filter' => 'greyscale']])
->withResponsiveImages() // generate responsive variants
->storingConversionsOnDisk('s3') // put conversions on different disk
->addCustomHeaders(['CacheControl' => 'max-age=31536000'])
->toMediaCollection('images');
```
### Store on cloud disk
```php
$model->addMedia($file)->toMediaCollectionOnCloudDisk('images');
```
## Media Collections
Define in `registerMediaCollections()`:
```php
public function registerMediaCollections(): void
{
// Basic collection
$this->addMediaCollection('images');
// Single file (replacing previous on new upload)
$this->addMediaCollection('avatar')
->singleFile();
// Keep only latest N items
$this->addMediaCollection('recent_photos')
->onlyKeepLatest(5);
// Specific disk
$this->addMediaCollection('downloads')
->useDisk('s3');
// With conversions disk
$this->addMediaCollection('photos')
->useDisk('s3')
->storeConversionsOnDisk('s3-thumbnails');
// MIME type restriction
$this->addMediaCollection('documents')
->acceptsMimeTypes(['application/pdf', 'application/zip']);
// Custom validation
$this->addMediaCollection('images')
->acceptsFile(function ($file) {
return $file->mimeType === 'image/jpeg';
});
// Fallback URL/path when collection is empty
$this->addMediaCollection('avatar')
->singleFile()
->useFallbackUrl('/images/default-avatar.jpg')
->useFallbackPath(public_path('/images/default-avatar.jpg'));
// Enable responsive images for entire collection
$this->addMediaCollection('hero_images')
->withResponsiveImages();
// Collection-specific conversions
$this->addMediaCollection('photos')
->registerMediaConversions(function () {
$this->addMediaConversion('card')
->fit(Fit::Crop, 400, 400);
});
}
```
## Media Conversions
Define in `registerMediaConversions()`:
```php
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\Image\Enums\Fit;
public function registerMediaConversions(?Media $media = null): void
{
$this->addMediaConversion('thumb')
->fit(Fit::Contain, 300, 300)
->nonQueued();
$this->addMediaConversion('preview')
->fit(Fit::Crop, 500, 500)
->withResponsiveImages()
->queued();
$this->addMediaConversion('banner')
->fit(Fit::Max, 1200, 630)
->performOnCollections('images', 'headers')
->nonQueued()
->sharpen(10);
// Conditional conversion based on media properties
if ($media?->mime_type === 'image/png') {
$this->addMediaConversion('png-thumb')
->fit(Fit::Contain, 150, 150);
}
// Keep original format instead of converting to jpg
$this->addMediaConversion('web')
->fit(Fit::Max, 800, 800)
->keepOriginalImageFormat();
// PDF page rendering
$this->addMediaConversion('pdf-preview')
->pdfPageNumber(1)
->fit(Fit::Contain, 400, 400);
// Video frame extraction
$this->addMediaConversion('video-thumb')
->extractVideoFrameAtSecond(5)
->fit(Fit::Crop, 300, 300);
}
```
### Image Manipulation Methods (via spatie/image)
Resizing and fitting:
- `width(int)`, `height(int)` — constrain dimensions
- `fit(Fit, int, int)` — fit within bounds using `Fit::Contain`, `Fit::Max`, `Fit::Fill`, `Fit::Stretch`, `Fit::Crop`
- `crop(int, int)` — crop to exact dimensions
Effects:
- `sharpen(int)`, `blur(int)`, `pixelate(int)`
- `greyscale()`, `sepia()`
- `brightness(int)`, `contrast(int)`, `colorize(int, int, int)`
Orientation:
- `orientation(int)`, `flip(string)`, `rotate(int)`
Format:
- `format(string)``'jpg'`, `'png'`, `'webp'`, `'avif'`
- `quality(int)` — 1-100
Other:
- `border(int, string, string)`, `watermark(string)`
- `optimize()`, `nonOptimized()`
### Conversion Configuration
- `performOnCollections('col1', 'col2')` — limit to specific collections
- `queued()` / `nonQueued()` — run async or sync
- `withResponsiveImages()` — also generate responsive variants for this conversion
- `keepOriginalImageFormat()` — preserve png/webp/gif instead of converting to jpg
- `pdfPageNumber(int)` — which PDF page to render
- `extractVideoFrameAtSecond(int)` — video thumbnail timing
## Retrieving Media
### Getting media items
```php
$media = $model->getMedia('images'); // all in collection
$first = $model->getFirstMedia('images'); // first item
$last = $model->getLastMedia('images'); // last item
$has = $model->hasMedia('images'); // boolean check
```
### Getting URLs
```php
$url = $model->getFirstMediaUrl('images'); // original URL
$thumbUrl = $model->getFirstMediaUrl('images', 'thumb'); // conversion URL
$lastUrl = $model->getLastMediaUrl('images', 'thumb');
```
### Getting paths
```php
$path = $model->getFirstMediaPath('images');
$thumbPath = $model->getFirstMediaPath('images', 'thumb');
```
### Temporary URLs (S3)
```php
$tempUrl = $model->getFirstTemporaryUrl(
now()->addMinutes(30),
'images',
'thumb'
);
```
### Fallback URLs
```php
$url = $model->getFallbackMediaUrl('avatar');
```
### From the Media model
```php
$media = $model->getFirstMedia('images');
$media->getUrl(); // original URL
$media->getUrl('thumb'); // conversion URL
$media->getPath(); // disk path
$media->getFullUrl(); // full URL with domain
$media->getTemporaryUrl(now()->addMinutes(30));
$media->hasGeneratedConversion('thumb'); // check if conversion exists
```
### Filtering media
```php
$media = $model->getMedia('images', function (Media $media) {
return $media->getCustomProperty('featured') === true;
});
$media = $model->getMedia('images', ['mime_type' => 'image/jpeg']);
```
## Custom Properties
Store arbitrary metadata on media items:
```php
// When adding
$model->addMedia($file)
->withCustomProperties([
'alt' => 'Descriptive text',
'credits' => 'Photographer Name',
])
->toMediaCollection('images');
// Get/set on existing media
$media->setCustomProperty('alt', 'Updated text');
$media->save();
$alt = $media->getCustomProperty('alt');
$has = $media->hasCustomProperty('alt');
$media->forgetCustomProperty('alt');
$media->save();
```
## Responsive Images
Generate multiple sizes for optimal loading:
```php
// On the FileAdder
$model->addMedia($file)
->withResponsiveImages()
->toMediaCollection('images');
// On a conversion
$this->addMediaConversion('hero')
->fit(Fit::Max, 1200, 800)
->withResponsiveImages();
// On a collection
$this->addMediaCollection('photos')
->withResponsiveImages();
```
### Using in Blade
```blade
{{-- Renders img tag with srcset --}}
{{ $media->toHtml() }}
{{-- With attributes --}}
{{ $media->img()->attributes(['class' => 'w-full', 'alt' => 'Photo']) }}
{{-- Get srcset string --}}
<img src="{{ $media->getUrl() }}" srcset="{{ $media->getSrcset() }}" />
{{-- Responsive conversion --}}
<img src="{{ $media->getUrl('hero') }}" srcset="{{ $media->getSrcset('hero') }}" />
```
### Placeholder SVG
```php
$svg = $media->responsiveImages()->getPlaceholderSvg(); // tiny blurred base64 placeholder
```
## Managing Media
### Clear a collection
```php
$model->clearMediaCollection('images');
```
### Clear except specific items
```php
$model->clearMediaCollectionExcept('images', $mediaToKeep);
```
### Delete specific media
```php
$model->deleteMedia($mediaId);
```
### Delete all media
```php
$model->deleteAllMedia();
```
### Delete model but keep media files
```php
$model->deletePreservingMedia();
```
### Reorder media
```php
Media::setNewOrder([3, 1, 2]); // media IDs in desired order
```
### Move/copy media between models
```php
$media->move($otherModel, 'images');
$media->copy($otherModel, 'images');
```
## Events
```php
use Spatie\MediaLibrary\MediaCollections\Events\MediaHasBeenAddedEvent;
use Spatie\MediaLibrary\Conversions\Events\ConversionWillStartEvent;
use Spatie\MediaLibrary\Conversions\Events\ConversionHasBeenCompletedEvent;
use Spatie\MediaLibrary\MediaCollections\Events\CollectionHasBeenClearedEvent;
```
Listen to these events to hook into the media lifecycle:
```php
Event::listen(MediaHasBeenAddedEvent::class, function ($event) {
$event->media; // the added Media model
});
Event::listen(ConversionHasBeenCompletedEvent::class, function ($event) {
$event->media;
$event->conversion;
});
```
## Configuration
Key `config/media-library.php` options:
```php
return [
'disk_name' => 'public', // default disk
'max_file_size' => 1024 * 1024 * 10, // 10MB
'queue_connection_name' => '', // queue connection
'queue_name' => '', // queue name
'queue_conversions_by_default' => true, // queue conversions
'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::class,
'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class,
'path_generator' => Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator::class,
'url_generator' => Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator::class,
'image_driver' => 'gd', // 'gd', 'imagick', or 'vips'
'image_optimizers' => [/* optimizer config */],
'version_urls' => true, // cache busting
'default_loading_attribute_value' => null, // 'lazy' for lazy loading
];
```
### Custom Path Generator
```php
use Spatie\MediaLibrary\Support\PathGenerator\PathGenerator;
class CustomPathGenerator implements PathGenerator
{
public function getPath(Media $media): string
{
return md5($media->id) . '/';
}
public function getPathForConversions(Media $media): string
{
return $this->getPath($media) . 'conversions/';
}
public function getPathForResponsiveImages(Media $media): string
{
return $this->getPath($media) . 'responsive/';
}
}
```
### Custom File Namer
```php
use Spatie\MediaLibrary\Support\FileNamer\FileNamer;
class CustomFileNamer extends FileNamer
{
public function originalFileName(string $fileName): string
{
return Str::slug(pathinfo($fileName, PATHINFO_FILENAME));
}
public function conversionFileName(string $fileName, Conversion $conversion): string
{
return $this->originalFileName($fileName) . '-' . $conversion->getName();
}
public function responsiveFileName(string $fileName): string
{
return pathinfo($fileName, PATHINFO_FILENAME);
}
}
```
### Custom Media Model
```php
use Spatie\MediaLibrary\MediaCollections\Models\Media as BaseMedia;
class Media extends BaseMedia
{
// Add custom methods, scopes, or override behavior
}
```
Register in config: `'media_model' => App\Models\Media::class`
## Downloading Media
### Single file
```php
return $media->toResponse($request); // download
return $media->toInlineResponse($request); // display inline
return $media->stream(); // stream
```
### ZIP download of collection
```php
use Spatie\MediaLibrary\Support\MediaStream;
return MediaStream::create('photos.zip')
->addMedia($model->getMedia('images'));
```
## Using with API Resources
```php
class PostResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'image' => $this->getFirstMediaUrl('images'),
'thumb' => $this->getFirstMediaUrl('images', 'thumb'),
'media' => $this->getMedia('images')->map(function ($media) {
return [
'id' => $media->id,
'url' => $media->getUrl(),
'thumb' => $media->getUrl('thumb'),
'name' => $media->name,
'size' => $media->size,
'type' => $media->mime_type,
];
}),
];
}
}
```

View File

@@ -0,0 +1,157 @@
---
name: pest-testing
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
license: MIT
metadata:
author: laravel
---
# Pest Testing 4
## Documentation
Use `search-docs` for detailed Pest 4 patterns and documentation.
## Basic Usage
### Creating Tests
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
### Test Organization
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
- Browser tests: `tests/Browser/` directory.
- Do NOT remove tests without approval - these are core application code.
### Basic Test Structure
<!-- Basic Pest Test Example -->
```php
it('is true', function () {
expect(true)->toBeTrue();
});
```
### Running Tests
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
- Run all tests: `php artisan test --compact`.
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
## Assertions
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
<!-- Pest Response Assertion -->
```php
it('returns all', function () {
$this->postJson('/api/docs', [])->assertSuccessful();
});
```
| Use | Instead of |
|-----|------------|
| `assertSuccessful()` | `assertStatus(200)` |
| `assertNotFound()` | `assertStatus(404)` |
| `assertForbidden()` | `assertStatus(403)` |
## Mocking
Import mock function before use: `use function Pest\Laravel\mock;`
## Datasets
Use datasets for repetitive tests (validation rules, etc.):
<!-- Pest Dataset Example -->
```php
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
```
## Pest 4 Features
| Feature | Purpose |
|---------|---------|
| Browser Testing | Full integration tests in real browsers |
| Smoke Testing | Validate multiple pages quickly |
| Visual Regression | Compare screenshots for visual changes |
| Test Sharding | Parallel CI runs |
| Architecture Testing | Enforce code conventions |
### Browser Test Example
Browser tests run in real browsers for full integration testing:
- Browser tests live in `tests/Browser/`.
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
- Use `RefreshDatabase` for clean state per test.
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
- Switch color schemes (light/dark mode) when appropriate.
- Take screenshots or pause tests for debugging.
<!-- Pest Browser Test Example -->
```php
it('may reset the password', function () {
Notification::fake();
$this->actingAs(User::factory()->create());
$page = visit('/sign-in');
$page->assertSee('Sign In')
->assertNoJavaScriptErrors()
->click('Forgot Password?')
->fill('email', 'nuno@laravel.com')
->click('Send Reset Link')
->assertSee('We have emailed your password reset link!');
Notification::assertSent(ResetPassword::class);
});
```
### Smoke Testing
Quickly validate multiple pages have no JavaScript errors:
<!-- Pest Smoke Testing Example -->
```php
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
```
### Visual Regression Testing
Capture and compare screenshots to detect visual changes.
### Test Sharding
Split tests across parallel processes for faster CI runs.
### Architecture Testing
Pest 4 includes architecture testing (from Pest 3):
<!-- Architecture Test Example -->
```php
arch('controllers')
->expect('App\Http\Controllers')
->toExtendNothing()
->toHaveSuffix('Controller');
```
## Common Pitfalls
- Not importing `use function Pest\Laravel\mock;` before using mock
- Using `assertStatus(200)` instead of `assertSuccessful()`
- Forgetting datasets for repetitive validation tests
- Deleting tests without approval
- Forgetting `assertNoJavaScriptErrors()` in browser tests

View File

@@ -0,0 +1,119 @@
---
name: tailwindcss-development
description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS."
license: MIT
metadata:
author: laravel
---
# Tailwind CSS Development
## Documentation
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
## Basic Usage
- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.
## Tailwind CSS v4 Specifics
- Always use Tailwind CSS v4 and avoid deprecated utilities.
- `corePlugins` is not supported in Tailwind v4.
### CSS-First Configuration
In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:
<!-- CSS-First Config -->
```css
@theme {
--color-brand: oklch(0.72 0.11 178);
}
```
### Import Syntax
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
<!-- v4 Import Syntax -->
```diff
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";
```
### Replaced Utilities
Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.
| Deprecated | Replacement |
|------------|-------------|
| bg-opacity-* | bg-black/* |
| text-opacity-* | text-black/* |
| border-opacity-* | border-black/* |
| divide-opacity-* | divide-black/* |
| ring-opacity-* | ring-black/* |
| placeholder-opacity-* | placeholder-black/* |
| flex-shrink-* | shrink-* |
| flex-grow-* | grow-* |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |
## Spacing
Use `gap` utilities instead of margins for spacing between siblings:
<!-- Gap Utilities -->
```html
<div class="flex gap-8">
<div>Item 1</div>
<div>Item 2</div>
</div>
```
## Dark Mode
If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:
<!-- Dark Mode -->
```html
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
Content adapts to color scheme
</div>
```
## Common Patterns
### Flexbox Layout
<!-- Flexbox Layout -->
```html
<div class="flex items-center justify-between gap-4">
<div>Left content</div>
<div>Right content</div>
</div>
```
### Grid Layout
<!-- Grid Layout -->
```html
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>Card 1</div>
<div>Card 2</div>
<div>Card 3</div>
</div>
```
## Common Pitfalls
- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
- Using `@tailwind` directives instead of `@import "tailwindcss"`
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
- Using margins for spacing between siblings instead of gap utilities
- Forgetting to add dark mode variants when the project uses dark mode

45
.dockerignore Normal file
View File

@@ -0,0 +1,45 @@
.idea/
.git/
.github/
docker/
!docker/production/entrypoint.d
!docker/production/inject.sh
node_modules/
database/*.sqlite
storage/app/*
!storage/app/templates*
storage/fonts/*
storage/framework/cache/data/*
storage/framework/sessions/*
storage/framework/views/*
storage/logs/*
tests/
vendor/
.dockerignore
.devenvconfig
.editorconfig
.env
.env.testing
.eslintrc.mjs
.gitattributes
.gitignore
.prettierrc.json
devenv
CODE_OF_CONDUCT.md
Dockerfile
*.Dockerfile
LICENSE
Makefile
SECURITY.md
_ide_helper.php
crowdin.yml
invoiceshelf.code-workspace
package-lock.json
phpunit.xml
readme.md

View File

@@ -1,43 +1,23 @@
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:kgk/4DW1vEVy7aEvet5FPp5un6PIGe/so8H0mvoUtW0=
APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://invoiceshelf.test
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=invoiceshelf
DB_USERNAME=invoiceshelf
DB_PASSWORD="invoiceshelf"
APP_NAME="InvoiceShelf"
APP_TIMEZONE=UTC
APP_URL=
APP_LOCALE=en
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_DRIVER=sync
SESSION_DRIVER=cookie
SESSION_LIFETIME=1440
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_DRIVER=smtp
MAIL_HOST=
MAIL_PORT=
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=
MAIL_FROM_NAME=
MAIL_FROM_ADDRESS=
PUSHER_APP_ID=
PUSHER_KEY=
PUSHER_SECRET=
SANCTUM_STATEFUL_DOMAINS=invoiceshelf.test
SESSION_DOMAIN=invoiceshelf.test
DB_CONNECTION=sqlite
DB_HOST=
DB_PORT=
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
SESSION_DOMAIN=null
SANCTUM_STATEFUL_DOMAIN=
TRUSTED_PROXIES="*"
CRON_JOB_AUTH_TOKEN=""
# Dompdf: keep false so untrusted HTML in PDF notes cannot trigger outbound requests (SSRF).
# Set true only if you fully trust all PDF HTML and need remote images/CSS.
DOMPDF_ENABLE_REMOTE=false

View File

@@ -3,7 +3,7 @@ APP_DEBUG=true
APP_KEY=base64:IdDlpLmYyWA9z4Ruj5st1FSYrhCR7lPOscLGCz2Jf4I=
DB_CONNECTION=sqlite
MAIL_DRIVER=smtp
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=587
MAIL_USERNAME=ff538f0e1037f4

View File

@@ -1,14 +0,0 @@
// .eslintrc.js
module.exports = {
extends: [
// add more generic rulesets here, such as:
// 'eslint:recommended',
"plugin:vue/vue3-recommended",
"prettier",
],
rules: {
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
},
};

24
.gitattributes vendored
View File

@@ -1,3 +1,25 @@
* text=auto
# Set the default behavior, in case people don't have core.autocrlf set.
* text eol=lf
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.gif binary
*.otf binary
*.eot binary
*.svg binary
*.ttf binary
*.woff binary
*.woff2 binary
*.css linguist-vendored
*.scss linguist-vendored
*.js linguist-vendored

65
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,65 @@
# Welcome to InvoiceShelf contributing guide <!-- omit in toc -->
Thank you for investing your time in contributing to our project! :sparkles:.
Read our [Code of Conduct](../CODE_OF_CONDUCT.md) to keep our community approachable and respectable.
In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR.
## New contributor guide
To get an overview of the project, read the [README](../README.md) file. Here are some resources to help you get started with open source contributions:
- [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github)
- [Set up Git](https://docs.github.com/en/get-started/getting-started-with-git/set-up-git)
- [GitHub flow](https://docs.github.com/en/get-started/using-github/github-flow)
- [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests)
## Getting started
### Issues
#### Create a new issue
If you spot a problem, [search if an issue already exists](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments). If a related issue doesn't exist, you can open a new issue using a relevant [issue form](https://github.com/InvoiceShelf/InvoiceShelf/issues/new/choose).
#### Solve an issue
Scan through our [existing issues](https://github.com/InvoiceShelf/InvoiceShelf/issues) to find one that interests you. You can narrow down the search using `labels` as filters. See "[Label reference](https://docs.github.com/en/contributing/collaborating-on-github-docs/label-reference)" for more information. As a general rule, we dont assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix.
### Make Changes
#### Make changes locally
1. Fork the repository.
- Using GitHub Desktop:
- [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop.
- Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)!
- Using the command line:
- [Fork the repo](https://docs.github.com/en/get-started/quickstart/fork-a-repo) so that you can make your changes without affecting the original project until you're ready to merge them.
2. Install or update to **Node.js**, at the version specified in `.node-version`. For more information, see [the development guide](../docker/development/README.md).
3. Create a working branch and start with your changes!
### Commit your update
Commit the changes once you are happy with them. Don't forget to use the "[Self review checklist](https://docs.github.com/en/contributing/collaborating-on-github-docs/self-review-checklist)" to speed up the review process :zap:.
### Pull Request
When you're finished with the changes, create a pull request, also known as a PR.
- Fill the "Ready for review" template so that we can review your PR. This template helps reviewers understand your changes as well as the purpose of your pull request.
- Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one.
- Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge.
Once you submit your PR, a project maintainer will review your proposal. We may ask questions or request additional information.
- We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch.
- As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations).
- If you run into any merge issues, check out this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues.
### Your PR is merged!
Congratulations :tada::tada: The InvoiceShelf team thanks you :sparkles:.
Once your PR is merged, your contributions will be publicly visible on the [InvoiceShelf repository](https://github.com/InvoiceShelf/InvoiceShelf).

View File

@@ -1,26 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Please complete the following information:**
- InvoiceShelf version:
- PHP version:
- Database type and version:
**Optional info**
- OS: [e.g. Ubuntu]
- Browser: [e.g. chrome, safari]

237
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,237 @@
name: Bug report
description: Template for bug reports
labels: ['bug', 'triage']
type: Bug
projects: ['InvoiceShelf/2']
assignees:
- rihards-simanovics
body:
# Ask user to confirm they've tried to fix or research the issue before
# posting a bug report.
- type: checkboxes
id: confirm-read-documentation
attributes:
label: Issue filing prerequisites
description: 'Prior to filing an issue please confirm that:'
options:
- label: I've checked the [documentation](https://docs.invoiceshelf.com/).
required: true
- label: I've looked for similar issues both Open and Closed.
required: true
- label: >-
I've tried clearing both cache and cookies in my browser or tried
opening the app in the Incognito/InPrivate window.
required: true
# Thank users for taking time to submit a bug report
- type: markdown
attributes:
value: >-
If you did all of the above, we would first like to thank you for taking
the time to fill out this bug report. It will help us tremendously to
find and fix the issue sooner!
# Description of the bug
- type: textarea
id: bug-description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
placeholder: >-
When doing `x`, `y`, and `z` in that order, an error occurs, but when `x`,
`z` and then `y` is done, the error does not occur.
validations:
required: true
# Reproduction steps
- type: textarea
id: reproduce
attributes:
label: Steps to Reproduce the issue
description: A clear step-by-step explanation of how to reproduce your issue
placeholder: >-
## Produces an error:
1. Do X;
2. Do Y;
3. Do Z;
## Works fine:
4. Do X;
5. Do Z;
6. Do Y.
validations:
required: true
# Expected Behaviour
- type: textarea
id: expected-behaviour
attributes:
label: Expected behaviour
description: A clear and concise description of what you expected to happen.
placeholder: Doing `x`, `y`, and `z` in that order should not generate an error.
validations:
required: true
# Actual Behaviour
- type: textarea
id: actual-behaviour
attributes:
label: Actual behaviour
description: A clear and concise description of what actually happens.
placeholder: Doing `x`, `y`, and `z` in that order generates an error.
validations:
required: true
# Section break
- type: markdown
attributes:
value: |-
## Environment
Now let's collect some information about your environment.
# Use Docker?
- type: checkboxes
id: confirm-docker-install
attributes:
label: Docker
description: 'Please note that unless the issue is with the app itself, you should file a bug report under the [docker](https://github.com/InvoiceShelf/docker) repository.'
options:
- label: App running in Docker Container.
- label: Docker container running behind Reverse proxy.
# App Version
- type: input
id: invoiceshelf-version
attributes:
label: InvoiceShelf version
placeholder: v0.0.0
validations:
required: true
# PHP Version
- type: input
id: php-version
attributes:
label: PHP version
placeholder: v0.0.0
validations:
required: true
# DB Type
- type: input
id: database-type
attributes:
label: Database type
placeholder: MariaDB / MySQL / PostgreSQL / SQLite
validations:
required: true
# DB Version
- type: input
id: db-version
attributes:
label: Database version
placeholder: v0.0.0
validations:
required: true
# Web Browser
- type: input
id: web-browser
attributes:
label: Web Browser
placeholder: 'Firefox / Safari / or any other (Chromium-based browser)'
# OS Version
- type: input
id: server-os
attributes:
label: Server OS
description: If Linux, please make sure to provide both the distro name and its version
placeholder: Windows / Linux (e.g. Ubuntu 24.04)
# Associated Logs
- type: markdown
attributes:
value: >-
## Logs
Last but not least, could you please take some time to get us the logs
for:
# Log Reverse proxy
- type: textarea
id: log-rev-proxy
attributes:
label: Reverse-proxy logs
description: >-
Please provide logs from your Apache, Nginx, Traefik, or any other
reverse proxy application.
placeholder: >-
2023-04-12 10:15:32 [NGINX] 172.16.0.5 - - [12/Apr/2023:10:15:32 +0000] "GET /index.html HTTP/1.1" 200 1024 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
2023-04-12 10:15:33 [NGINX] 172.16.0.7 - - [12/Apr/2023:10:15:33 +0000] "POST /login HTTP/1.1" 302 - "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8"
2023-04-12 10:15:34 [NGINX] 172.16.0.9 - - [12/Apr/2023:10:15:34 +0000] "GET /about HTTP/1.1" 200 2048 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"
2023-04-12 10:15:35 [NGINX] 172.16.0.11 - - [12/Apr/2023:10:15:35 +0000] "PUT /api/v1/users/123 HTTP/1.1" 200 - "https://example.com/profile" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1"
2023-04-12 10:15:36 [NGINX] 172.16.0.13 - - [12/Apr/2023:10:15:36 +0000] "DELETE /api/v1/products/456 HTTP/1.1" 204 - "-" "Dalvik/2.1.0 (Linux; U; Android 10; Pixel 4 Build/QD1A.190821.014.C2)"
2023-04-12 10:15:37 [NGINX] 172.16.0.15 - - [12/Apr/2023:10:15:37 +0000] "GET /easter-egg HTTP/1.1" 200 42 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.3"
render: irc logs
# PHP/Laravel Logs
- type: textarea
id: log-laravel
attributes:
label: Laravel/PHP logs
description: Please provide logs from either PHP or Laravel or both.
placeholder: >-
[2015-02-04 10:00:00] production.INFO: Laravel 5.0 released. The world rejoices.
[2015-12-03 12:00:00] production.INFO: PHP 7.0 introduced scalar type declarations. Minds blown.
[2016-05-15 14:00:00] production.INFO: Composer installed new dependencies. Dependency hell avoided.
[2017-08-20 16:00:00] production.INFO: Artisan command executed: migrate. Smooth sailing.
[2018-01-10 09:00:00] production.INFO: User login successful.
[2018-01-10 09:05:00] production.ERROR: Database connection failed. Did you try turning it off and on again?
[2018-01-10 09:10:00] production.WARNING: Deprecated function used in UserController.php. Time to refactor, again.
[2018-01-10 09:15:00] production.ERROR: Uncaught Exception: Division by zero. Oops, maths is hard.
[2018-01-10 09:20:00] production.INFO: User logout successful. See you later, alligator.
[2019-03-25 11:00:00] production.INFO: Cache cleared. Fresh start!
[2019-06-30 13:00:00] production.ERROR: Syntax error. Missing semicolon strikes again.
[2020-09-10 15:00:00] production.INFO: User registered. Welcome aboard!
[2021-11-05 17:00:00] production.WARNING: Low disk space. Time to clean up.
[2022-12-20 19:00:00] production.INFO: Server rebooted. All systems go.
[2024-11-14 18:56:20] production.INFO: Unexpected item in the bagging area. Please wait for assistance.
render: irc logs
# Special thanks
- type: markdown
attributes:
value: >-
This template was generated with [Issue Forms
Creator](https://issue-forms-creator.netlify.app)

View File

@@ -0,0 +1,94 @@
# Big thanks to https://github.com/files-community/Files/ for a template of the code_quality_issue
name: Code Quality Issue
description: Create a code quality issue to help InvoiceShelf keep a clean codebase
labels:
- code quality
- triage
projects: ['InvoiceShelf/2']
assignees:
- rihards-simanovics
body:
# Tip to warn of checking for existing issues
- type: markdown
attributes:
value: >
> [!TIP]
> Have you checked for similar code quality issues? There's a
possibility your suggestion is already being tracked. Please do a
thorough search before creating a new issue.
# Ask user to confirm they've tried to fix or research the issue before
# posting a bug report.
- type: checkboxes
id: confirm-read-documentation
attributes:
label: Issue filing prerequisites
description: 'Prior to filing an issue please confirm that:'
options:
- label: I've checked the [documentation](https://docs.invoiceshelf.com/).
required: true
- label: I've looked for similar issues both Open and Closed.
required: true
- label: >-
I've tried clearing both cache and cookies in my browser or tried
opening the app in the Incognito/InPrivate window.
required: true
# Issue body
- type: textarea
id: description
attributes:
label: Description
description: A clear and concise description of what the code quality issue is.
validations:
required: true
# Related code
- type: textarea
id: code-in-question
attributes:
label: The code in question
description: >-
A list of the different InvoiceShelf and/or areas of the code concerned by the issue.
validations:
required: true
# Gains
- type: textarea
id: gains
attributes:
label: Gains
description: What would fixing this code quality issue bring to the source code?
placeholder: >-
- e.g. Better readability.
- e.g. Uncoupling concepts X and Y.
- e.g. Clarifying the responsibility of class C.
validations:
required: true
# Requirements
- type: textarea
id: requirements
attributes:
label: Requirements
description: Describe all the requirements to solve the code quality issue.
placeholder: >-
- e.g. Using a specific design pattern.
- e.g. Separating Interface I into three new interfaces I1, I2 and I3.
- e.g. Regrouping the duplicated process into a new helper.
# Remarks
- type: textarea
id: comments
attributes:
label: Comments
description: >-
Additional information, comments or screenshots about the code quality
issue.

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Documentation
url: https://docs.invoiceshelf.com/
about: App documentation.
- name: Translation issue
url: https://crowdin.com/project/invoiceshelf
about: Help improve translations on Crowdin.
- name: Support, Question & Discussion - Official Discord Server
url: https://discord.gg/hwg2FtwWHW
about: Post your questions and join the discussion. You can participate in chats on every channel.

View File

@@ -1,17 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

View File

@@ -0,0 +1,96 @@
# Big thanks to https://github.com/files-community/Files/ for a template of the feature_request
name: Feature Request
labels:
- feature request
- triage
type: Feature
projects: ['InvoiceShelf/2']
assignees:
- rihards-simanovics
description: >-
This project thrives from differentiation from competing apps. Suggest an idea
for InvoiceShelf.
body:
# Tip to warn of checking for existing issues
- type: markdown
attributes:
value: >
> [!TIP]
>Have you checked for similar feature requests? There's a possibility
your suggestion is already being tracked. Please do a thorough search
before creating a new issue.
# Ask user to confirm they've tried to fix or research the issue before
# posting a bug report.
- type: checkboxes
id: confirm-read-documentation
attributes:
label: Issue filing prerequisites
description: 'Prior to filing an issue please confirm that:'
options:
- label: I've checked the [documentation](https://docs.invoiceshelf.com/).
required: true
- label: I've looked for similar issues with feature requests both Open and Closed.
required: true
- label: >-
I've tried clearing both cache and cookies in my browser or tried
opening the app in the Incognito/InPrivate window.
required: true
# Description
- type: textarea
id: what-feature
attributes:
label: What feature or improvement do you think would benefit InvoiceShelf?
description: Please include your use case
placeholder: >-
I propose that feature J be implemented as I believe it will greatly benefit the application.
validations:
required: true
# Tooltip about Requirements
- type: markdown
attributes:
value: >
---
Please include a list of changes required to make this improvement. A good
rule of thumb is to start your proposal with no more than 7 high-level
requirements.
# Requirements
- type: textarea
id: requirements
attributes:
label: Requirements
description: Describe all the requirements to make your idea happen.
placeholder: >-
- This proposal will accomplish X
- This proposal will accomplish Y
- This proposal will accomplish Z
validations:
required: true
# InvoiceShelf Version
- type: input
id: invoiceshelf_version
attributes:
label: App Version
description: Which version of InvoiceShelf are you currently using?
placeholder: v0.0.0
validations:
required: true
# Additional Comments
- type: textarea
id: comments
attributes:
label: Comments
description: >-
Additional information, comments or screenshots about the feature
request.

View File

@@ -0,0 +1,17 @@
## Pre-review request checklist
- [ ] (\*) I have listed all changes in the `Changes` section.
- [ ] (opt) I have listed all issues this PR addresses in the `Issues` section [with the issue/discussion IDs](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue).
- [ ] (\*) I have tested my changes.
- [ ] (\*) I have considered backwards compatibility.
## Changes
- Added feature X
- Removed Code Y
- Refactored code Z
## Issues
- fixes #
- closes #

18
.github/release.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
changelog:
categories:
- title: New Features
labels:
- enhancement
- feature
- title: Bug Fixes
labels:
- bug
- fix
- title: Maintenance
labels:
- chore
- dependencies
- ci
- title: Other Changes
labels:
- "*"

View File

@@ -3,19 +3,41 @@ name: Check
# Run this workflow every time a new commit pushed to your repository
on:
push:
paths-ignore:
- '**/*.md'
- 'public/build/*.js'
- 'public/build/**/*.js'
tags-ignore:
- "*"
branches-ignore:
- 'translations'
pull_request:
paths-ignore:
- '**/*.md'
- 'public/build/*.js'
- 'public/build/**/*.js'
branches-ignore:
- 'translations'
# Allow manually triggering the workflow.
workflow_dispatch:
jobs:
changes:
name: 🔍 Detect changes
runs-on: ubuntu-latest
outputs:
php: ${{ steps.filter.outputs.php }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check for file changes
uses: dorny/paths-filter@v3
id: filter
with:
filters: |
php:
- 'app/**'
- 'config/**'
- 'database/**'
- 'routes/**'
- 'tests/**'
- 'composer.json'
- 'composer.lock'
- 'phpunit.xml'
kill_previous:
name: 0⃣ Kill previous runs
runs-on: ubuntu-latest
@@ -32,11 +54,13 @@ jobs:
runs-on: ubuntu-latest
needs:
- kill_previous
- changes
if: needs.changes.outputs.php == 'true'
steps:
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
php-version: 8.4
- name: Checkout code
uses: actions/checkout@v4
@@ -51,18 +75,19 @@ jobs:
name: 2⃣ PHP ${{ matrix.php-version }} Tests
needs:
- php_syntax_errors
- changes
if: needs.changes.outputs.php == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
php-version:
- 8.2
- 8.3
- 8.4
env:
extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_mysql, zip
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup PHP Action
uses: shivammathur/setup-php@v2
@@ -75,10 +100,10 @@ jobs:
- name: Install Composer dependencies
uses: ramsey/composer-install@v2
- name: Use Node.js 20
uses: actions/setup-node@v3
- name: Use Node.js 24
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
- name: Install
run: npm install
@@ -88,52 +113,3 @@ jobs:
- name: Apply tests ${{ matrix.php-version }}
run: php artisan test
createReleaseFile:
name: 3⃣ Build / Upload - Release File
if: github.ref_type == 'tag'
needs:
- tests
runs-on: ubuntu-latest
env:
extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_mysql, zip
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
extensions: ${{ env.extensions }}
coverage: none
- name: Install Composer dependencies
uses: ramsey/composer-install@v2
with:
composer-options: --no-dev
- name: Use Node.js 20
uses: actions/setup-node@v3
with:
node-version: 20
- name: Install
run: npm install
- name: Compile Front-end
run: npm run build
- name: Build Dist
run: |
make clean dist
- name: Upload package
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ github.token }}
file: InvoiceShelf.zip
asset_name: InvoiceShelf.zip
tag: ${{ github.ref }}
overwrite: true

259
.github/workflows/docker.yaml vendored Normal file
View File

@@ -0,0 +1,259 @@
name: Docker Build and Push
on:
release:
types: [published]
schedule:
# Run nightly at 2 AM UTC
- cron: '0 2 * * *'
workflow_dispatch:
inputs:
tag:
description: 'Docker tag'
required: true
default: 'latest'
jobs:
php_syntax_errors:
name: 1⃣ PHP Code Style errors
if: github.event_name == 'release'
runs-on: ubuntu-latest
steps:
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.4
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
uses: ramsey/composer-install@v2
- name: Check source code for syntax errors
run: ./vendor/bin/pint --test
tests:
name: 2⃣ PHP Tests
if: github.event_name == 'release'
needs:
- php_syntax_errors
runs-on: ubuntu-latest
strategy:
matrix:
php-version:
- 8.4
env:
extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_mysql, zip
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP Action
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: ${{ env.extensions }}
coverage: xdebug
tools: pecl, composer
- name: Install Composer dependencies
uses: ramsey/composer-install@v2
- name: Use Node.js 24
uses: actions/setup-node@v4
with:
node-version: 24
- name: Install
run: npm install
- name: Compile Front-end
run: npm run build
- name: Apply tests ${{ matrix.php-version }}
run: php artisan test
release_artifact_build:
name: 🏗️ Build / Upload - Release File
if: github.event_name == 'release'
needs:
- tests
runs-on: ubuntu-latest
env:
extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_mysql, zip
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.4
extensions: ${{ env.extensions }}
coverage: none
- name: Install Composer dependencies
uses: ramsey/composer-install@v2
with:
composer-options: --no-dev
- name: Use Node.js 24
uses: actions/setup-node@v4
with:
node-version: 24
- name: Install
run: npm install
- name: Compile Front-end
run: npm run build
- name: Build Dist
run: |
make clean dist
- name: Upload package
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ github.token }}
file: InvoiceShelf.zip
asset_name: InvoiceShelf.zip
tag: ${{ github.ref }}
overwrite: true
release_docker_build:
name: 🐳 Release Docker Build
if: github.event_name == 'release'
needs:
- tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: invoiceshelf/invoiceshelf
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: docker/production/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
manual_docker_build:
name: 🛠️ Manual Docker Build
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: docker/production/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: invoiceshelf/invoiceshelf:${{ github.event.inputs.tag }}
cache-from: type=gha
cache-to: type=gha,mode=max
nightly_build:
name: 🌙 Nightly Docker Build
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
strategy:
matrix:
branch: [master, develop]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ matrix.branch }}
fetch-depth: 2
- name: Check for recent changes
id: changes
run: |
# Check if there are commits in the last 24 hours
RECENT_COMMITS=$(git log --since="24 hours ago" --oneline | wc -l)
echo "recent_commits=$RECENT_COMMITS" >> $GITHUB_OUTPUT
if [ "$RECENT_COMMITS" -gt 0 ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "has_changes=false" >> $GITHUB_OUTPUT
fi
- name: Set up Docker Buildx
if: steps.changes.outputs.has_changes == 'true'
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
if: steps.changes.outputs.has_changes == 'true'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Set Docker tag
if: steps.changes.outputs.has_changes == 'true'
id: tag
run: |
if [ "${{ matrix.branch }}" = "master" ]; then
echo "tag=nightly" >> $GITHUB_OUTPUT
elif [ "${{ matrix.branch }}" = "develop" ]; then
echo "tag=alpha" >> $GITHUB_OUTPUT
fi
- name: Build and push Docker image
if: steps.changes.outputs.has_changes == 'true'
uses: docker/build-push-action@v5
with:
context: .
file: docker/production/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: invoiceshelf/invoiceshelf:${{ steps.tag.outputs.tag }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: No changes detected
if: steps.changes.outputs.has_changes == 'false'
run: |
echo "No commits found in the last 24 hours for ${{ matrix.branch }} branch. Skipping build."

96
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,96 @@
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
release:
name: Build & Release
runs-on: ubuntu-latest
env:
extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_mysql, zip
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.4
extensions: ${{ env.extensions }}
tools: composer
- name: Install Composer dependencies
run: composer install --no-dev --optimize-autoloader --no-interaction
- name: Use Node.js 24
uses: actions/setup-node@v4
with:
node-version: 24
- name: Install npm dependencies
run: npm ci
- name: Build frontend
run: npm run build
- name: Prepare release directory
run: |
mkdir -p /tmp/InvoiceShelf/public
cp -r app /tmp/InvoiceShelf/
cp -r bootstrap /tmp/InvoiceShelf/
cp -r config /tmp/InvoiceShelf/
cp -r database /tmp/InvoiceShelf/
cp -r public/build /tmp/InvoiceShelf/public/
cp -r public/favicons /tmp/InvoiceShelf/public/
cp public/.htaccess /tmp/InvoiceShelf/public/
cp public/index.php /tmp/InvoiceShelf/public/
cp public/robots.txt /tmp/InvoiceShelf/public/
cp public/web.config /tmp/InvoiceShelf/public/
cp -r resources /tmp/InvoiceShelf/
cp -r lang /tmp/InvoiceShelf/
cp -r routes /tmp/InvoiceShelf/
cp -r storage /tmp/InvoiceShelf/
cp -r vendor /tmp/InvoiceShelf/ 2>/dev/null || true
cp -r scripts /tmp/InvoiceShelf/
cp version.md /tmp/InvoiceShelf/
cp .env.example /tmp/InvoiceShelf/
cp artisan /tmp/InvoiceShelf/
cp composer.json /tmp/InvoiceShelf/
cp composer.lock /tmp/InvoiceShelf/
cp LICENSE /tmp/InvoiceShelf/
cp readme.md /tmp/InvoiceShelf/
cp SECURITY.md /tmp/InvoiceShelf/
cp server.php /tmp/InvoiceShelf/
# Clean up runtime artifacts
find /tmp/InvoiceShelf -wholename '*/[Tt]ests/*' -delete
find /tmp/InvoiceShelf -wholename '*/[Tt]est/*' -delete
rm -rf /tmp/InvoiceShelf/storage/framework/cache/data/* 2>/dev/null || true
rm -f /tmp/InvoiceShelf/storage/framework/sessions/* 2>/dev/null || true
rm -f /tmp/InvoiceShelf/storage/framework/views/* 2>/dev/null || true
rm -f /tmp/InvoiceShelf/storage/logs/* 2>/dev/null || true
touch /tmp/InvoiceShelf/storage/logs/laravel.log
- name: Generate manifest
run: php scripts/generate-manifest.php /tmp/InvoiceShelf
- name: Create zip
working-directory: /tmp
run: zip -r InvoiceShelf.zip InvoiceShelf/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: /tmp/InvoiceShelf.zip
generate_release_notes: true
make_latest: true

5
.gitignore vendored
View File

@@ -14,11 +14,16 @@ Homestead.yaml
.rnd
/.expo
/.vscode
/.devcontainer
.gitkeep
/public/docs
/.scribe
!storage/fonts/.gitkeep
.DS_Store
.php-cs-fixer.cache
.devenvconfig
/storage/fonts*
package-lock.json
/docker/development/docker-compose.yml
/docker/production/docker-compose.yml
/docker-compose.yaml

1
.node-version Normal file
View File

@@ -0,0 +1 @@
24

5909
.phpstorm.meta.php Normal file

File diff suppressed because it is too large Load Diff

234
AGENTS.md Normal file
View File

@@ -0,0 +1,234 @@
<laravel-boost-guidelines>
=== foundation rules ===
# Laravel Boost Guidelines
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.4
- laravel/framework (LARAVEL) - v13
- laravel/prompts (PROMPTS) - v0
- laravel/sanctum (SANCTUM) - v4
- phpunit/phpunit (PHPUNIT) - v12
- laravel/boost (BOOST) - v2
- laravel/mcp (MCP) - v0
- laravel/pint (PINT) - v1
- laravel/sail (SAIL) - v1
- pestphp/pest (PEST) - v4
- vue (VUE) - v3
- eslint (ESLINT) - v9
- prettier (PRETTIER) - v3
- tailwindcss (TAILWINDCSS) - v3
## Skills Activation
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
- `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code.
- `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS.
- `medialibrary-development` — Build and work with spatie/laravel-medialibrary features including associating files with Eloquent models, defining media collections and conversions, generating responsive images, and retrieving media URLs and paths.
## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.
## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
## Application Structure & Architecture
- Stick to existing directory structure; don't create new base folders without approval.
- Do not change the application's dependencies without approval.
## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
## Documentation Files
- You must only create documentation files if explicitly requested by the user.
## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
=== boost rules ===
# Laravel Boost
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
## Artisan Commands
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`, `php artisan tinker --execute "..."`).
- Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
## URLs
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
## Debugging
- Use the `database-query` tool when you only need to read from the database.
- Use the `database-schema` tool to inspect table structure before writing migrations or models.
- To execute PHP code for debugging, run `php artisan tinker --execute "your code here"` directly.
- To read configuration values, read the config files directly or run `php artisan config:show [key]`.
- To inspect routes, run `php artisan route:list` directly.
- To check environment variables, read the `.env` file directly.
## Reading Browser Logs With the `browser-logs` Tool
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
- Only recent browser logs will be useful - ignore old logs.
## Searching Documentation (Critically Important)
- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
- Search the documentation before making code changes to ensure we are taking the correct approach.
- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
### Available Search Syntax
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
=== php rules ===
# PHP
- Always use curly braces for control structures, even for single-line bodies.
## Constructors
- Use PHP 8 constructor property promotion in `__construct()`.
- `public function __construct(public GitHub $github) { }`
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
## Type Declarations
- Always use explicit return type declarations for methods and functions.
- Use appropriate PHP type hints for method parameters.
<!-- Explicit Return Types and Method Params -->
```php
protected function isAccessible(User $user, ?string $path = null): bool
{
...
}
```
## Enums
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
## Comments
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
## PHPDoc Blocks
- Add useful array shape type definitions when appropriate.
=== herd rules ===
# Laravel Herd
- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs for the user.
- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd.
=== tests rules ===
# Test Enforcement
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
=== laravel/core rules ===
# Do Things the Laravel Way
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
- If you're creating a generic PHP class, use `php artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
## Database
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
- Use Eloquent models and relationships before suggesting raw database queries.
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
- Generate code that prevents N+1 query problems by using eager loading.
- Use Laravel's query builder for very complex database operations.
### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
### APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
## Controllers & Validation
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
- Check sibling Form Requests to see if the application uses array or string based validation rules.
## Authentication & Authorization
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
## URL Generation
- When generating links to other pages, prefer named routes and the `route()` function.
## Queues
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
## Configuration
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
## Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
## Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
=== pint/core rules ===
# Laravel Pint Code Formatter
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
=== pest/core rules ===
## Pest
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
- Do NOT delete tests without approval.
=== spatie/laravel-medialibrary rules ===
## Media Library
- `spatie/laravel-medialibrary` associates files with Eloquent models, with support for collections, conversions, and responsive images.
- Always activate the `medialibrary-development` skill when working with media uploads, conversions, collections, responsive images, or any code that uses the `HasMedia` interface or `InteractsWithMedia` trait.
</laravel-boost-guidelines>

80
CLAUDE.md Normal file
View File

@@ -0,0 +1,80 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
InvoiceShelf is an open-source invoicing and expense tracking application built with Laravel 13 (PHP 8.4) and Vue 3. It supports multi-company tenancy, customer portals, recurring invoices, and PDF generation.
## Common Commands
### Development
```bash
composer run dev # Starts PHP server, queue listener, log tail, and Vite dev server concurrently
npm run dev # Vite dev server only
npm run build # Production frontend build
```
### Testing
```bash
php artisan test --compact # Run all tests
php artisan test --compact --filter=testName # Run specific test
./vendor/bin/pest --stop-on-failure # Run via Pest directly
make test # Makefile shortcut
```
Tests use SQLite in-memory DB, configured in `phpunit.xml`. Tests seed via `DatabaseSeeder` + `DemoSeeder` in `beforeEach`. Authenticate with `Sanctum::actingAs()` and set the `company` header.
### Code Style
```bash
vendor/bin/pint --dirty --format agent # Fix style on modified PHP files
vendor/bin/pint --test # Check style without fixing (CI uses this)
```
### Artisan Generators
Always use `php artisan make:*` with `--no-interaction` to create new files (models, controllers, migrations, tests, etc.).
## Architecture
### Multi-Tenancy
Every major model has a `company_id` foreign key. The `CompanyMiddleware` sets the active company from the `company` request header. Bouncer authorization is scoped to the company level via `DefaultScope` (`app/Bouncer/Scopes/DefaultScope.php`).
### Authentication
Three guards: `web` (session), `api` (Sanctum tokens for `/api/v1/`), `customer` (session for customer portal). API routes use `auth:sanctum` middleware; customer portal uses `auth:customer`.
### Routing
- **API**: All endpoints under `/api/v1/` in `routes/api.php`, grouped with `auth:sanctum`, `company`, and `bouncer` middleware
- **Web**: `routes/web.php` serves PDF endpoints, auth pages, and catch-all SPA routes (`/admin/{vue?}`, `/{company:slug}/customer/{vue?}`)
### Frontend
- Entry point: `resources/scripts/main.js`
- Vue Router: `resources/scripts/admin/admin-router.js` (admin), `resources/scripts/customer/customer-router.js` (customer portal)
- State: Pinia stores in `resources/scripts/admin/stores/`
- Path aliases: `@` = `resources/`, `$fonts`, `$images` for static assets
- Vite dev server expects `invoiceshelf.test` hostname
### Backend Patterns
- **Authorization**: Silber/Bouncer with policies in `app/Policies/`. Controllers use `$this->authorize()`.
- **Validation**: Form Request classes, never inline validation
- **API responses**: Eloquent API Resources in `app/Http/Resources/`
- **PDF generation**: DomPDF (`GeneratesPdfTrait`) or Gotenberg
- **Email**: Mailable classes with `EmailLog` tracking
- **File storage**: Spatie MediaLibrary, supports local/S3/Dropbox
- **Serial numbers**: `SerialNumberFormatter` service
- **Company settings**: `CompanySetting` model (key-value per company)
### Database
Supports MySQL, PostgreSQL, and SQLite. Prefer Eloquent over raw queries. Use `Model::query()` instead of `DB::`. Use eager loading to prevent N+1 queries.
## Code Conventions
- PHP: snake_case, constructor property promotion, explicit return types, PHPDoc blocks over inline comments
- JS: camelCase
- Always check sibling files for patterns before creating new ones
- Use `config()` helper, never `env()` outside config files
- Every change must have tests (feature tests preferred over unit tests)
- Run `vendor/bin/pint --dirty --format agent` after modifying PHP files
## CI Pipeline
GitHub Actions (`check.yaml`): runs Pint style check, then builds frontend and runs Pest tests on PHP 8.4.

View File

@@ -29,14 +29,16 @@ dist-gen: clean composer npm-build
@cp -r routes InvoiceShelf
@cp -r storage InvoiceShelf
@cp -r vendor InvoiceShelf 2> /dev/null || true
@cp -r scripts InvoiceShelf
@cp -r version.md InvoiceShelf
@cp -r .env.example InvoiceShelf
@cp -r artisan InvoiceShelf
@cp -r composer.json InvoiceShelf
@cp -r composer.lock InvoiceShelf
@cp -r server.php InvoiceShelf
@cp -r LICENSE InvoiceShelf
@cp -r readme.md InvoiceShelf
@cp -r SECURITY.md InvoiceShelf
@cp -r server.php InvoiceShelf
@touch InvoiceShelf/storage/logs/laravel.log
dist-clean: dist-gen
@@ -46,6 +48,7 @@ dist-clean: dist-gen
@rm InvoiceShelf/storage/framework/sessions/* 2> /dev/null || true
@rm InvoiceShelf/storage/framework/views/* 2> /dev/null || true
@rm InvoiceShelf/storage/logs/* 2> /dev/null || true
@php scripts/generate-manifest.php InvoiceShelf
dist: dist-clean
@zip -r InvoiceShelf.zip InvoiceShelf

View File

@@ -2,4 +2,6 @@
## Reporting a Vulnerability
Please email security@invoiceshelf.com to report any security vulnerabilities. We will acknowledge receipt of your vulnerability and strive to send you regular updates about our progress. If you're curious about the status of your disclosure please feel free to email us again.
Please email **security@invoiceshelf.com** and cc **security@griffin-web.studio** to report any security vulnerabilities. In the unlikely event that you havent heard back, try reaching out on Discord to one of our moderators.
We will acknowledge receipt of your report and strive to provide regular updates on our progress. If you're curious about the status of your disclosure, please feel free to email us again.

File diff suppressed because it is too large Load Diff

1629
_ide_helper_models.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
<?php
namespace InvoiceShelf\Bouncer\Scopes;
namespace App\Bouncer\Scopes;
use Silber\Bouncer\Database\Scope\Scope;

View File

@@ -1,10 +1,10 @@
<?php
namespace InvoiceShelf\Console\Commands;
namespace App\Console\Commands;
use App\Models\Estimate;
use Carbon\Carbon;
use Illuminate\Console\Command;
use InvoiceShelf\Models\Estimate;
class CheckEstimateStatus extends Command
{
@@ -37,7 +37,7 @@ class CheckEstimateStatus extends Command
*
* @return mixed
*/
public function handle()
public function handle(): void
{
$date = Carbon::now();
$status = [Estimate::STATUS_ACCEPTED, Estimate::STATUS_REJECTED, Estimate::STATUS_EXPIRED];

View File

@@ -1,10 +1,10 @@
<?php
namespace InvoiceShelf\Console\Commands;
namespace App\Console\Commands;
use App\Models\Invoice;
use Carbon\Carbon;
use Illuminate\Console\Command;
use InvoiceShelf\Models\Invoice;
class CheckInvoiceStatus extends Command
{
@@ -37,7 +37,7 @@ class CheckInvoiceStatus extends Command
*
* @return mixed
*/
public function handle()
public function handle(): void
{
$date = Carbon::now();
$invoices = Invoice::whereNotIn('status', [Invoice::STATUS_COMPLETED, Invoice::STATUS_DRAFT])

View File

@@ -1,9 +1,12 @@
<?php
namespace InvoiceShelf\Console\Commands;
namespace App\Console\Commands;
use App\Space\PdfTemplateUtils;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class CreateTemplateCommand extends Command
{
@@ -33,32 +36,57 @@ class CreateTemplateCommand extends Command
/**
* Execute the console command.
*
* @return int
*/
public function handle()
public function handle(): int
{
$templateName = $this->argument('name');
$type = $this->option('type');
$templateType = $this->option('type');
if (! $type) {
$type = $this->choice('Create a template for?', ['invoice', 'estimate']);
if (! $templateType) {
$templateType = $this->choice('Create a template for?', ['invoice', 'estimate']);
}
if (Storage::disk('views')->exists("/app/pdf/{$type}/{$templateName}.blade.php")) {
if (PdfTemplateUtils::customTemplateFileExists($templateType, sprintf('%s.blade.php', $templateName))) {
$this->info('Template with given name already exists.');
return 0;
return self::INVALID;
}
Storage::disk('views')->copy("/app/pdf/{$type}/{$type}1.blade.php", "/app/pdf/{$type}/{$templateName}.blade.php");
copy(public_path("/build/img/PDF/{$type}1.png"), public_path("/build/img/PDF/{$templateName}.png"));
copy(resource_path("/static/img/PDF/{$type}1.png"), resource_path("/static/img/PDF/{$templateName}.png"));
if (! PdfTemplateUtils::toCustomTemplateMarkupFile(
Str::replace(
sprintf('app.pdf.%s', $templateType),
sprintf('pdf_templates::%s', $templateType),
Storage::disk('views')->get("/app/pdf/{$templateType}/{$templateType}1.blade.php"),
),
$templateType,
$templateName
)) {
$this->error(sprintf('Unable to create %s template.', ucfirst($templateType)));
$path = resource_path("views/app/pdf/{$type}/{$templateName}.blade.php");
$type = ucfirst($type);
$this->info("{$type} Template created successfully at ".$path);
return self::FAILURE;
}
return 0;
PdfTemplateUtils::toCustomTemplateImageFile(
File::get(resource_path("static/img/PDF/{$templateType}1.png")),
$templateType,
$templateName,
);
if (! PdfTemplateUtils::customTemplateFileExists($templateType, 'partials/table.blade.php')) {
PdfTemplateUtils::toCustomTemplateFile(
Storage::disk('views')->get("/app/pdf/{$templateType}/partials/table.blade.php"),
$templateType,
'partials/table.blade.php'
);
}
$this->info(
sprintf('%s Template created successfully at %s',
ucfirst($templateType),
PdfTemplateUtils::getCustomTemplateFilePath($templateType, sprintf('%s.blade.php', $templateName))
)
);
return self::SUCCESS;
}
}

View File

@@ -1,9 +1,9 @@
<?php
namespace InvoiceShelf\Console\Commands;
namespace App\Console\Commands;
use App\Space\ModuleInstaller;
use Illuminate\Console\Command;
use InvoiceShelf\Space\ModuleInstaller;
class InstallModuleCommand extends Command
{
@@ -33,10 +33,8 @@ class InstallModuleCommand extends Command
/**
* Execute the console command.
*
* @return int
*/
public function handle()
public function handle(): int
{
ModuleInstaller::complete($this->argument('module'), $this->argument('version'));

View File

@@ -1,11 +1,13 @@
<?php
namespace InvoiceShelf\Console\Commands;
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Support\Facades\Artisan;
use function Laravel\Prompts\confirm;
class ResetApp extends Command
{
use ConfirmableTrait;
@@ -22,7 +24,7 @@ class ResetApp extends Command
*
* @var string
*/
protected $description = 'Clean database, database_created and public/storage folder';
protected $description = 'Clean database and public/storage folder';
/**
* Create a new command instance.
@@ -39,30 +41,49 @@ class ResetApp extends Command
*
* @return mixed
*/
public function handle()
/**
* Execute the console command to reset the application.
*
* This will:
* 1. Enable maintenance mode to prevent access during reset
* 2. Fresh migrate the database with initial seeds
* 3. Seed demo data using DemoSeeder
* 4. Clear all application caches
* 5. Disable maintenance mode
*
* The --force flag can be used to skip confirmation prompt.
*/
public function handle(): void
{
if (! $this->confirmToProceed()) {
return;
if (! $this->option('force')) {
if (! confirm('Are you sure you want to reset the application?')) {
$this->components->error('Reset cancelled');
return;
}
}
$this->info('Running migrate:fresh');
// Enable maintenance mode to prevent access during reset
$this->info('Activating maintenance mode...');
Artisan::call('down');
// Fresh migrate database and run initial seeds
$this->info('Running migrate:fresh');
Artisan::call('migrate:fresh --seed --force');
// Seed demo data
$this->info('Seeding database');
Artisan::call('db:seed', ['--class' => 'DemoSeeder', '--force' => true]);
$path = base_path('.env');
// Clear all application caches
$this->info('Clearing cache...');
Artisan::call('optimize:clear');
if (file_exists($path)) {
file_put_contents($path, str_replace(
'APP_DEBUG=true',
'APP_DEBUG=false',
file_get_contents($path)
));
}
// Disable maintenance mode
$this->info('Deactivating maintenance mode...');
Artisan::call('up');
$this->info('App has been reset successfully');
$this->info('App reset completed successfully!');
}
}

View File

@@ -1,10 +1,10 @@
<?php
namespace InvoiceShelf\Console\Commands;
namespace App\Console\Commands;
use App\Space\Updater;
use Illuminate\Console\Command;
use InvoiceShelf\Models\Setting;
use InvoiceShelf\Space\Updater;
use Illuminate\Support\Facades\File;
// Implementation taken from Akaunting - https://github.com/akaunting/akaunting
class UpdateCommand extends Command
@@ -42,7 +42,7 @@ class UpdateCommand extends Command
/**
* Execute the console command.
*/
public function handle()
public function handle(): void
{
set_time_limit(3600); // 1 hour
@@ -98,7 +98,7 @@ class UpdateCommand extends Command
public function getInstalledVersion()
{
return Setting::getSetting('version');
return preg_replace('~[\r\n]+~', '', File::get(base_path('version.md')));
}
public function getLatestVersionResponse()

View File

@@ -1,60 +0,0 @@
<?php
namespace InvoiceShelf\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use InvoiceShelf\Models\CompanySetting;
use InvoiceShelf\Models\RecurringInvoice;
use InvoiceShelf\Space\InstallUtils;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
Commands\ResetApp::class,
Commands\UpdateCommand::class,
Commands\CreateTemplateCommand::class,
Commands\InstallModuleCommand::class,
];
/**
* Define the application's command schedule.
*
* @return void
*/
protected function schedule(Schedule $schedule)
{
if (InstallUtils::isDbCreated()) {
$schedule->command('check:invoices:status')
->daily();
$schedule->command('check:estimates:status')
->daily();
$recurringInvoices = RecurringInvoice::where('status', 'ACTIVE')->get();
foreach ($recurringInvoices as $recurringInvoice) {
$timeZone = CompanySetting::getSetting('time_zone', $recurringInvoice->company_id);
$schedule->call(function () use ($recurringInvoice) {
$recurringInvoice->generateInvoice();
})->cron($recurringInvoice->frequency)->timezone($timeZone);
}
}
}
/**
* Register the Closure based commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace InvoiceShelf\Events;
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;

View File

@@ -1,6 +1,6 @@
<?php
namespace InvoiceShelf\Events;
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;

View File

@@ -1,6 +1,6 @@
<?php
namespace InvoiceShelf\Events;
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;

View File

@@ -1,6 +1,6 @@
<?php
namespace InvoiceShelf\Events;
namespace App\Events;
use Illuminate\Foundation\Events\Dispatchable;

View File

@@ -1,51 +0,0 @@
<?php
namespace InvoiceShelf\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @return void
*/
public function report(Throwable $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function render($request, Throwable $exception)
{
return parent::render($request, $exception);
}
}

22
app/Facades/Hashids.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
/**
* @method static string encode(mixed ...$numbers)
* @method static array decode(string $hash)
* @method static string encodeHex(string $str)
* @method static string decodeHex(string $hash)
* @method static \Hashids\Hashids connection(string|null $name = null)
*/
class Hashids extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'hashids';
}
}

16
app/Facades/PDF.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
/**
* @method static \Psr\Http\Message\ResponseInterface loadView(string $template)
*/
class PDF extends Facade
{
protected static function getFacadeAccessor()
{
return 'pdf.driver';
}
}

View File

@@ -1,10 +1,10 @@
<?php
namespace InvoiceShelf\Generators;
namespace App\Generators;
use InvoiceShelf\Models\Estimate;
use InvoiceShelf\Models\Invoice;
use InvoiceShelf\Models\Payment;
use App\Models\Estimate;
use App\Models\Invoice;
use App\Models\Payment;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\MediaLibrary\Support\PathGenerator\PathGenerator;

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Hashids;
use Hashids\Hashids;
use Illuminate\Support\Arr;
class HashidsFactory
{
public function make(array $config): Hashids
{
$config = $this->getConfig($config);
return $this->getClient($config);
}
/**
* @return array{salt: string, length: int, alphabet: string}
*/
protected function getConfig(array $config): array
{
return [
'salt' => Arr::get($config, 'salt', ''),
'length' => Arr::get($config, 'length', 0),
'alphabet' => Arr::get($config, 'alphabet', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'),
];
}
protected function getClient(array $config): Hashids
{
return new Hashids($config['salt'], $config['length'], $config['alphabet']);
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Hashids;
use Hashids\Hashids;
use Illuminate\Contracts\Config\Repository;
use InvalidArgumentException;
/**
* Resolves named Hashids clients from config/hashids.php (one "connection" per model class).
*
* @method string encode(mixed ...$numbers)
* @method array decode(string $hash)
* @method string encodeHex(string $str)
* @method string decodeHex(string $hash)
*/
class HashidsManager
{
/**
* @var array<string, Hashids>
*/
protected array $connections = [];
public function __construct(
protected Repository $config,
protected HashidsFactory $factory
) {}
public function connection(?string $name = null): Hashids
{
$name = $name ?? $this->getDefaultConnection();
if (! isset($this->connections[$name])) {
$this->connections[$name] = $this->factory->make(
$this->getConnectionConfig($name)
);
}
return $this->connections[$name];
}
public function getDefaultConnection(): string
{
return (string) $this->config->get('hashids.default');
}
public function getFactory(): HashidsFactory
{
return $this->factory;
}
/**
* @return array<string, mixed>
*/
protected function getConnectionConfig(string $name): array
{
$connections = $this->config->get('hashids.connections', []);
if (! is_array($connections) || ! isset($connections[$name])) {
throw new InvalidArgumentException("Hashids connection [{$name}] not configured.");
}
return $connections[$name];
}
public function __call(string $method, array $parameters): mixed
{
return $this->connection()->$method(...$parameters);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Hashids;
use Hashids\Hashids as HashidsClient;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\ServiceProvider;
class HashidsServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton('hashids.factory', function () {
return new HashidsFactory;
});
$this->app->alias('hashids.factory', HashidsFactory::class);
$this->app->singleton('hashids', function (Container $app) {
return new HashidsManager($app['config'], $app['hashids.factory']);
});
$this->app->alias('hashids', HashidsManager::class);
$this->app->bind('hashids.connection', function (Container $app) {
return $app['hashids']->connection();
});
$this->app->alias('hashids.connection', HashidsClient::class);
}
/**
* @return list<string>
*/
public function provides(): array
{
return [
'hashids',
'hashids.factory',
'hashids.connection',
];
}
}

View File

@@ -1,23 +1,32 @@
<?php
namespace InvoiceShelf\Http\Controllers;
namespace App\Http\Controllers;
use App\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use InvoiceShelf\Models\Setting;
use Illuminate\Support\Facades\File;
class AppVersionController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return JsonResponse
*/
public function __invoke(Request $request)
{
$version = Setting::getSetting('version');
$version = preg_replace('~[\r\n]+~', '', File::get(base_path('version.md')));
$channel = Setting::getSetting('updater_channel');
if (is_null($channel)) {
$channel = 'stable';
Setting::setSetting('updater_channel', 'stable'); // default.
}
return response()->json([
'version' => $version,
'channel' => $channel,
]);
}
}

View File

@@ -1,13 +1,13 @@
<?php
namespace InvoiceShelf\Http\Controllers;
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
abstract class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;

View File

@@ -1,10 +1,10 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Auth;
namespace App\Http\Controllers\V1\Admin\Auth;
use App\Http\Controllers\Controller;
use App\Providers\AppServiceProvider;
use Illuminate\Foundation\Auth\ConfirmsPasswords;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Providers\RouteServiceProvider;
class ConfirmPasswordController extends Controller
{
@@ -26,7 +26,7 @@ class ConfirmPasswordController extends Controller
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
protected $redirectTo = AppServiceProvider::HOME;
/**
* Create a new controller instance.

View File

@@ -1,10 +1,12 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Auth;
namespace App\Http\Controllers\V1\Admin\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
class ForgotPasswordController extends Controller
{
@@ -25,7 +27,7 @@ class ForgotPasswordController extends Controller
* Get the response for a successful password reset link.
*
* @param string $response
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
* @return RedirectResponse|JsonResponse
*/
protected function sendResetLinkResponse(Request $request, $response)
{
@@ -39,7 +41,7 @@ class ForgotPasswordController extends Controller
* Get the response for a failed password reset link.
*
* @param string $response
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
* @return RedirectResponse|JsonResponse
*/
protected function sendResetLinkFailedResponse(Request $request, $response)
{

View File

@@ -1,10 +1,10 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Auth;
namespace App\Http\Controllers\V1\Admin\Auth;
use App\Http\Controllers\Controller;
use App\Providers\AppServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Providers\RouteServiceProvider;
class LoginController extends Controller
{
@@ -26,7 +26,7 @@ class LoginController extends Controller
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
protected $redirectTo = AppServiceProvider::HOME;
/**
* Create a new controller instance.

View File

@@ -1,12 +1,12 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Auth;
namespace App\Http\Controllers\V1\Admin\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Providers\AppServiceProvider;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Validator;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Models\User;
use InvoiceShelf\Providers\RouteServiceProvider;
class RegisterController extends Controller
{
@@ -28,7 +28,7 @@ class RegisterController extends Controller
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
protected $redirectTo = AppServiceProvider::HOME;
/**
* Create a new controller instance.

View File

@@ -1,13 +1,16 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Auth;
namespace App\Http\Controllers\V1\Admin\Auth;
use App\Http\Controllers\Controller;
use App\Providers\AppServiceProvider;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Contracts\Auth\CanResetPassword;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Providers\RouteServiceProvider;
class ResetPasswordController extends Controller
{
@@ -29,13 +32,13 @@ class ResetPasswordController extends Controller
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
protected $redirectTo = AppServiceProvider::HOME;
/**
* Get the response for a successful password reset.
*
* @param string $response
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
* @return RedirectResponse|JsonResponse
*/
protected function sendResetResponse(Request $request, $response)
{
@@ -47,7 +50,7 @@ class ResetPasswordController extends Controller
/**
* Reset the given user's password.
*
* @param \Illuminate\Contracts\Auth\CanResetPassword $user
* @param CanResetPassword $user
* @param string $password
* @return void
*/
@@ -66,7 +69,7 @@ class ResetPasswordController extends Controller
* Get the response for a failed password reset.
*
* @param string $response
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
* @return RedirectResponse|JsonResponse
*/
protected function sendResetFailedResponse(Request $request, $response)
{

View File

@@ -1,10 +1,10 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Auth;
namespace App\Http\Controllers\V1\Admin\Auth;
use App\Http\Controllers\Controller;
use App\Providers\AppServiceProvider;
use Illuminate\Foundation\Auth\VerifiesEmails;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Providers\RouteServiceProvider;
class VerificationController extends Controller
{
@@ -26,7 +26,7 @@ class VerificationController extends Controller
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
protected $redirectTo = AppServiceProvider::HOME;
/**
* Create a new controller instance.

View File

@@ -2,10 +2,10 @@
// Implementation taken from nova-backup-tool - https://github.com/spatie/nova-backup-tool/
namespace InvoiceShelf\Http\Controllers\V1\Admin\Backup;
namespace App\Http\Controllers\V1\Admin\Backup;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use InvoiceShelf\Http\Controllers\Controller;
class ApiController extends Controller
{

View File

@@ -2,13 +2,14 @@
// Implementation taken from nova-backup-tool - https://github.com/spatie/nova-backup-tool/
namespace InvoiceShelf\Http\Controllers\V1\Admin\Backup;
namespace App\Http\Controllers\V1\Admin\Backup;
use App\Jobs\CreateBackupJob;
use App\Models\FileDisk;
use App\Rules\Backup\PathToZip;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use InvoiceShelf\Jobs\CreateBackupJob;
use InvoiceShelf\Rules\Backup\PathToZip;
use Spatie\Backup\BackupDestination\Backup;
use Spatie\Backup\BackupDestination\BackupDestination;
use Spatie\Backup\Helpers\Format;
@@ -27,6 +28,16 @@ class BackupsController extends ApiController
$configuredBackupDisks = config('backup.backup.destination.disks');
try {
if ($request->file_disk_id) {
$fileDisk = FileDisk::find($request->file_disk_id);
if ($fileDisk) {
$fileDisk->setConfig();
$prefix = env('DYNAMIC_DISK_PREFIX', 'temp_');
config(['backup.backup.destination.disks' => [$prefix.$fileDisk->driver]]);
$configuredBackupDisks = config('backup.backup.destination.disks');
}
}
$backupDestination = BackupDestination::create(config('filesystems.default'), config('backup.backup.name'));
$backups = Cache::remember("backups-{$request->file_disk_id}", now()->addSeconds(4), function () use ($backupDestination) {
@@ -65,7 +76,10 @@ class BackupsController extends ApiController
{
$this->authorize('manage backups');
dispatch(new CreateBackupJob($request->all()))->onQueue(config('backup.queue.name'));
$data = $request->all();
$data['company'] = $request->header('company');
dispatch(new CreateBackupJob($data))->onQueue(config('backup.queue.name'));
return $this->respondSuccess();
}
@@ -80,7 +94,7 @@ class BackupsController extends ApiController
$this->authorize('manage backups');
$validated = $request->validate([
'path' => ['required', new PathToZip()],
'path' => ['required', new PathToZip],
]);
$backupDestination = BackupDestination::create(config('filesystems.default'), config('backup.backup.name'));

View File

@@ -2,10 +2,10 @@
// Implementation taken from nova-backup-tool - https://github.com/spatie/nova-backup-tool/
namespace InvoiceShelf\Http\Controllers\V1\Admin\Backup;
namespace App\Http\Controllers\V1\Admin\Backup;
use App\Rules\Backup\PathToZip;
use Illuminate\Http\Request;
use InvoiceShelf\Rules\Backup\PathToZip;
use Spatie\Backup\BackupDestination\Backup;
use Spatie\Backup\BackupDestination\BackupDestination;
use Symfony\Component\HttpFoundation\Response;
@@ -18,7 +18,7 @@ class DownloadBackupController extends ApiController
$this->authorize('manage backups');
$validated = $request->validate([
'path' => ['required', new PathToZip()],
'path' => ['required', new PathToZip],
]);
$backupDestination = BackupDestination::create(config('filesystems.default'), config('backup.backup.name'));

View File

@@ -1,15 +1,15 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Company;
namespace App\Http\Controllers\V1\Admin\Company;
use App\Facades\Hashids;
use App\Http\Controllers\Controller;
use App\Http\Requests\CompaniesRequest;
use App\Http\Resources\CompanyResource;
use App\Models\Company;
use App\Models\User;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Requests\CompaniesRequest;
use InvoiceShelf\Http\Resources\CompanyResource;
use InvoiceShelf\Models\Company;
use InvoiceShelf\Models\User;
use Silber\Bouncer\BouncerFacade;
use Vinkla\Hashids\Facades\Hashids;
class CompaniesController extends Controller
{
@@ -61,10 +61,10 @@ class CompaniesController extends Controller
$company = Company::find($request->header('company'));
$this->authorize('transfer company ownership', $company);
if ($user->hasCompany($company->id)) {
if (! $user->hasCompany($company->id)) {
return response()->json([
'success' => false,
'message' => 'User does not belongs to this company.',
'message' => 'User does not belong to this company.',
]);
}

View File

@@ -1,18 +1,19 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Company;
namespace App\Http\Controllers\V1\Admin\Company;
use App\Http\Controllers\Controller;
use App\Http\Resources\CompanyResource;
use App\Models\Company;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Resources\CompanyResource;
use InvoiceShelf\Models\Company;
use Illuminate\Http\Response;
class CompanyController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request)
{

View File

@@ -1,16 +1,17 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Config;
namespace App\Http\Controllers\V1\Admin\Config;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use Illuminate\Http\Response;
class FiscalYearsController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request)
{

View File

@@ -1,16 +1,17 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Config;
namespace App\Http\Controllers\V1\Admin\Config;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use Illuminate\Http\Response;
class LanguagesController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request)
{

View File

@@ -1,16 +1,17 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Config;
namespace App\Http\Controllers\V1\Admin\Config;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use Illuminate\Http\Response;
class RetrospectiveEditsController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request)
{

View File

@@ -1,19 +1,20 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\CustomField;
namespace App\Http\Controllers\V1\Admin\CustomField;
use App\Http\Controllers\Controller;
use App\Http\Requests\CustomFieldRequest;
use App\Http\Resources\CustomFieldResource;
use App\Models\CustomField;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Requests\CustomFieldRequest;
use InvoiceShelf\Http\Resources\CustomFieldResource;
use InvoiceShelf\Models\CustomField;
use Illuminate\Http\Response;
class CustomFieldsController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function index(Request $request)
{
@@ -33,7 +34,7 @@ class CustomFieldsController extends Controller
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\CustomFieldRequest $request
* @return \Illuminate\Http\Response
* @return Response
*/
public function store(CustomFieldRequest $request)
{
@@ -48,7 +49,7 @@ class CustomFieldsController extends Controller
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
* @return Response
*/
public function show(CustomField $customField)
{
@@ -60,9 +61,9 @@ class CustomFieldsController extends Controller
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param Request $request
* @param int $id
* @return \Illuminate\Http\Response
* @return Response
*/
public function update(CustomFieldRequest $request, CustomField $customField)
{
@@ -77,7 +78,7 @@ class CustomFieldsController extends Controller
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
* @return Response
*/
public function destroy(CustomField $customField)
{

View File

@@ -1,23 +1,24 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Customer;
namespace App\Http\Controllers\V1\Admin\Customer;
use App\Http\Controllers\Controller;
use App\Http\Resources\CustomerResource;
use App\Models\CompanySetting;
use App\Models\Customer;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\Payment;
use Carbon\Carbon;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Resources\CustomerResource;
use InvoiceShelf\Models\CompanySetting;
use InvoiceShelf\Models\Customer;
use InvoiceShelf\Models\Expense;
use InvoiceShelf\Models\Invoice;
use InvoiceShelf\Models\Payment;
use Illuminate\Http\Response;
class CustomerStatsController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request, Customer $customer)
{
@@ -35,15 +36,16 @@ class CustomerStatsController extends Controller
$start = Carbon::now();
$end = Carbon::now();
$terms = explode('-', $fiscalYear);
$companyStartMonth = intval($terms[0]);
if ($terms[0] <= $start->month) {
$startDate->month($terms[0])->startOfMonth();
$start->month($terms[0])->startOfMonth();
$end->month($terms[0])->endOfMonth();
if ($companyStartMonth <= $start->month) {
$startDate->month($companyStartMonth)->startOfMonth();
$start->month($companyStartMonth)->startOfMonth();
$end->month($companyStartMonth)->endOfMonth();
} else {
$startDate->subYear()->month($terms[0])->startOfMonth();
$start->subYear()->month($terms[0])->startOfMonth();
$end->subYear()->month($terms[0])->endOfMonth();
$startDate->subYear()->month($companyStartMonth)->startOfMonth();
$start->subYear()->month($companyStartMonth)->startOfMonth();
$end->subYear()->month($companyStartMonth)->endOfMonth();
}
if ($request->has('previous_year')) {
@@ -60,7 +62,7 @@ class CustomerStatsController extends Controller
)
->whereCompany()
->whereCustomer($customer->id)
->sum('total') ?? 0
->sum('base_total') ?? 0
);
array_push(
$expenseTotals,
@@ -70,7 +72,7 @@ class CustomerStatsController extends Controller
)
->whereCompany()
->whereUser($customer->id)
->sum('amount') ?? 0
->sum('base_amount') ?? 0
);
array_push(
$receiptTotals,
@@ -80,14 +82,14 @@ class CustomerStatsController extends Controller
)
->whereCompany()
->whereCustomer($customer->id)
->sum('amount') ?? 0
->sum('base_amount') ?? 0
);
array_push(
$netProfits,
($receiptTotals[$i] - $expenseTotals[$i])
);
$i++;
array_push($months, $start->format('M'));
array_push($months, $start->translatedFormat('M'));
$monthCounter++;
$end->startOfMonth();
$start->addMonth()->startOfMonth();
@@ -102,21 +104,21 @@ class CustomerStatsController extends Controller
)
->whereCompany()
->whereCustomer($customer->id)
->sum('total');
->sum('base_total');
$totalReceipts = Payment::whereBetween(
'payment_date',
[$startDate->format('Y-m-d'), $start->format('Y-m-d')]
)
->whereCompany()
->whereCustomer($customer->id)
->sum('amount');
->sum('base_amount');
$totalExpenses = Expense::whereBetween(
'expense_date',
[$startDate->format('Y-m-d'), $start->format('Y-m-d')]
)
->whereCompany()
->whereUser($customer->id)
->sum('amount');
->sum('base_amount');
$netProfit = (int) $totalReceipts - (int) $totalExpenses;
$chartData = [

View File

@@ -1,21 +1,21 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Customer;
namespace App\Http\Controllers\V1\Admin\Customer;
use App\Http\Controllers\Controller;
use App\Http\Requests;
use App\Http\Requests\DeleteCustomersRequest;
use App\Http\Resources\CustomerResource;
use App\Models\Customer;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Requests;
use InvoiceShelf\Http\Requests\DeleteCustomersRequest;
use InvoiceShelf\Http\Resources\CustomerResource;
use InvoiceShelf\Models\Customer;
class CustomersController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function index(Request $request)
{
@@ -26,13 +26,8 @@ class CustomersController extends Controller
$customers = Customer::with('creator')
->whereCompany()
->applyFilters($request->all())
->select(
'customers.*',
DB::raw('sum(invoices.base_due_amount) as base_due_amount'),
DB::raw('sum(invoices.due_amount) as due_amount'),
)
->groupBy('customers.id')
->leftJoin('invoices', 'customers.id', '=', 'invoices.customer_id')
->withSum('invoices as base_due_amount', 'base_due_amount')
->withSum('invoices as due_amount', 'due_amount')
->paginateData($limit);
return CustomerResource::collection($customers)
@@ -44,8 +39,8 @@ class CustomersController extends Controller
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
* @param Request $request
* @return JsonResponse
*/
public function store(Requests\CustomerRequest $request)
{
@@ -59,7 +54,7 @@ class CustomersController extends Controller
/**
* Display the specified resource.
*
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function show(Customer $customer)
{
@@ -71,8 +66,8 @@ class CustomersController extends Controller
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
* @param Request $request
* @return JsonResponse
*/
public function update(Requests\CustomerRequest $request, Customer $customer)
{
@@ -90,14 +85,18 @@ class CustomersController extends Controller
/**
* Remove a list of Customers along side all their resources (ie. Estimates, Invoices, Payments and Addresses)
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
* @param Request $request
* @return JsonResponse
*/
public function delete(DeleteCustomersRequest $request)
{
$this->authorize('delete multiple customers');
Customer::deleteCustomers($request->ids);
$ids = Customer::whereCompany()
->whereIn('id', $request->ids)
->pluck('id');
Customer::deleteCustomers($ids);
return response()->json([
'success' => true,

View File

@@ -1,17 +1,18 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Dashboard;
namespace App\Http\Controllers\V1\Admin\Dashboard;
use App\Http\Controllers\Controller;
use App\Models\Company;
use App\Models\CompanySetting;
use App\Models\Customer;
use App\Models\Estimate;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\Payment;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Models\Company;
use InvoiceShelf\Models\CompanySetting;
use InvoiceShelf\Models\Customer;
use InvoiceShelf\Models\Estimate;
use InvoiceShelf\Models\Expense;
use InvoiceShelf\Models\Invoice;
use InvoiceShelf\Models\Payment;
use Silber\Bouncer\BouncerFacade;
class DashboardController extends Controller
@@ -19,7 +20,7 @@ class DashboardController extends Controller
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function __invoke(Request $request)
{
@@ -40,15 +41,16 @@ class DashboardController extends Controller
$start = Carbon::now();
$end = Carbon::now();
$terms = explode('-', $fiscalYear);
$companyStartMonth = intval($terms[0]);
if ($terms[0] <= $start->month) {
$startDate->month($terms[0])->startOfMonth();
$start->month($terms[0])->startOfMonth();
$end->month($terms[0])->endOfMonth();
if ($companyStartMonth <= $start->month) {
$startDate->month($companyStartMonth)->startOfMonth();
$start->month($companyStartMonth)->startOfMonth();
$end->month($companyStartMonth)->endOfMonth();
} else {
$startDate->subYear()->month($terms[0])->startOfMonth();
$start->subYear()->month($terms[0])->startOfMonth();
$end->subYear()->month($terms[0])->endOfMonth();
$startDate->subYear()->month($companyStartMonth)->startOfMonth();
$start->subYear()->month($companyStartMonth)->startOfMonth();
$end->subYear()->month($companyStartMonth)->endOfMonth();
}
if ($request->has('previous_year')) {
@@ -90,7 +92,7 @@ class DashboardController extends Controller
($receipt_totals[$i] - $expense_totals[$i])
);
$i++;
array_push($months, $start->format('M'));
array_push($months, $start->translatedFormat('M'));
$monthCounter++;
$end->startOfMonth();
$start->addMonth()->startOfMonth();

View File

@@ -1,17 +1,18 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Estimate;
namespace App\Http\Controllers\V1\Admin\Estimate;
use App\Http\Controllers\Controller;
use App\Models\Estimate;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Models\Estimate;
use Illuminate\Http\Response;
class ChangeEstimateStatusController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request, Estimate $estimate)
{

View File

@@ -0,0 +1,133 @@
<?php
namespace App\Http\Controllers\V1\Admin\Estimate;
use App\Facades\Hashids;
use App\Http\Controllers\Controller;
use App\Http\Resources\EstimateResource;
use App\Models\CompanySetting;
use App\Models\Estimate;
use App\Services\SerialNumberFormatter;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CloneEstimateController extends Controller
{
/**
* Mail a specific invoice to the corresponding customer's email address.
*
* @return JsonResponse
*/
public function __invoke(Request $request, Estimate $estimate)
{
$this->authorize('view', $estimate);
$this->authorize('create', Estimate::class);
$date = Carbon::now();
$serial = (new SerialNumberFormatter)
->setModel($estimate)
->setCompany($estimate->company_id)
->setCustomer($estimate->customer_id)
->setNextNumbers();
$due_date = null;
$dueDateEnabled = CompanySetting::getSetting(
'estimate_set_expiry_date_automatically',
$request->header('company')
);
if ($dueDateEnabled === 'YES') {
$dueDateDays = intval(CompanySetting::getSetting(
'estimate_expiry_date_days',
$request->header('company')
));
$due_date = Carbon::now()->addDays($dueDateDays)->format('Y-m-d');
}
$exchange_rate = $estimate->exchange_rate;
$newEstimate = Estimate::create([
'estimate_date' => $date->format('Y-m-d'),
'expiry_date' => $due_date,
'estimate_number' => $serial->getNextNumber(),
'sequence_number' => $serial->nextSequenceNumber,
'customer_sequence_number' => $serial->nextCustomerSequenceNumber,
'reference_number' => $estimate->reference_number,
'customer_id' => $estimate->customer_id,
'company_id' => $request->header('company'),
'template_name' => $estimate->template_name,
'status' => Estimate::STATUS_DRAFT,
'sub_total' => $estimate->sub_total,
'discount' => $estimate->discount,
'discount_type' => $estimate->discount_type,
'discount_val' => $estimate->discount_val,
'total' => $estimate->total,
'due_amount' => $estimate->total,
'tax_per_item' => $estimate->tax_per_item,
'discount_per_item' => $estimate->discount_per_item,
'tax' => $estimate->tax,
'notes' => $estimate->notes,
'exchange_rate' => $exchange_rate,
'base_total' => $estimate->total * $exchange_rate,
'base_discount_val' => $estimate->discount_val * $exchange_rate,
'base_sub_total' => $estimate->sub_total * $exchange_rate,
'base_tax' => $estimate->tax * $exchange_rate,
'base_due_amount' => $estimate->total * $exchange_rate,
'currency_id' => $estimate->currency_id,
'sales_tax_type' => $estimate->sales_tax_type,
'sales_tax_address_type' => $estimate->sales_tax_address_type,
]);
$newEstimate->unique_hash = Hashids::connection(Estimate::class)->encode($newEstimate->id);
$newEstimate->save();
$estimate->load('items.taxes');
$estimateItems = $estimate->items->toArray();
foreach ($estimateItems as $estimateItem) {
$estimateItem['company_id'] = $request->header('company');
$estimateItem['name'] = $estimateItem['name'];
$estimateItem['exchange_rate'] = $exchange_rate;
$estimateItem['base_price'] = $estimateItem['price'] * $exchange_rate;
$estimateItem['base_discount_val'] = $estimateItem['discount_val'] * $exchange_rate;
$estimateItem['base_tax'] = $estimateItem['tax'] * $exchange_rate;
$estimateItem['base_total'] = $estimateItem['total'] * $exchange_rate;
$item = $newEstimate->items()->create($estimateItem);
if (array_key_exists('taxes', $estimateItem) && $estimateItem['taxes']) {
foreach ($estimateItem['taxes'] as $tax) {
$tax['company_id'] = $request->header('company');
if ($tax['amount']) {
$item->taxes()->create($tax);
}
}
}
}
if ($estimate->taxes) {
foreach ($estimate->taxes->toArray() as $tax) {
$tax['company_id'] = $request->header('company');
$newEstimate->taxes()->create($tax);
}
}
if ($estimate->fields()->exists()) {
$customFields = [];
foreach ($estimate->fields as $data) {
$customFields[] = [
'id' => $data->custom_field_id,
'value' => $data->defaultAnswer,
];
}
$newEstimate->addCustomFields($customFields);
}
return new EstimateResource($newEstimate);
}
}

View File

@@ -1,24 +1,25 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Estimate;
namespace App\Http\Controllers\V1\Admin\Estimate;
use App\Facades\Hashids;
use App\Http\Controllers\Controller;
use App\Http\Resources\InvoiceResource;
use App\Models\CompanySetting;
use App\Models\Estimate;
use App\Models\Invoice;
use App\Services\SerialNumberFormatter;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Resources\InvoiceResource;
use InvoiceShelf\Models\CompanySetting;
use InvoiceShelf\Models\Estimate;
use InvoiceShelf\Models\Invoice;
use InvoiceShelf\Services\SerialNumberFormatter;
use Vinkla\Hashids\Facades\Hashids;
class ConvertEstimateController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request, Estimate $estimate, Invoice $invoice)
{
@@ -35,14 +36,14 @@ class ConvertEstimateController extends Controller
);
if ($dueDateEnabled === 'YES') {
$dueDateDays = CompanySetting::getSetting(
$dueDateDays = intval(CompanySetting::getSetting(
'invoice_due_date_days',
$request->header('company')
);
));
$due_date = Carbon::now()->addDays($dueDateDays)->format('Y-m-d');
}
$serial = (new SerialNumberFormatter())
$serial = (new SerialNumberFormatter)
->setModel($invoice)
->setCompany($estimate->company_id)
->setCustomer($estimate->customer_id)

View File

@@ -1,23 +1,25 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Estimate;
namespace App\Http\Controllers\V1\Admin\Estimate;
use App\Http\Controllers\Controller;
use App\Models\Estimate;
use App\Space\PdfTemplateUtils;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Models\Estimate;
class EstimateTemplatesController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return JsonResponse
*/
public function __invoke(Request $request)
{
$this->authorize('viewAny', Estimate::class);
$estimateTemplates = Estimate::estimateTemplates();
$estimateTemplates = PdfTemplateUtils::getFormattedTemplates('estimate');
return response()->json([
'estimateTemplates' => $estimateTemplates,

View File

@@ -1,14 +1,14 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Estimate;
namespace App\Http\Controllers\V1\Admin\Estimate;
use App\Http\Controllers\Controller;
use App\Http\Requests\DeleteEstimatesRequest;
use App\Http\Requests\EstimatesRequest;
use App\Http\Resources\EstimateResource;
use App\Jobs\GenerateEstimatePdfJob;
use App\Models\Estimate;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Requests\DeleteEstimatesRequest;
use InvoiceShelf\Http\Requests\EstimatesRequest;
use InvoiceShelf\Http\Resources\EstimateResource;
use InvoiceShelf\Jobs\GenerateEstimatePdfJob;
use InvoiceShelf\Models\Estimate;
class EstimatesController extends Controller
{
@@ -68,7 +68,11 @@ class EstimatesController extends Controller
{
$this->authorize('delete multiple estimates');
Estimate::destroy($request->ids);
$ids = Estimate::whereCompany()
->whereIn('id', $request->ids)
->pluck('id');
Estimate::destroy($ids);
return response()->json([
'success' => true,

View File

@@ -1,17 +1,18 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Estimate;
namespace App\Http\Controllers\V1\Admin\Estimate;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Requests\SendEstimatesRequest;
use InvoiceShelf\Models\Estimate;
use App\Http\Controllers\Controller;
use App\Http\Requests\SendEstimatesRequest;
use App\Models\Estimate;
use Illuminate\Http\JsonResponse;
class SendEstimateController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function __invoke(SendEstimatesRequest $request, Estimate $estimate)
{

View File

@@ -1,18 +1,19 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Estimate;
namespace App\Http\Controllers\V1\Admin\Estimate;
use App\Http\Controllers\Controller;
use App\Http\Requests\SendEstimatesRequest;
use App\Models\Estimate;
use Illuminate\Http\JsonResponse;
use Illuminate\Mail\Markdown;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Requests\SendEstimatesRequest;
use InvoiceShelf\Models\Estimate;
class SendEstimatePreviewController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function __invoke(SendEstimatesRequest $request, Estimate $estimate)
{

View File

@@ -1,19 +1,20 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\ExchangeRate;
namespace App\Http\Controllers\V1\Admin\ExchangeRate;
use App\Http\Controllers\Controller;
use App\Http\Requests\ExchangeRateProviderRequest;
use App\Http\Resources\ExchangeRateProviderResource;
use App\Models\ExchangeRateProvider;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Requests\ExchangeRateProviderRequest;
use InvoiceShelf\Http\Resources\ExchangeRateProviderResource;
use InvoiceShelf\Models\ExchangeRateProvider;
use Illuminate\Http\Response;
class ExchangeRateProviderController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function index(Request $request)
{
@@ -29,8 +30,8 @@ class ExchangeRateProviderController extends Controller
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @param Request $request
* @return Response
*/
public function store(ExchangeRateProviderRequest $request)
{
@@ -56,7 +57,7 @@ class ExchangeRateProviderController extends Controller
/**
* Display the specified resource.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function show(ExchangeRateProvider $exchangeRateProvider)
{
@@ -68,8 +69,8 @@ class ExchangeRateProviderController extends Controller
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @param Request $request
* @return Response
*/
public function update(ExchangeRateProviderRequest $request, ExchangeRateProvider $exchangeRateProvider)
{
@@ -95,7 +96,7 @@ class ExchangeRateProviderController extends Controller
/**
* Remove the specified resource from storage.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function destroy(ExchangeRateProvider $exchangeRateProvider)
{

View File

@@ -1,18 +1,19 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\ExchangeRate;
namespace App\Http\Controllers\V1\Admin\ExchangeRate;
use App\Http\Controllers\Controller;
use App\Models\Currency;
use App\Models\ExchangeRateProvider;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Models\Currency;
use InvoiceShelf\Models\ExchangeRateProvider;
use Illuminate\Http\Response;
class GetActiveProviderController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request, Currency $currency)
{

View File

@@ -1,15 +1,16 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\ExchangeRate;
namespace App\Http\Controllers\V1\Admin\ExchangeRate;
use App\Http\Controllers\Controller;
use App\Models\CompanySetting;
use App\Models\Currency;
use App\Models\ExchangeRateLog;
use App\Models\ExchangeRateProvider;
use App\Traits\ExchangeRateProvidersTrait;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Models\CompanySetting;
use InvoiceShelf\Models\Currency;
use InvoiceShelf\Models\ExchangeRateLog;
use InvoiceShelf\Models\ExchangeRateProvider;
use InvoiceShelf\Traits\ExchangeRateProvidersTrait;
class GetExchangeRateController extends Controller
{
@@ -18,7 +19,7 @@ class GetExchangeRateController extends Controller
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request, Currency $currency)
{

View File

@@ -1,11 +1,12 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\ExchangeRate;
namespace App\Http\Controllers\V1\Admin\ExchangeRate;
use App\Http\Controllers\Controller;
use App\Models\ExchangeRateProvider;
use App\Traits\ExchangeRateProvidersTrait;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Models\ExchangeRateProvider;
use InvoiceShelf\Traits\ExchangeRateProvidersTrait;
use Illuminate\Http\Response;
class GetSupportedCurrenciesController extends Controller
{
@@ -14,7 +15,7 @@ class GetSupportedCurrenciesController extends Controller
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request)
{

View File

@@ -1,17 +1,18 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\ExchangeRate;
namespace App\Http\Controllers\V1\Admin\ExchangeRate;
use App\Http\Controllers\Controller;
use App\Models\ExchangeRateProvider;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Models\ExchangeRateProvider;
use Illuminate\Http\Response;
class GetUsedCurrenciesController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request)
{

View File

@@ -1,19 +1,21 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Expense;
namespace App\Http\Controllers\V1\Admin\Expense;
use App\ExpensesCategory;
use App\Http\Controllers\Controller;
use App\Http\Requests\ExpenseCategoryRequest;
use App\Http\Resources\ExpenseCategoryResource;
use App\Models\ExpenseCategory;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Requests\ExpenseCategoryRequest;
use InvoiceShelf\Http\Resources\ExpenseCategoryResource;
use InvoiceShelf\Models\ExpenseCategory;
use Illuminate\Http\Response;
class ExpenseCategoriesController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function index(Request $request)
{
@@ -32,8 +34,8 @@ class ExpenseCategoriesController extends Controller
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @param Request $request
* @return Response
*/
public function store(ExpenseCategoryRequest $request)
{
@@ -47,7 +49,7 @@ class ExpenseCategoriesController extends Controller
/**
* Display the specified resource.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function show(ExpenseCategory $category)
{
@@ -59,9 +61,9 @@ class ExpenseCategoriesController extends Controller
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \InvoiceShelf\Models\ExpenseCategory $ExpenseCategory
* @return \Illuminate\Http\Response
* @param Request $request
* @param ExpenseCategory $ExpenseCategory
* @return Response
*/
public function update(ExpenseCategoryRequest $request, ExpenseCategory $category)
{
@@ -75,8 +77,8 @@ class ExpenseCategoriesController extends Controller
/**
* Remove the specified resource from storage.
*
* @param \InvoiceShelf\ExpensesCategory $category
* @return \Illuminate\Http\Response
* @param ExpensesCategory $category
* @return Response
*/
public function destroy(ExpenseCategory $category)
{

View File

@@ -1,20 +1,21 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Expense;
namespace App\Http\Controllers\V1\Admin\Expense;
use App\Http\Controllers\Controller;
use App\Http\Requests\DeleteExpensesRequest;
use App\Http\Requests\ExpenseRequest;
use App\Http\Resources\ExpenseResource;
use App\Models\Expense;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Requests\DeleteExpensesRequest;
use InvoiceShelf\Http\Requests\ExpenseRequest;
use InvoiceShelf\Http\Resources\ExpenseResource;
use InvoiceShelf\Models\Expense;
class ExpensesController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function index(Request $request)
{
@@ -39,7 +40,7 @@ class ExpensesController extends Controller
/**
* Store a newly created resource in storage.
*
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function store(ExpenseRequest $request)
{
@@ -53,7 +54,7 @@ class ExpensesController extends Controller
/**
* Display the specified resource.
*
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function show(Expense $expense)
{
@@ -65,7 +66,7 @@ class ExpensesController extends Controller
/**
* Update the specified resource in storage.
*
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function update(ExpenseRequest $request, Expense $expense)
{
@@ -80,7 +81,11 @@ class ExpensesController extends Controller
{
$this->authorize('delete multiple expenses');
Expense::destroy($request->ids);
$ids = Expense::whereCompany()
->whereIn('id', $request->ids)
->pluck('id');
Expense::destroy($ids);
return response()->json([
'success' => true,

View File

@@ -1,16 +1,17 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Expense;
namespace App\Http\Controllers\V1\Admin\Expense;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Models\Expense;
use App\Http\Controllers\Controller;
use App\Models\Expense;
use Illuminate\Http\JsonResponse;
class ShowReceiptController extends Controller
{
/**
* Retrieve details of an expense receipt from storage.
*
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function __invoke(Expense $expense)
{

View File

@@ -1,18 +1,20 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\Expense;
namespace App\Http\Controllers\V1\Admin\Expense;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Requests\UploadExpenseReceiptRequest;
use InvoiceShelf\Models\Expense;
use App\Http\Controllers\Controller;
use App\Http\Requests\ExpenseRequest;
use App\Http\Requests\UploadExpenseReceiptRequest;
use App\Models\Expense;
use Illuminate\Http\JsonResponse;
class UploadReceiptController extends Controller
{
/**
* Upload the expense receipts to storage.
*
* @param \InvoiceShelf\Http\Requests\ExpenseRequest $request
* @return \Illuminate\Http\JsonResponse
* @param ExpenseRequest $request
* @return JsonResponse
*/
public function __invoke(UploadExpenseReceiptRequest $request, Expense $expense)
{

View File

@@ -1,17 +1,18 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\General;
namespace App\Http\Controllers\V1\Admin\General;
use App\Http\Controllers\Controller;
use App\Http\Resources\CompanyResource;
use App\Http\Resources\UserResource;
use App\Models\Company;
use App\Models\CompanySetting;
use App\Models\Currency;
use App\Models\Module;
use App\Models\Setting;
use App\Traits\GeneratesMenuTrait;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Resources\CompanyResource;
use InvoiceShelf\Http\Resources\UserResource;
use InvoiceShelf\Models\Company;
use InvoiceShelf\Models\CompanySetting;
use InvoiceShelf\Models\Currency;
use InvoiceShelf\Models\Module;
use InvoiceShelf\Models\Setting;
use InvoiceShelf\Traits\GeneratesMenuTrait;
use Silber\Bouncer\BouncerFacade;
class BootstrapController extends Controller
@@ -21,7 +22,7 @@ class BootstrapController extends Controller
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function __invoke(Request $request)
{

View File

@@ -1,22 +1,24 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\General;
namespace App\Http\Controllers\V1\Admin\General;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Requests\BulkExchangeRateRequest;
use InvoiceShelf\Models\CompanySetting;
use InvoiceShelf\Models\Estimate;
use InvoiceShelf\Models\Invoice;
use InvoiceShelf\Models\Payment;
use InvoiceShelf\Models\Tax;
use App\Http\Controllers\Controller;
use App\Http\Requests\BulkExchangeRateRequest;
use App\Models\CompanySetting;
use App\Models\Estimate;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Tax;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class BulkExchangeRateController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @param Request $request
* @return Response
*/
public function __invoke(BulkExchangeRateRequest $request)
{

View File

@@ -1,16 +1,17 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\General;
namespace App\Http\Controllers\V1\Admin\General;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use Illuminate\Http\Response;
class ConfigController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request)
{

View File

@@ -1,18 +1,19 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\General;
namespace App\Http\Controllers\V1\Admin\General;
use App\Http\Controllers\Controller;
use App\Http\Resources\CountryResource;
use App\Models\Country;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Resources\CountryResource;
use InvoiceShelf\Models\Country;
class CountriesController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function __invoke(Request $request)
{

View File

@@ -1,18 +1,19 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\General;
namespace App\Http\Controllers\V1\Admin\General;
use App\Http\Controllers\Controller;
use App\Http\Resources\CurrencyResource;
use App\Models\Currency;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Resources\CurrencyResource;
use InvoiceShelf\Models\Currency;
use Illuminate\Http\Response;
class CurrenciesController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request)
{

View File

@@ -1,17 +1,18 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\General;
namespace App\Http\Controllers\V1\Admin\General;
use App\Http\Controllers\Controller;
use App\Space\DateFormatter;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Space\DateFormatter;
use Illuminate\Http\Response;
class DateFormatsController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request)
{

View File

@@ -1,21 +1,22 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\General;
namespace App\Http\Controllers\V1\Admin\General;
use App\Http\Controllers\Controller;
use App\Models\Currency;
use App\Models\Estimate;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Tax;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Models\Currency;
use InvoiceShelf\Models\Estimate;
use InvoiceShelf\Models\Invoice;
use InvoiceShelf\Models\Payment;
use InvoiceShelf\Models\Tax;
use Illuminate\Http\Response;
class GetAllUsedCurrenciesController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request)
{

View File

@@ -1,26 +1,27 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\General;
namespace App\Http\Controllers\V1\Admin\General;
use App\Http\Controllers\Controller;
use App\Models\Estimate;
use App\Models\Invoice;
use App\Models\Payment;
use App\Services\SerialNumberFormatter;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Models\Estimate;
use InvoiceShelf\Models\Invoice;
use InvoiceShelf\Models\Payment;
use InvoiceShelf\Services\SerialNumberFormatter;
use Illuminate\Http\Response;
class NextNumberController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request, Invoice $invoice, Estimate $estimate, Payment $payment)
{
$key = $request->key;
$nextNumber = null;
$serial = (new SerialNumberFormatter())
$serial = (new SerialNumberFormatter)
->setCompany($request->header('company'))
->setCustomer($request->userId);

View File

@@ -1,19 +1,20 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\General;
namespace App\Http\Controllers\V1\Admin\General;
use App\Http\Controllers\Controller;
use App\Http\Requests\NotesRequest;
use App\Http\Resources\NoteResource;
use App\Models\Note;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Http\Requests\NotesRequest;
use InvoiceShelf\Http\Resources\NoteResource;
use InvoiceShelf\Models\Note;
use Illuminate\Http\Response;
class NotesController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function index(Request $request)
{
@@ -32,8 +33,8 @@ class NotesController extends Controller
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @param Request $request
* @return Response
*/
public function store(NotesRequest $request)
{
@@ -41,13 +42,22 @@ class NotesController extends Controller
$note = Note::create($request->getNotesPayload());
if ($note->is_default) {
Note::where('id', '!=', $note->id)
->where('type', $note->type)
->where('is_default', true)
->update([
'is_default' => false,
]);
}
return new NoteResource($note);
}
/**
* Display the specified resource.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function show(Note $note)
{
@@ -59,8 +69,8 @@ class NotesController extends Controller
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @param Request $request
* @return Response
*/
public function update(NotesRequest $request, Note $note)
{
@@ -68,13 +78,22 @@ class NotesController extends Controller
$note->update($request->getNotesPayload());
if ($note->is_default) {
Note::where('id', '!=', $note->id)
->where('type', $note->type)
->where('is_default', true)
->update([
'is_default' => false,
]);
}
return new NoteResource($note);
}
/**
* Remove the specified resource from storage.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function destroy(Note $note)
{

View File

@@ -1,17 +1,18 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\General;
namespace App\Http\Controllers\V1\Admin\General;
use App\Http\Controllers\Controller;
use App\Services\SerialNumberFormatter;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Services\SerialNumberFormatter;
use Illuminate\Http\Response;
class NumberPlaceholdersController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request)
{

View File

@@ -1,18 +1,19 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\General;
namespace App\Http\Controllers\V1\Admin\General;
use App\Http\Controllers\Controller;
use App\Models\Customer;
use App\Models\User;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Models\Customer;
use InvoiceShelf\Models\User;
use Illuminate\Http\Response;
class SearchController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request)
{
@@ -24,7 +25,8 @@ class SearchController extends Controller
->paginate(10);
if ($user->isOwner()) {
$users = User::applyFilters($request->only(['search']))
$users = User::whereCompany()
->applyFilters($request->only(['search']))
->latest()
->paginate(10);
}

View File

@@ -1,17 +1,18 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\General;
namespace App\Http\Controllers\V1\Admin\General;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Models\User;
use Illuminate\Http\Response;
class SearchUsersController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request)
{

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\V1\Admin\General;
use App\Http\Controllers\Controller;
use App\Space\TimeFormatter;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class TimeFormatsController extends Controller
{
/**
* Handle the incoming request.
*
* @return Response
*/
public function __invoke(Request $request)
{
return response()->json([
'time_formats' => TimeFormatter::get_list(),
]);
}
}

View File

@@ -1,17 +1,18 @@
<?php
namespace InvoiceShelf\Http\Controllers\V1\Admin\General;
namespace App\Http\Controllers\V1\Admin\General;
use App\Http\Controllers\Controller;
use App\Space\TimeZones;
use Illuminate\Http\Request;
use InvoiceShelf\Http\Controllers\Controller;
use InvoiceShelf\Space\TimeZones;
use Illuminate\Http\Response;
class TimezonesController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function __invoke(Request $request)
{

Some files were not shown because too many files have changed in this diff Show More