Compare commits
148 Commits
626-introd
...
v3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c11147e95 | ||
|
|
31a2a66127 | ||
|
|
e44657bf7e | ||
|
|
112cc56922 | ||
|
|
7885bf9d11 | ||
|
|
345bfde306 | ||
|
|
3d79fe1abc | ||
|
|
23d1476870 | ||
|
|
2af31d0e5f | ||
|
|
42ce99eeba | ||
|
|
9174254165 | ||
|
|
1d2cca5837 | ||
|
|
93b04a0c2a | ||
|
|
7743c2e126 | ||
|
|
e6eeacb6d4 | ||
|
|
84725b2dfa | ||
|
|
b2b7a07e0c | ||
|
|
61e1efd81b | ||
|
|
1fb5886d06 | ||
|
|
bd756f2365 | ||
|
|
999ff3e977 | ||
|
|
cb37de6da4 | ||
|
|
119a1712b0 | ||
|
|
f5e876122d | ||
|
|
66f7dce701 | ||
|
|
6fdf10b2b1 | ||
|
|
71388ec6a5 | ||
|
|
064bdf5395 | ||
|
|
f83ec6e78f | ||
|
|
9345d3e525 | ||
|
|
fac9ad8cef | ||
|
|
04952d91ed | ||
|
|
27c60bb6f5 | ||
|
|
53932e9e16 | ||
|
|
5ab9c5f736 | ||
|
|
78ed332d06 | ||
|
|
c5c9677ffc | ||
|
|
ba5c6c39ba | ||
|
|
346e5df7ee | ||
|
|
9c3013bb24 | ||
|
|
5efd1054f4 | ||
|
|
20085cab5d | ||
|
|
ea1fc9b799 | ||
|
|
6dd9ed1232 | ||
|
|
67268ac2b7 | ||
|
|
39c9179888 | ||
|
|
631d838834 | ||
|
|
9638e02eb8 | ||
|
|
c46118be3b | ||
|
|
0093bf4d53 | ||
|
|
25b61b73a0 | ||
|
|
8508e7e1b8 | ||
|
|
9ca998e64a | ||
|
|
c328d1cd10 | ||
|
|
5c0e761dfa | ||
|
|
6106ac8208 | ||
|
|
45f347ebef | ||
|
|
b07a252523 | ||
|
|
b0b7d40c73 | ||
|
|
e64529468c | ||
|
|
2bdfb6a8b6 | ||
|
|
74b4b2df4e | ||
|
|
cab785172e | ||
|
|
97f88eaf2c | ||
|
|
af92a361a5 | ||
|
|
a46cca5cd8 | ||
|
|
812956abcc | ||
|
|
d91f6ff2e3 | ||
|
|
774b2614f0 | ||
|
|
e43e515614 | ||
|
|
2b996d30bf | ||
|
|
991b716b33 | ||
|
|
53a0e1491d | ||
|
|
827c5c8938 | ||
|
|
9c8e4ae558 | ||
|
|
7e15eb7c7a | ||
|
|
a10d4d2de9 | ||
|
|
249b2759b6 | ||
|
|
d7f88d3103 | ||
|
|
960e6d3517 | ||
|
|
7a1e2cd2c3 | ||
|
|
d36fbbbf27 | ||
|
|
2a3774f437 | ||
|
|
dabdd2b417 | ||
|
|
350068706c | ||
|
|
88adfe0e50 | ||
|
|
7fbe3d85a3 | ||
|
|
eb0a588164 | ||
|
|
29b3abd317 | ||
|
|
c85051161b | ||
|
|
c3a59a46db | ||
|
|
fae59221d3 | ||
|
|
afbc6c1db3 | ||
|
|
c49c130e9e | ||
|
|
51f0e6285b | ||
|
|
03afb98452 | ||
|
|
acce67f514 | ||
|
|
8e966965f5 | ||
|
|
e0e302e1cf | ||
|
|
8d3029c877 | ||
|
|
c1994887ef | ||
|
|
6343b4a17f | ||
|
|
8a6c085288 | ||
|
|
92a1baced4 | ||
|
|
4318c59976 | ||
|
|
c3ad718799 | ||
|
|
dee17a1da8 | ||
|
|
00d5abae5f | ||
|
|
5912995164 | ||
|
|
6f095210d6 | ||
|
|
b9e34ff25c | ||
|
|
8e7c48f532 | ||
|
|
20ace694fe | ||
|
|
3f5accc0f0 | ||
|
|
7bb6d9bcc3 | ||
|
|
142899cfd7 | ||
|
|
d505677a74 | ||
|
|
e9ee74cd01 | ||
|
|
0fa1aac748 | ||
|
|
c794f92932 | ||
|
|
c90dd1f2ac | ||
|
|
85b62dfdf8 | ||
|
|
8f29e8f5de | ||
|
|
ece6ce737b | ||
|
|
00599b6943 | ||
|
|
4f47db9258 | ||
|
|
64c481e963 | ||
|
|
0aaf0419c3 | ||
|
|
c0454613a8 | ||
|
|
92872e7e1c | ||
|
|
2191417151 | ||
|
|
5f389ea3b0 | ||
|
|
f76f351244 | ||
|
|
735eef6e9b | ||
|
|
4bfec37a53 | ||
|
|
1ca915a0a3 | ||
|
|
6b5e4878fb | ||
|
|
bbf46577dc | ||
|
|
de06c335ac | ||
|
|
62f31960ab | ||
|
|
129028518d | ||
|
|
e0b8b86e06 | ||
|
|
e208e3ba56 | ||
|
|
0ce88ab817 | ||
|
|
23ff69026e | ||
|
|
55416bc633 | ||
|
|
c469f64c79 | ||
|
|
9432da467e |
5
.claude/settings.json
Normal file
5
.claude/settings.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"frontend-design@claude-plugins-official": true
|
||||
}
|
||||
}
|
||||
@@ -21,3 +21,8 @@ TRUSTED_PROXIES="*"
|
||||
# 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
|
||||
|
||||
# InvoiceShelf marketplace and updater base URL.
|
||||
# Defaults to https://invoiceshelf.com. Override to point at a local website
|
||||
# checkout for development:
|
||||
# INVOICESHELF_BASE_URL=http://invoiceshelf-website.test
|
||||
|
||||
18
.github/release.yml
vendored
Normal file
18
.github/release.yml
vendored
Normal 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:
|
||||
- "*"
|
||||
96
.github/workflows/release.yaml
vendored
Normal file
96
.github/workflows/release.yaml
vendored
Normal 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
|
||||
@@ -64,8 +64,8 @@ namespace PHPSTORM_META {
|
||||
'flare.logger' => \Monolog\Logger::class,
|
||||
'hash' => \Illuminate\Hashing\HashManager::class,
|
||||
'hash.driver' => \Illuminate\Hashing\BcryptHasher::class,
|
||||
'hashids' => \App\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Hashids\HashidsFactory::class,
|
||||
'hashids' => \App\Services\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Services\Hashids\HashidsFactory::class,
|
||||
'log' => \Illuminate\Log\LogManager::class,
|
||||
'mail.manager' => \Illuminate\Mail\MailManager::class,
|
||||
'mailer' => \Illuminate\Mail\Mailer::class,
|
||||
@@ -73,7 +73,7 @@ namespace PHPSTORM_META {
|
||||
'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class,
|
||||
'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class,
|
||||
'migrator' => \Illuminate\Database\Migrations\Migrator::class,
|
||||
'pdf.driver' => \App\Services\PDFService::class,
|
||||
'pdf.driver' => \App\Services\Pdf\PdfService::class,
|
||||
'pipeline' => \Illuminate\Pipeline\Pipeline::class,
|
||||
'queue' => \Illuminate\Queue\QueueManager::class,
|
||||
'queue.connection' => \Illuminate\Queue\SyncQueue::class,
|
||||
@@ -147,8 +147,8 @@ namespace PHPSTORM_META {
|
||||
'flare.logger' => \Monolog\Logger::class,
|
||||
'hash' => \Illuminate\Hashing\HashManager::class,
|
||||
'hash.driver' => \Illuminate\Hashing\BcryptHasher::class,
|
||||
'hashids' => \App\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Hashids\HashidsFactory::class,
|
||||
'hashids' => \App\Services\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Services\Hashids\HashidsFactory::class,
|
||||
'log' => \Illuminate\Log\LogManager::class,
|
||||
'mail.manager' => \Illuminate\Mail\MailManager::class,
|
||||
'mailer' => \Illuminate\Mail\Mailer::class,
|
||||
@@ -156,7 +156,7 @@ namespace PHPSTORM_META {
|
||||
'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class,
|
||||
'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class,
|
||||
'migrator' => \Illuminate\Database\Migrations\Migrator::class,
|
||||
'pdf.driver' => \App\Services\PDFService::class,
|
||||
'pdf.driver' => \App\Services\Pdf\PdfService::class,
|
||||
'pipeline' => \Illuminate\Pipeline\Pipeline::class,
|
||||
'queue' => \Illuminate\Queue\QueueManager::class,
|
||||
'queue.connection' => \Illuminate\Queue\SyncQueue::class,
|
||||
@@ -230,8 +230,8 @@ namespace PHPSTORM_META {
|
||||
'flare.logger' => \Monolog\Logger::class,
|
||||
'hash' => \Illuminate\Hashing\HashManager::class,
|
||||
'hash.driver' => \Illuminate\Hashing\BcryptHasher::class,
|
||||
'hashids' => \App\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Hashids\HashidsFactory::class,
|
||||
'hashids' => \App\Services\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Services\Hashids\HashidsFactory::class,
|
||||
'log' => \Illuminate\Log\LogManager::class,
|
||||
'mail.manager' => \Illuminate\Mail\MailManager::class,
|
||||
'mailer' => \Illuminate\Mail\Mailer::class,
|
||||
@@ -239,7 +239,7 @@ namespace PHPSTORM_META {
|
||||
'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class,
|
||||
'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class,
|
||||
'migrator' => \Illuminate\Database\Migrations\Migrator::class,
|
||||
'pdf.driver' => \App\Services\PDFService::class,
|
||||
'pdf.driver' => \App\Services\Pdf\PdfService::class,
|
||||
'pipeline' => \Illuminate\Pipeline\Pipeline::class,
|
||||
'queue' => \Illuminate\Queue\QueueManager::class,
|
||||
'queue.connection' => \Illuminate\Queue\SyncQueue::class,
|
||||
@@ -313,8 +313,8 @@ namespace PHPSTORM_META {
|
||||
'flare.logger' => \Monolog\Logger::class,
|
||||
'hash' => \Illuminate\Hashing\HashManager::class,
|
||||
'hash.driver' => \Illuminate\Hashing\BcryptHasher::class,
|
||||
'hashids' => \App\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Hashids\HashidsFactory::class,
|
||||
'hashids' => \App\Services\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Services\Hashids\HashidsFactory::class,
|
||||
'log' => \Illuminate\Log\LogManager::class,
|
||||
'mail.manager' => \Illuminate\Mail\MailManager::class,
|
||||
'mailer' => \Illuminate\Mail\Mailer::class,
|
||||
@@ -322,7 +322,7 @@ namespace PHPSTORM_META {
|
||||
'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class,
|
||||
'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class,
|
||||
'migrator' => \Illuminate\Database\Migrations\Migrator::class,
|
||||
'pdf.driver' => \App\Services\PDFService::class,
|
||||
'pdf.driver' => \App\Services\Pdf\PdfService::class,
|
||||
'pipeline' => \Illuminate\Pipeline\Pipeline::class,
|
||||
'queue' => \Illuminate\Queue\QueueManager::class,
|
||||
'queue.connection' => \Illuminate\Queue\SyncQueue::class,
|
||||
@@ -396,8 +396,8 @@ namespace PHPSTORM_META {
|
||||
'flare.logger' => \Monolog\Logger::class,
|
||||
'hash' => \Illuminate\Hashing\HashManager::class,
|
||||
'hash.driver' => \Illuminate\Hashing\BcryptHasher::class,
|
||||
'hashids' => \App\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Hashids\HashidsFactory::class,
|
||||
'hashids' => \App\Services\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Services\Hashids\HashidsFactory::class,
|
||||
'log' => \Illuminate\Log\LogManager::class,
|
||||
'mail.manager' => \Illuminate\Mail\MailManager::class,
|
||||
'mailer' => \Illuminate\Mail\Mailer::class,
|
||||
@@ -405,7 +405,7 @@ namespace PHPSTORM_META {
|
||||
'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class,
|
||||
'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class,
|
||||
'migrator' => \Illuminate\Database\Migrations\Migrator::class,
|
||||
'pdf.driver' => \App\Services\PDFService::class,
|
||||
'pdf.driver' => \App\Services\Pdf\PdfService::class,
|
||||
'pipeline' => \Illuminate\Pipeline\Pipeline::class,
|
||||
'queue' => \Illuminate\Queue\QueueManager::class,
|
||||
'queue.connection' => \Illuminate\Queue\SyncQueue::class,
|
||||
@@ -479,8 +479,8 @@ namespace PHPSTORM_META {
|
||||
'flare.logger' => \Monolog\Logger::class,
|
||||
'hash' => \Illuminate\Hashing\HashManager::class,
|
||||
'hash.driver' => \Illuminate\Hashing\BcryptHasher::class,
|
||||
'hashids' => \App\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Hashids\HashidsFactory::class,
|
||||
'hashids' => \App\Services\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Services\Hashids\HashidsFactory::class,
|
||||
'log' => \Illuminate\Log\LogManager::class,
|
||||
'mail.manager' => \Illuminate\Mail\MailManager::class,
|
||||
'mailer' => \Illuminate\Mail\Mailer::class,
|
||||
@@ -488,7 +488,7 @@ namespace PHPSTORM_META {
|
||||
'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class,
|
||||
'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class,
|
||||
'migrator' => \Illuminate\Database\Migrations\Migrator::class,
|
||||
'pdf.driver' => \App\Services\PDFService::class,
|
||||
'pdf.driver' => \App\Services\Pdf\PdfService::class,
|
||||
'pipeline' => \Illuminate\Pipeline\Pipeline::class,
|
||||
'queue' => \Illuminate\Queue\QueueManager::class,
|
||||
'queue.connection' => \Illuminate\Queue\SyncQueue::class,
|
||||
@@ -562,8 +562,8 @@ namespace PHPSTORM_META {
|
||||
'flare.logger' => \Monolog\Logger::class,
|
||||
'hash' => \Illuminate\Hashing\HashManager::class,
|
||||
'hash.driver' => \Illuminate\Hashing\BcryptHasher::class,
|
||||
'hashids' => \App\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Hashids\HashidsFactory::class,
|
||||
'hashids' => \App\Services\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Services\Hashids\HashidsFactory::class,
|
||||
'log' => \Illuminate\Log\LogManager::class,
|
||||
'mail.manager' => \Illuminate\Mail\MailManager::class,
|
||||
'mailer' => \Illuminate\Mail\Mailer::class,
|
||||
@@ -571,7 +571,7 @@ namespace PHPSTORM_META {
|
||||
'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class,
|
||||
'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class,
|
||||
'migrator' => \Illuminate\Database\Migrations\Migrator::class,
|
||||
'pdf.driver' => \App\Services\PDFService::class,
|
||||
'pdf.driver' => \App\Services\Pdf\PdfService::class,
|
||||
'pipeline' => \Illuminate\Pipeline\Pipeline::class,
|
||||
'queue' => \Illuminate\Queue\QueueManager::class,
|
||||
'queue.connection' => \Illuminate\Queue\SyncQueue::class,
|
||||
@@ -645,8 +645,8 @@ namespace PHPSTORM_META {
|
||||
'flare.logger' => \Monolog\Logger::class,
|
||||
'hash' => \Illuminate\Hashing\HashManager::class,
|
||||
'hash.driver' => \Illuminate\Hashing\BcryptHasher::class,
|
||||
'hashids' => \App\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Hashids\HashidsFactory::class,
|
||||
'hashids' => \App\Services\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Services\Hashids\HashidsFactory::class,
|
||||
'log' => \Illuminate\Log\LogManager::class,
|
||||
'mail.manager' => \Illuminate\Mail\MailManager::class,
|
||||
'mailer' => \Illuminate\Mail\Mailer::class,
|
||||
@@ -654,7 +654,7 @@ namespace PHPSTORM_META {
|
||||
'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class,
|
||||
'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class,
|
||||
'migrator' => \Illuminate\Database\Migrations\Migrator::class,
|
||||
'pdf.driver' => \App\Services\PDFService::class,
|
||||
'pdf.driver' => \App\Services\Pdf\PdfService::class,
|
||||
'pipeline' => \Illuminate\Pipeline\Pipeline::class,
|
||||
'queue' => \Illuminate\Queue\QueueManager::class,
|
||||
'queue.connection' => \Illuminate\Queue\SyncQueue::class,
|
||||
@@ -728,8 +728,8 @@ namespace PHPSTORM_META {
|
||||
'flare.logger' => \Monolog\Logger::class,
|
||||
'hash' => \Illuminate\Hashing\HashManager::class,
|
||||
'hash.driver' => \Illuminate\Hashing\BcryptHasher::class,
|
||||
'hashids' => \App\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Hashids\HashidsFactory::class,
|
||||
'hashids' => \App\Services\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Services\Hashids\HashidsFactory::class,
|
||||
'log' => \Illuminate\Log\LogManager::class,
|
||||
'mail.manager' => \Illuminate\Mail\MailManager::class,
|
||||
'mailer' => \Illuminate\Mail\Mailer::class,
|
||||
@@ -737,7 +737,7 @@ namespace PHPSTORM_META {
|
||||
'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class,
|
||||
'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class,
|
||||
'migrator' => \Illuminate\Database\Migrations\Migrator::class,
|
||||
'pdf.driver' => \App\Services\PDFService::class,
|
||||
'pdf.driver' => \App\Services\Pdf\PdfService::class,
|
||||
'pipeline' => \Illuminate\Pipeline\Pipeline::class,
|
||||
'queue' => \Illuminate\Queue\QueueManager::class,
|
||||
'queue.connection' => \Illuminate\Queue\SyncQueue::class,
|
||||
@@ -811,8 +811,8 @@ namespace PHPSTORM_META {
|
||||
'flare.logger' => \Monolog\Logger::class,
|
||||
'hash' => \Illuminate\Hashing\HashManager::class,
|
||||
'hash.driver' => \Illuminate\Hashing\BcryptHasher::class,
|
||||
'hashids' => \App\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Hashids\HashidsFactory::class,
|
||||
'hashids' => \App\Services\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Services\Hashids\HashidsFactory::class,
|
||||
'log' => \Illuminate\Log\LogManager::class,
|
||||
'mail.manager' => \Illuminate\Mail\MailManager::class,
|
||||
'mailer' => \Illuminate\Mail\Mailer::class,
|
||||
@@ -820,7 +820,7 @@ namespace PHPSTORM_META {
|
||||
'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class,
|
||||
'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class,
|
||||
'migrator' => \Illuminate\Database\Migrations\Migrator::class,
|
||||
'pdf.driver' => \App\Services\PDFService::class,
|
||||
'pdf.driver' => \App\Services\Pdf\PdfService::class,
|
||||
'pipeline' => \Illuminate\Pipeline\Pipeline::class,
|
||||
'queue' => \Illuminate\Queue\QueueManager::class,
|
||||
'queue.connection' => \Illuminate\Queue\SyncQueue::class,
|
||||
@@ -894,8 +894,8 @@ namespace PHPSTORM_META {
|
||||
'flare.logger' => \Monolog\Logger::class,
|
||||
'hash' => \Illuminate\Hashing\HashManager::class,
|
||||
'hash.driver' => \Illuminate\Hashing\BcryptHasher::class,
|
||||
'hashids' => \App\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Hashids\HashidsFactory::class,
|
||||
'hashids' => \App\Services\Hashids\HashidsManager::class,
|
||||
'hashids.factory' => \App\Services\Hashids\HashidsFactory::class,
|
||||
'log' => \Illuminate\Log\LogManager::class,
|
||||
'mail.manager' => \Illuminate\Mail\MailManager::class,
|
||||
'mailer' => \Illuminate\Mail\Mailer::class,
|
||||
@@ -903,7 +903,7 @@ namespace PHPSTORM_META {
|
||||
'migration.creator' => \Illuminate\Database\Migrations\MigrationCreator::class,
|
||||
'migration.repository' => \Illuminate\Database\Migrations\DatabaseMigrationRepository::class,
|
||||
'migrator' => \Illuminate\Database\Migrations\Migrator::class,
|
||||
'pdf.driver' => \App\Services\PDFService::class,
|
||||
'pdf.driver' => \App\Services\Pdf\PdfService::class,
|
||||
'pipeline' => \Illuminate\Pipeline\Pipeline::class,
|
||||
'queue' => \Illuminate\Queue\QueueManager::class,
|
||||
'queue.connection' => \Illuminate\Queue\SyncQueue::class,
|
||||
|
||||
21
AGENTS.md
21
AGENTS.md
@@ -59,6 +59,27 @@ This project has domain-specific skills available. You MUST activate the relevan
|
||||
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
=== invoiceshelf rules ===
|
||||
|
||||
# InvoiceShelf Architecture
|
||||
|
||||
## Service Pattern (Required)
|
||||
All business logic must live in Service classes (`app/Services/`). This is mandatory — do not put business logic in Models or Controllers.
|
||||
- **Controllers** are thin: authorize, call the service, return a response.
|
||||
- **Models** only contain relationships, scopes, accessors, mutators, and constants.
|
||||
- **Services** are injected into controllers via constructor injection.
|
||||
- Check existing services in `app/Services/` for patterns before creating new ones.
|
||||
|
||||
## Testing (TDD)
|
||||
InvoiceShelf follows TDD development style. Every new feature or bug fix must have tests.
|
||||
- **Feature tests** (`tests/Feature/`) — test API routes end-to-end (HTTP requests, responses, database assertions). These are the primary test type.
|
||||
- **Unit tests** (`tests/Unit/`) — test service classes and business logic in isolation.
|
||||
- Write tests before or alongside implementation, not after.
|
||||
|
||||
## Roles
|
||||
- **`super admin`** — global platform admin role (unscoped, manages all companies)
|
||||
- **`owner`** — company-level admin role (scoped to a specific company via Bouncer, full access to that company)
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
# Laravel Boost
|
||||
|
||||
71
CLAUDE.md
71
CLAUDE.md
@@ -47,33 +47,82 @@ Three guards: `web` (session), `api` (Sanctum tokens for `/api/v1/`), `customer`
|
||||
- **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
|
||||
- Vue 3 + TypeScript + Pinia + vue-router + Tailwind v4 (`@tailwindcss/vite`)
|
||||
- Entry point: `resources/scripts/main.ts` (single Vite input)
|
||||
- Feature-folder layout under `resources/scripts/features/{admin,auth,company,customer-portal,...}` — each feature owns its own `routes.ts`, `views/`, `components/`
|
||||
- Shared layers: `resources/scripts/{api,stores,components,composables,layouts,plugins,utils,types,config}`
|
||||
- Path aliases: `@` → `resources/` (so most imports look like `@/scripts/api/client`, `@/scripts/stores/global.store`); `$fonts` → `resources/static/fonts`; `$images` → `resources/static/img`. There is no `@v2` alias — that was retired when the legacy v1 SPA was deleted.
|
||||
- i18n: `lang/*.json` are dynamically imported by `resources/scripts/plugins/i18n.ts`. Locale-code → filename mismatches (e.g. `pt_BR` → `pt-br.json`) live in `LOCALE_FILE_MAP`. English is statically bundled; other locales lazy-load. Only edit `lang/en.json` directly — other locales are Crowdin-sourced.
|
||||
- Vite dev server expects the `invoiceshelf.test` hostname (configured in `vite.config.js`)
|
||||
|
||||
### CSS Theme Tokens
|
||||
The styling system uses **Tailwind v4 with CSS custom properties as the source of truth** — colors are not configured in JS, they live in CSS and are exposed to Tailwind via the `@theme` directive. Two files own this:
|
||||
|
||||
1. **`resources/css/themes.css`** — defines every color as a CSS custom property on `:root` (light) and `[data-theme="dark"]` (dark). This is where you change actual values.
|
||||
2. **`resources/css/invoiceshelf.css`** — has an `@theme inline { ... }` block that **registers** each custom property as a Tailwind theme token (e.g. `--color-heading: var(--color-heading);`), making it available as utility classes (`bg-heading`, `text-heading`, `border-heading`, etc.). The block also uses the legacy `@theme { --spacing-88: 22rem; --font-base: Poppins, sans-serif; }` for non-color tokens.
|
||||
|
||||
**Token categories defined today:**
|
||||
- `primary-{50…950}` — brand color scale
|
||||
- `surface`, `surface-secondary`, `surface-tertiary`, `surface-muted` — background depth tiers
|
||||
- `heading`, `body`, `muted`, `subtle` — text emphasis tiers
|
||||
- `line-{light,default,strong}` — borders
|
||||
- `hover`, `hover-strong` — hover backgrounds
|
||||
- `header-from`, `header-to` — fixed header gradient stops (not dark-mode-aware)
|
||||
- `btn-primary`, `btn-primary-hover` — button colors (fixed, always bold)
|
||||
- `status-{yellow,green,blue,red,purple}` — status badge text colors
|
||||
- `alert-{warning,error,success}-{bg,text}` — alert variants
|
||||
|
||||
**Dark mode** is toggled via the `[data-theme="dark"]` attribute on the `<html>` element. The same custom-property names get redefined under that selector — components do **not** need `dark:` variants or conditional logic, they just reference the semantic tokens and the right value is picked up automatically.
|
||||
|
||||
**Adding a new color token is a two-step ritual:**
|
||||
1. Add the custom property to **both** `:root` and `[data-theme="dark"]` in `themes.css`
|
||||
2. Add a matching `--color-X: var(--color-X);` line inside the `@theme inline` block in `invoiceshelf.css`
|
||||
|
||||
After that the token is usable as `bg-X` / `text-X` / `border-X` in Vue templates and as `var(--color-X)` in raw CSS. Skip step 2 and the value exists at the CSS level but Tailwind utility classes won't be generated.
|
||||
|
||||
**Convention — never hardcode hex/rgb values in components.** Use the semantic tokens: `text-heading` not `text-gray-900`, `bg-surface` not `bg-white`, `border-line-default` not `border-gray-300`. Hardcoded values won't follow dark-mode flips and will diverge from the rest of the app over time. There are **no exceptions** in the project — even the auth pages (which sit outside the admin chrome) use the same `bg-surface` / `text-heading` / `border-line-default` vocabulary as `BaseCard`, just composed differently.
|
||||
|
||||
### 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
|
||||
- **PDF generation**: Pluggable driver — `dompdf` (default, via `GeneratesPdfTrait`) or `gotenberg` (headless Chromium). Driver chosen per company through the **PDF Generation** admin settings page.
|
||||
- **Email**: Mailable classes with `EmailLog` tracking. Mail driver is configurable globally and may be overridden per-company.
|
||||
- **File storage**: Spatie MediaLibrary backed by the **FileDisk** model — admins create named disk entries (local / S3 / Dropbox / DigitalOcean Spaces) and assign them to purposes (`media_storage`, `pdf_storage`, `backup_storage`) in **Admin → File Disks → Disk Assignments**. New uploads go to the assigned disk; existing files stay where they were and require `php artisan media:secure` to migrate.
|
||||
- **Serial numbers**: `SerialNumberService`
|
||||
- **Company settings**: `CompanySetting` model (key-value per company)
|
||||
- **User settings**: User-level preferences (notably `language`) stored as JSON via `setSettings()`. The sentinel value `'default'` means "inherit the company-level setting" — used for the per-user language preference so promoting/inviting members doesn't freeze a copy of the inviter's language.
|
||||
|
||||
### PDF Font System
|
||||
PDFs ship with bundled **Noto Sans** (Latin / Greek / Cyrillic) as the default face. Non-Latin scripts come from on-demand **Font Packages** managed in **Admin → Font Packages** and defined in `FontService::FONT_PACKAGES` (`app/Services/FontService.php`). Currently shipped packages: `noto-sans` (bundled, marker only), `noto-sans-{sc,tc,jp,kr}` (CJK), `noto-sans-hebrew`, `noto-naskh-arabic` (covers `ar`/`fa`/`ur`), `noto-sans-devanagari` (`hi`), `sarabun` (Thai). `GeneratesPdfTrait::ensureFontsForLocale()` synchronously installs the matching package on the first PDF render for a given company language.
|
||||
|
||||
Two non-obvious constraints when extending the font system:
|
||||
1. **dompdf's PHP-Font-Lib does not parse variable fonts** (`fvar`/`gvar` tables). Any new package must source **static TTF** files — Google Fonts' main repo ships variable fonts and produces empty boxes. Reliable static-TTF sources used today: `openmaptiles/fonts` for non-CJK Noto scripts, `life888888/cjk-fonts-ttf` for the CJK packages, `google/fonts/ofl/sarabun` for Thai.
|
||||
2. **dompdf does not glyph-fall-back through the `font-family` chain** — it uses the *first* font for ALL characters. So locale-specific packages must be the **primary** font for that locale, not a fallback. Selection happens in `FontService::getFontFamilyForLocale()`. This is also why a Latin-locale company with a Hebrew customer name will still render boxes for the Hebrew text — solving that needs Gotenberg or a custom mid-render font-switching pass.
|
||||
|
||||
The bundled NotoSans is also surfaced as a `bundled: true` package entry (no download URL, files served from `resources/static/fonts/` instead of `storage/fonts/`) so it appears alongside the on-demand packages in the admin UI with a "Bundled" pill instead of an Install button.
|
||||
|
||||
### 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.
|
||||
|
||||
### Service Pattern
|
||||
All business logic must live in Service classes (`app/Services/`), not in Models or Controllers. Controllers are thin — they authorize, call the service, and return a response. Models only contain relationships, scopes, accessors, mutators, and constants. Services are injected via constructor injection.
|
||||
|
||||
### Testing (TDD)
|
||||
InvoiceShelf follows TDD development style:
|
||||
- **Feature tests** (`tests/Feature/`) — test API routes end-to-end (HTTP requests, responses, database assertions)
|
||||
- **Unit tests** (`tests/Unit/`) — test service classes and business logic in isolation
|
||||
- Write tests before or alongside implementation. Every new feature or bug fix must have tests.
|
||||
|
||||
## Code Conventions
|
||||
|
||||
- PHP: snake_case, constructor property promotion, explicit return types, PHPDoc blocks over inline comments
|
||||
- JS: camelCase
|
||||
- TS / Vue: camelCase, `<script setup lang="ts">`, prefer Composition API + Pinia stores over component-local state for anything cross-cutting
|
||||
- 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)
|
||||
- Every change must have tests
|
||||
- Run `vendor/bin/pint --dirty --format agent` after modifying PHP files
|
||||
- After editing `lang/en.json` or any file under `resources/scripts/`, rebuild via `npm run build` — the bundled chunks (including locale chunks) are content-hashed by Vite, so the browser will pick them up on hard refresh
|
||||
|
||||
## CI Pipeline
|
||||
|
||||
|
||||
68
CONTRIBUTING.md
Normal file
68
CONTRIBUTING.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Contributing to InvoiceShelf
|
||||
|
||||
Thank you for your interest in contributing to InvoiceShelf! We welcome contributions from the community and appreciate your effort to improve this project.
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. **Fork the repository** and create a new branch from `master`
|
||||
2. **Make your changes** — follow the existing code style and conventions
|
||||
3. **Write tests** if applicable
|
||||
4. **Run the test suite** to ensure nothing is broken
|
||||
5. **Submit a Pull Request** with a clear description of the changes
|
||||
|
||||
## Development Setup
|
||||
|
||||
Please refer to the [README](README.md) for instructions on setting up the development environment.
|
||||
|
||||
## Code Style
|
||||
|
||||
- **PHP**: Follow PSR-12 standards. Run `vendor/bin/pint --dirty` before committing
|
||||
- **TypeScript/Vue**: Follow the existing patterns in `resources/scripts/`
|
||||
- **Tests**: Every new feature or bug fix should include tests
|
||||
|
||||
## Contributor License Agreement (CLA)
|
||||
|
||||
By submitting a pull request, patch, commit, or any other contribution (collectively, "Contribution") to this repository, you agree to the following terms:
|
||||
|
||||
### Grant of Rights
|
||||
|
||||
You hereby grant to **Ideologix Media Dooel** (the maintainer and owner of InvoiceShelf), and to recipients of software distributed by Ideologix Media Dooel, a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable license to:
|
||||
|
||||
- Reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute your Contribution and any derivative works thereof
|
||||
- Use your Contribution for any purpose, including but not limited to incorporating it into InvoiceShelf or any other product or service
|
||||
- Relicense your Contribution under any license, including proprietary licenses
|
||||
|
||||
### Copyright Assignment
|
||||
|
||||
You agree that your Contribution, once merged, becomes part of the InvoiceShelf project and that **Ideologix Media Dooel** retains full rights to the combined work, including the right to:
|
||||
|
||||
- Offer the software under dual licensing (open-source and commercial)
|
||||
- Enforce the license terms against third parties
|
||||
- Make licensing decisions without requiring additional permission from contributors
|
||||
|
||||
### Representations
|
||||
|
||||
By submitting a Contribution, you represent that:
|
||||
|
||||
1. You have the legal right to grant the above license
|
||||
2. Your Contribution is your original work, or you have sufficient rights to submit it
|
||||
3. Your Contribution does not violate any third-party intellectual property rights
|
||||
4. You understand that your Contribution is provided voluntarily and is not confidential
|
||||
|
||||
### Scope
|
||||
|
||||
This agreement applies to all Contributions made to any repository owned by InvoiceShelf / Ideologix Media Dooel, including but not limited to code, documentation, translations, and design assets.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
- Use [GitHub Issues](https://github.com/InvoiceShelf/InvoiceShelf/issues) for bug reports and feature requests
|
||||
- Include steps to reproduce for bug reports
|
||||
- Check existing issues before creating a new one
|
||||
|
||||
## Questions?
|
||||
|
||||
If you have questions about contributing, feel free to open a discussion on GitHub.
|
||||
|
||||
---
|
||||
|
||||
*InvoiceShelf is maintained by [Ideologix Media Dooel](https://ideologix.com/) and licensed under the [AGPL-3.0 License](LICENSE).*
|
||||
2
Makefile
2
Makefile
@@ -29,6 +29,7 @@ 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
|
||||
@@ -47,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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Space\PdfTemplateUtils;
|
||||
use App\Services\Pdf\PdfTemplateUtils;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Space\ModuleInstaller;
|
||||
use App\Services\Module\ModuleInstaller;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class InstallModuleCommand extends Command
|
||||
|
||||
132
app/Console/Commands/MigrateMediaToPrivateDisk.php
Normal file
132
app/Console/Commands/MigrateMediaToPrivateDisk.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\FileDisk;
|
||||
use App\Models\Setting;
|
||||
use App\Services\FileDiskService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class MigrateMediaToPrivateDisk extends Command
|
||||
{
|
||||
protected $signature = 'media:secure {--dry-run : Show what would be moved without moving}';
|
||||
|
||||
protected $description = 'Move sensitive media files (receipts) from the public disk to the private disk';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$targetDisk = $this->resolveTargetDisk();
|
||||
|
||||
if (! $targetDisk) {
|
||||
$this->error('No target disk found. Set a default FileDisk or configure media_disk_id setting.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$targetDiskName = app(FileDiskService::class)->registerDisk($targetDisk);
|
||||
$targetRoot = config('filesystems.disks.'.$targetDiskName.'.root');
|
||||
|
||||
if (! $targetRoot) {
|
||||
$this->error('Could not resolve target disk root path.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$records = DB::table('media')
|
||||
->where('disk', 'public')
|
||||
->where(function ($query) {
|
||||
$query->where('collection_name', 'receipts');
|
||||
})
|
||||
->get();
|
||||
|
||||
if ($records->isEmpty()) {
|
||||
$this->info('No media files to migrate.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info('Found '.$records->count().' file(s) to migrate.');
|
||||
|
||||
if ($this->option('dry-run')) {
|
||||
foreach ($records as $record) {
|
||||
$this->line(" Would move: media/{$record->id}/{$record->file_name}");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$moved = 0;
|
||||
$skipped = 0;
|
||||
$publicMediaRoot = public_path('media');
|
||||
|
||||
$bar = $this->output->createProgressBar($records->count());
|
||||
$bar->start();
|
||||
|
||||
foreach ($records as $record) {
|
||||
$relativePath = $record->id.DIRECTORY_SEPARATOR.$record->file_name;
|
||||
$sourcePath = $publicMediaRoot.DIRECTORY_SEPARATOR.$relativePath;
|
||||
$destPath = $targetRoot.DIRECTORY_SEPARATOR.$relativePath;
|
||||
|
||||
if (! file_exists($sourcePath)) {
|
||||
$skipped++;
|
||||
$bar->advance();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$destDir = dirname($destPath);
|
||||
if (! File::isDirectory($destDir)) {
|
||||
File::makeDirectory($destDir, 0755, true);
|
||||
}
|
||||
|
||||
File::move($sourcePath, $destPath);
|
||||
|
||||
DB::table('media')
|
||||
->where('id', $record->id)
|
||||
->update(['disk' => $targetDiskName]);
|
||||
|
||||
$moved++;
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
$this->info("Done. Moved: {$moved}, Skipped (missing): {$skipped}");
|
||||
|
||||
// Clean up empty directories in public/media
|
||||
$this->cleanEmptyDirectories($publicMediaRoot);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function resolveTargetDisk(): ?FileDisk
|
||||
{
|
||||
$mediaDiskId = Setting::getSetting('media_disk_id');
|
||||
|
||||
if ($mediaDiskId) {
|
||||
return FileDisk::find($mediaDiskId);
|
||||
}
|
||||
|
||||
return FileDisk::where('set_as_default', true)->first();
|
||||
}
|
||||
|
||||
private function cleanEmptyDirectories(string $path): void
|
||||
{
|
||||
if (! File::isDirectory($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$directories = File::directories($path);
|
||||
|
||||
foreach ($directories as $dir) {
|
||||
$this->cleanEmptyDirectories($dir);
|
||||
|
||||
if (count(File::allFiles($dir)) === 0 && count(File::directories($dir)) === 0) {
|
||||
File::deleteDirectory($dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Space\Updater;
|
||||
use App\Services\Update\Updater;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Facade;
|
||||
/**
|
||||
* @method static \Psr\Http\Message\ResponseInterface loadView(string $template)
|
||||
*/
|
||||
class PDF extends Facade
|
||||
class Pdf extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
48
app/Http/Controllers/Admin/AdminDashboardController.php
Normal file
48
app/Http/Controllers/Admin/AdminDashboardController.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Company;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class AdminDashboardController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$version = preg_replace('~[\r\n]+~', '', File::get(base_path('version.md')));
|
||||
|
||||
$dbDriver = config('database.default');
|
||||
$dbVersion = $this->getDatabaseVersion($dbDriver);
|
||||
|
||||
return response()->json([
|
||||
'app_version' => $version,
|
||||
'php_version' => phpversion(),
|
||||
'database' => [
|
||||
'driver' => $dbDriver,
|
||||
'version' => $dbVersion,
|
||||
],
|
||||
'counts' => [
|
||||
'companies' => Company::count(),
|
||||
'users' => User::count(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function getDatabaseVersion(string $driver): ?string
|
||||
{
|
||||
try {
|
||||
return match ($driver) {
|
||||
'mysql' => DB::selectOne('SELECT VERSION() as version')?->version,
|
||||
'pgsql' => DB::selectOne('SHOW server_version')?->server_version,
|
||||
'sqlite' => DB::selectOne('SELECT sqlite_version() as version')?->version,
|
||||
default => null,
|
||||
};
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
117
app/Http/Controllers/Admin/BackupsController.php
Normal file
117
app/Http/Controllers/Admin/BackupsController.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\CreateBackupJob;
|
||||
use App\Rules\Backup\PathToZip;
|
||||
use App\Services\Backup\BackupService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Spatie\Backup\BackupDestination\Backup;
|
||||
use Spatie\Backup\Helpers\Format;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class BackupsController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BackupService $backupService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage backups');
|
||||
|
||||
try {
|
||||
$destination = $this->backupService->getDestination($request->file_disk_id);
|
||||
|
||||
$backups = $destination
|
||||
->backups()
|
||||
->map(function (Backup $backup) {
|
||||
return [
|
||||
'path' => $backup->path(),
|
||||
'created_at' => $backup->date()->format('Y-m-d H:i:s'),
|
||||
'size' => Format::humanReadableSize($backup->sizeInBytes()),
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
|
||||
return response()->json([
|
||||
'backups' => $backups,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'backups' => [],
|
||||
'error' => 'invalid_disk_credentials',
|
||||
'error_message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage backups');
|
||||
|
||||
$data = $request->all();
|
||||
|
||||
dispatch(new CreateBackupJob($data))->onQueue(config('backup.queue.name'));
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
public function destroy($disk, Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage backups');
|
||||
|
||||
$validated = $request->validate([
|
||||
'path' => ['required', new PathToZip],
|
||||
]);
|
||||
|
||||
$destination = $this->backupService->getDestination($request->file_disk_id);
|
||||
|
||||
$destination
|
||||
->backups()
|
||||
->first(function (Backup $backup) use ($validated) {
|
||||
return $backup->path() === $validated['path'];
|
||||
})
|
||||
->delete();
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
public function download(Request $request): Response|StreamedResponse
|
||||
{
|
||||
$this->authorize('manage backups');
|
||||
|
||||
$validated = $request->validate([
|
||||
'path' => ['required', new PathToZip],
|
||||
]);
|
||||
|
||||
$destination = $this->backupService->getDestination($request->file_disk_id);
|
||||
|
||||
$backup = $destination->backups()->first(function (Backup $backup) use ($validated) {
|
||||
return $backup->path() === $validated['path'];
|
||||
});
|
||||
|
||||
if (! $backup) {
|
||||
return response('Backup not found', 422);
|
||||
}
|
||||
|
||||
$fileName = pathinfo($backup->path(), PATHINFO_BASENAME);
|
||||
|
||||
return response()->stream(function () use ($backup) {
|
||||
$stream = $backup->stream();
|
||||
fpassthru($stream);
|
||||
if (is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
}, 200, [
|
||||
'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0',
|
||||
'Content-Type' => 'application/zip',
|
||||
'Content-Length' => $backup->sizeInBytes(),
|
||||
'Content-Disposition' => 'attachment; filename="'.$fileName.'"',
|
||||
'Pragma' => 'public',
|
||||
]);
|
||||
}
|
||||
}
|
||||
113
app/Http/Controllers/Admin/CompaniesController.php
Normal file
113
app/Http/Controllers/Admin/CompaniesController.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Facades\Hashids;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\AdminCompanyUpdateRequest;
|
||||
use App\Http\Requests\CompaniesRequest;
|
||||
use App\Http\Resources\CompanyResource;
|
||||
use App\Models\Company;
|
||||
use App\Services\CompanyService;
|
||||
use Illuminate\Http\Request;
|
||||
use Silber\Bouncer\BouncerFacade;
|
||||
|
||||
class CompaniesController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CompanyService $companyService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$companies = Company::query()
|
||||
->with(['owner', 'address'])
|
||||
->when($request->has('search'), function ($query) use ($request) {
|
||||
$query->where('name', 'like', '%'.$request->search.'%');
|
||||
})
|
||||
->when($request->has('orderByField') && $request->has('orderBy'), function ($query) use ($request) {
|
||||
$query->orderBy($request->orderByField, $request->orderBy);
|
||||
}, function ($query) {
|
||||
$query->orderBy('name', 'asc');
|
||||
})
|
||||
->paginate($request->input('limit', 10));
|
||||
|
||||
return CompanyResource::collection($companies);
|
||||
}
|
||||
|
||||
public function show(Company $company)
|
||||
{
|
||||
$company->load(['owner', 'address']);
|
||||
|
||||
return new CompanyResource($company);
|
||||
}
|
||||
|
||||
public function update(AdminCompanyUpdateRequest $request, Company $company)
|
||||
{
|
||||
$company->update([
|
||||
'name' => $request->name,
|
||||
'vat_id' => $request->vat_id,
|
||||
'tax_id' => $request->tax_id,
|
||||
'owner_id' => $request->owner_id,
|
||||
]);
|
||||
|
||||
if ($request->has('address')) {
|
||||
$company->address()->updateOrCreate(
|
||||
['company_id' => $company->id],
|
||||
$request->address,
|
||||
);
|
||||
}
|
||||
|
||||
$company->load(['owner', 'address']);
|
||||
|
||||
return new CompanyResource($company);
|
||||
}
|
||||
|
||||
public function store(CompaniesRequest $request)
|
||||
{
|
||||
$this->authorize('create company');
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$company = Company::create($request->getCompanyPayload());
|
||||
$company->unique_hash = Hashids::connection(Company::class)->encode($company->id);
|
||||
$company->save();
|
||||
$this->companyService->setupDefaults($company);
|
||||
$user->companies()->attach($company->id);
|
||||
|
||||
BouncerFacade::scope()->to($company->id);
|
||||
$user->assign('owner');
|
||||
|
||||
if ($request->address) {
|
||||
$company->address()->create($request->address);
|
||||
}
|
||||
|
||||
return new CompanyResource($company);
|
||||
}
|
||||
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$company = Company::find($request->header('company'));
|
||||
|
||||
$this->authorize('delete company', $company);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if ($request->name !== $company->name) {
|
||||
return respondJson('company_name_must_match_with_given_name', 'Company name must match with given name');
|
||||
}
|
||||
|
||||
$this->companyService->delete($company, $user);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function userCompanies(Request $request)
|
||||
{
|
||||
$companies = $request->user()->companies;
|
||||
|
||||
return CompanyResource::collection($companies);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\General;
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\CountryResource;
|
||||
23
app/Http/Controllers/Admin/CurrenciesController.php
Normal file
23
app/Http/Controllers/Admin/CurrenciesController.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\CurrencyResource;
|
||||
use App\Services\CurrencyService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
|
||||
class CurrenciesController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CurrencyService $currencyService,
|
||||
) {}
|
||||
|
||||
public function __invoke(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$currencies = $this->currencyService->getAllWithCommonFirst();
|
||||
|
||||
return CurrencyResource::collection($currencies);
|
||||
}
|
||||
}
|
||||
52
app/Http/Controllers/Admin/FontController.php
Normal file
52
app/Http/Controllers/Admin/FontController.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\FontService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class FontController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly FontService $fontService,
|
||||
) {}
|
||||
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
$this->authorize('manage settings');
|
||||
|
||||
return response()->json([
|
||||
'packages' => $this->fontService->getPackageStatuses(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function install(string $package): JsonResponse
|
||||
{
|
||||
$this->authorize('manage settings');
|
||||
|
||||
if (! isset(FontService::FONT_PACKAGES[$package])) {
|
||||
return response()->json(['error' => 'Unknown font package'], 404);
|
||||
}
|
||||
|
||||
$pkg = FontService::FONT_PACKAGES[$package];
|
||||
|
||||
if ($this->fontService->isInstalled($pkg)) {
|
||||
return response()->json(['success' => true, 'message' => 'Already installed']);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->fontService->downloadPackage($pkg);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'installed' => true,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Modules;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\UnzipUpdateRequest;
|
||||
use App\Http\Requests\UploadModuleRequest;
|
||||
use App\Services\Module\ModuleInstaller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ModuleInstallationController extends Controller
|
||||
{
|
||||
public function download(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage modules');
|
||||
|
||||
$response = ModuleInstaller::download(
|
||||
(string) $request->slug,
|
||||
(string) $request->version,
|
||||
$request->checksum_sha256 ? (string) $request->checksum_sha256 : null,
|
||||
);
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
|
||||
public function upload(UploadModuleRequest $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage modules');
|
||||
|
||||
$response = ModuleInstaller::upload($request);
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
|
||||
public function unzip(UnzipUpdateRequest $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage modules');
|
||||
|
||||
$path = ModuleInstaller::unzip($request->module_name ?? $request->module, $request->path);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'path' => $path,
|
||||
]);
|
||||
}
|
||||
|
||||
public function copy(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage modules');
|
||||
|
||||
$response = ModuleInstaller::copyFiles($request->module_name ?? $request->module, $request->path);
|
||||
|
||||
return response()->json([
|
||||
'success' => $response,
|
||||
]);
|
||||
}
|
||||
|
||||
public function complete(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage modules');
|
||||
|
||||
$response = ModuleInstaller::complete($request->module_name ?? $request->module, $request->version);
|
||||
|
||||
return response()->json([
|
||||
'success' => $response,
|
||||
]);
|
||||
}
|
||||
}
|
||||
86
app/Http/Controllers/Admin/Modules/ModulesController.php
Normal file
86
app/Http/Controllers/Admin/Modules/ModulesController.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Modules;
|
||||
|
||||
use App\Events\ModuleDisabledEvent;
|
||||
use App\Events\ModuleEnabledEvent;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ModuleResource;
|
||||
use App\Models\Module as ModelsModule;
|
||||
use App\Services\Module\ModuleInstaller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
|
||||
class ModulesController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('manage modules');
|
||||
|
||||
$response = ModuleInstaller::getModules();
|
||||
|
||||
if (($response['status'] ?? 0) !== 200 || ! isset($response['body']->modules)) {
|
||||
return response()->json(['error' => 'marketplace_unavailable'], 503);
|
||||
}
|
||||
|
||||
return ModuleResource::collection(collect($response['body']->modules));
|
||||
}
|
||||
|
||||
public function show(Request $request, string $module)
|
||||
{
|
||||
$this->authorize('manage modules');
|
||||
|
||||
$response = ModuleInstaller::getModule($module);
|
||||
|
||||
if (($response['status'] ?? 0) === 404) {
|
||||
return response()->json(['error' => 'not_found'], 404);
|
||||
}
|
||||
|
||||
if (($response['status'] ?? 0) !== 200 || ! isset($response['body']->data)) {
|
||||
return response()->json(['error' => 'marketplace_unavailable'], 503);
|
||||
}
|
||||
|
||||
return (new ModuleResource($response['body']->data))
|
||||
->additional(['meta' => [
|
||||
'modules' => ModuleResource::collection(
|
||||
collect($response['body']->meta->modules ?? [])
|
||||
),
|
||||
]]);
|
||||
}
|
||||
|
||||
public function checkToken(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage modules');
|
||||
|
||||
return ModuleInstaller::checkToken($request->api_token);
|
||||
}
|
||||
|
||||
public function enable(Request $request, string $module): JsonResponse
|
||||
{
|
||||
$this->authorize('manage modules');
|
||||
|
||||
$module = ModelsModule::where('name', $module)->first();
|
||||
$module->update(['enabled' => true]);
|
||||
$installedModule = Module::find($module->name);
|
||||
$installedModule->enable();
|
||||
|
||||
ModuleEnabledEvent::dispatch($module);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
public function disable(Request $request, string $module): JsonResponse
|
||||
{
|
||||
$this->authorize('manage modules');
|
||||
|
||||
$module = ModelsModule::where('name', $module)->first();
|
||||
$module->update(['enabled' => false]);
|
||||
$installedModule = Module::find($module->name);
|
||||
$installedModule->disable();
|
||||
|
||||
ModuleDisabledEvent::dispatch($module);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
}
|
||||
268
app/Http/Controllers/Admin/Settings/DiskController.php
Normal file
268
app/Http/Controllers/Admin/Settings/DiskController.php
Normal file
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\DiskEnvironmentRequest;
|
||||
use App\Http\Resources\FileDiskResource;
|
||||
use App\Models\FileDisk;
|
||||
use App\Models\Setting;
|
||||
use App\Services\FileDiskService;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DiskController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly FileDiskService $fileDiskService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws AuthorizationException
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$this->authorize('manage file disk');
|
||||
|
||||
$limit = $request->has('limit') ? $request->limit : 5;
|
||||
$disks = FileDisk::applyFilters($request->all())
|
||||
->latest()
|
||||
->paginateData($limit);
|
||||
|
||||
return FileDiskResource::collection($disks);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return JsonResponse
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function store(DiskEnvironmentRequest $request): JsonResponse|FileDiskResource
|
||||
{
|
||||
$this->authorize('manage file disk');
|
||||
|
||||
if (! $this->fileDiskService->validateCredentials($request->credentials, $request->driver)) {
|
||||
return respondJson('invalid_credentials', 'Invalid Credentials.');
|
||||
}
|
||||
|
||||
$disk = $this->fileDiskService->create($request);
|
||||
|
||||
return new FileDiskResource($disk);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function update(FileDisk $disk, Request $request): JsonResponse|FileDiskResource
|
||||
{
|
||||
$this->authorize('manage file disk');
|
||||
|
||||
$credentials = $request->credentials;
|
||||
$driver = $request->driver;
|
||||
|
||||
if ($credentials && $driver && $disk->type !== 'SYSTEM') {
|
||||
if (! $this->fileDiskService->validateCredentials($credentials, $driver)) {
|
||||
return respondJson('invalid_credentials', 'Invalid Credentials.');
|
||||
}
|
||||
|
||||
$this->fileDiskService->update($disk, $request);
|
||||
} elseif ($request->set_as_default) {
|
||||
$this->fileDiskService->setAsDefault($disk);
|
||||
}
|
||||
|
||||
return new FileDiskResource($disk);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function show($disk): JsonResponse
|
||||
{
|
||||
|
||||
$this->authorize('manage file disk');
|
||||
|
||||
$diskData = [];
|
||||
switch ($disk) {
|
||||
case 'local':
|
||||
// Path is relative to storage/app/.
|
||||
// e.g., "backups" resolves to storage/app/backups/ at runtime.
|
||||
$diskData = [
|
||||
'root' => '',
|
||||
];
|
||||
|
||||
break;
|
||||
|
||||
case 's3':
|
||||
$diskData = [
|
||||
'key' => '',
|
||||
'secret' => '',
|
||||
'region' => '',
|
||||
'bucket' => '',
|
||||
'root' => '',
|
||||
];
|
||||
|
||||
break;
|
||||
|
||||
case 's3compat':
|
||||
$diskData = [
|
||||
'endpoint' => '',
|
||||
'key' => '',
|
||||
'secret' => '',
|
||||
'region' => '',
|
||||
'bucket' => '',
|
||||
'root' => '',
|
||||
];
|
||||
|
||||
case 'doSpaces':
|
||||
$diskData = [
|
||||
'key' => '',
|
||||
'secret' => '',
|
||||
'region' => '',
|
||||
'bucket' => '',
|
||||
'endpoint' => '',
|
||||
'root' => '',
|
||||
];
|
||||
|
||||
break;
|
||||
|
||||
case 'dropbox':
|
||||
$diskData = [
|
||||
'token' => '',
|
||||
'key' => '',
|
||||
'secret' => '',
|
||||
'app' => '',
|
||||
'root' => '',
|
||||
];
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$data = array_merge($diskData);
|
||||
|
||||
return response()->json($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param FileDisk $taxType
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function destroy(FileDisk $disk): JsonResponse
|
||||
{
|
||||
$this->authorize('manage file disk');
|
||||
|
||||
if ($disk->type === 'SYSTEM') {
|
||||
return respondJson('not_allowed', 'System disks cannot be deleted.');
|
||||
}
|
||||
|
||||
if ($disk->setAsDefault()) {
|
||||
return respondJson('not_allowed', 'The default disk cannot be deleted.');
|
||||
}
|
||||
|
||||
$prefix = env('DYNAMIC_DISK_PREFIX', 'temp_');
|
||||
$diskName = $prefix.$disk->driver;
|
||||
$mediaCount = DB::table('media')
|
||||
->where('disk', $diskName)
|
||||
->orWhere('disk', $disk->driver)
|
||||
->count();
|
||||
|
||||
if ($mediaCount > 0) {
|
||||
return respondJson('disk_has_files', 'Cannot delete this disk — it contains '.$mediaCount.' file(s). Migrate files first.');
|
||||
}
|
||||
|
||||
$disk->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AuthorizationException
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function getDiskDrivers(): JsonResponse
|
||||
{
|
||||
$this->authorize('manage file disk');
|
||||
|
||||
$drivers = [
|
||||
[
|
||||
'name' => 'Local',
|
||||
'value' => 'local',
|
||||
],
|
||||
[
|
||||
'name' => 'Amazon S3',
|
||||
'value' => 's3',
|
||||
],
|
||||
[
|
||||
'name' => 'S3 Compatible Storage',
|
||||
'value' => 's3compat',
|
||||
],
|
||||
[
|
||||
'name' => 'Digital Ocean Spaces',
|
||||
'value' => 'doSpaces',
|
||||
],
|
||||
[
|
||||
'name' => 'Dropbox',
|
||||
'value' => 'dropbox',
|
||||
],
|
||||
];
|
||||
|
||||
$defaultDisk = FileDisk::where('set_as_default', true)->first();
|
||||
|
||||
return response()->json([
|
||||
'drivers' => $drivers,
|
||||
'default' => $defaultDisk?->driver ?? 'local',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getDiskPurposes(): JsonResponse
|
||||
{
|
||||
$this->authorize('manage file disk');
|
||||
|
||||
$defaultDisk = FileDisk::where('set_as_default', true)->first();
|
||||
$defaultId = $defaultDisk?->id;
|
||||
|
||||
return response()->json([
|
||||
'media_disk_id' => Setting::getSetting('media_disk_id') ?? $defaultId,
|
||||
'pdf_disk_id' => Setting::getSetting('pdf_disk_id') ?? $defaultId,
|
||||
'backup_disk_id' => Setting::getSetting('backup_disk_id') ?? $defaultId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateDiskPurposes(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage file disk');
|
||||
|
||||
$request->validate([
|
||||
'media_disk_id' => 'nullable|exists:file_disks,id',
|
||||
'pdf_disk_id' => 'nullable|exists:file_disks,id',
|
||||
'backup_disk_id' => 'nullable|exists:file_disks,id',
|
||||
]);
|
||||
|
||||
if ($request->has('media_disk_id')) {
|
||||
Setting::setSetting('media_disk_id', $request->media_disk_id);
|
||||
}
|
||||
|
||||
if ($request->has('pdf_disk_id')) {
|
||||
Setting::setSetting('pdf_disk_id', $request->pdf_disk_id);
|
||||
}
|
||||
|
||||
if ($request->has('backup_disk_id')) {
|
||||
Setting::setSetting('backup_disk_id', $request->backup_disk_id);
|
||||
}
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
}
|
||||
94
app/Http/Controllers/Admin/Settings/MailConfigurationController.php
Executable file
94
app/Http/Controllers/Admin/Settings/MailConfigurationController.php
Executable file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\MailEnvironmentRequest;
|
||||
use App\Mail\TestMail;
|
||||
use App\Models\Setting;
|
||||
use App\Services\MailConfigurationService;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class MailConfigurationController extends Controller
|
||||
{
|
||||
public function __construct(private readonly MailConfigurationService $mailConfigurationService) {}
|
||||
|
||||
/**
|
||||
* Save the mail environment variables
|
||||
*
|
||||
*
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function saveMailEnvironment(MailEnvironmentRequest $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage email config');
|
||||
|
||||
$setting = Setting::getSetting('profile_complete');
|
||||
|
||||
$this->mailConfigurationService->saveGlobalConfig($request->validated());
|
||||
|
||||
if ($setting !== 'COMPLETED') {
|
||||
Setting::setSetting('profile_complete', 4);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => 'mail_variables_save_successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the mail environment variables
|
||||
*
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function getMailEnvironment(): JsonResponse
|
||||
{
|
||||
$this->authorize('manage email config');
|
||||
|
||||
return response()->json($this->mailConfigurationService->getGlobalConfig());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the available mail drivers
|
||||
*
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function getMailDrivers(): JsonResponse
|
||||
{
|
||||
$this->authorize('manage email config');
|
||||
|
||||
return response()->json($this->mailConfigurationService->getAvailableDrivers());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the email configuration
|
||||
*
|
||||
*
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function testEmailConfig(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage email config');
|
||||
|
||||
$this->validate($request, [
|
||||
'to' => 'required|email',
|
||||
'subject' => 'required',
|
||||
'message' => 'required',
|
||||
]);
|
||||
|
||||
Mail::to($request->to)->send(new TestMail($request->subject, $request->message));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Settings;
|
||||
namespace App\Http\Controllers\Admin\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\PDFConfigurationRequest;
|
||||
use App\Models\Setting;
|
||||
use App\Space\EnvironmentManager;
|
||||
use App\Services\Setup\EnvironmentManager;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class PDFConfigurationController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var EnvironmentManager
|
||||
*/
|
||||
protected $environmentManager;
|
||||
protected EnvironmentManager $environmentManager;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct(EnvironmentManager $environmentManager)
|
||||
{
|
||||
$this->environmentManager = $environmentManager;
|
||||
}
|
||||
|
||||
public function getDrivers()
|
||||
/**
|
||||
* Returns the available drivers
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function getDrivers(): JsonResponse
|
||||
{
|
||||
$this->authorize('manage pdf config');
|
||||
|
||||
@@ -31,7 +38,12 @@ class PDFConfigurationController extends Controller
|
||||
return response()->json($drivers);
|
||||
}
|
||||
|
||||
public function getEnvironment()
|
||||
/**
|
||||
* Return the PDF settings
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function getEnvironment(): JsonResponse
|
||||
{
|
||||
$this->authorize('manage pdf config');
|
||||
|
||||
@@ -53,7 +65,12 @@ class PDFConfigurationController extends Controller
|
||||
return response()->json($config);
|
||||
}
|
||||
|
||||
public function saveEnvironment(PDFConfigurationRequest $request)
|
||||
/**
|
||||
* Saves the settings
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function saveEnvironment(PDFConfigurationRequest $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage pdf config');
|
||||
|
||||
@@ -70,10 +87,8 @@ class PDFConfigurationController extends Controller
|
||||
|
||||
/**
|
||||
* Prepare PDF settings for database storage
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function preparePDFSettingsForDatabase(PDFConfigurationRequest $request)
|
||||
private function preparePDFSettingsForDatabase(PDFConfigurationRequest $request): array
|
||||
{
|
||||
$driver = $request->get('pdf_driver');
|
||||
|
||||
35
app/Http/Controllers/Admin/Settings/SettingsController.php
Normal file
35
app/Http/Controllers/Admin/Settings/SettingsController.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\GetSettingRequest;
|
||||
use App\Http\Requests\SettingRequest;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
public function show(GetSettingRequest $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage settings');
|
||||
|
||||
$setting = Setting::getSetting($request->key);
|
||||
|
||||
return response()->json([
|
||||
$request->key => $setting,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(SettingRequest $request): JsonResponse
|
||||
{
|
||||
$this->authorize('manage settings');
|
||||
|
||||
Setting::setSettings($request->settings);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
$request->settings,
|
||||
]);
|
||||
}
|
||||
}
|
||||
116
app/Http/Controllers/Admin/UpdateController.php
Normal file
116
app/Http/Controllers/Admin/UpdateController.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Update\Updater;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class UpdateController extends Controller
|
||||
{
|
||||
public function checkVersion(Request $request): JsonResponse
|
||||
{
|
||||
$this->ensureSuperAdmin();
|
||||
|
||||
set_time_limit(600);
|
||||
|
||||
$channel = $request->get('channel', 'stable');
|
||||
$version = preg_replace('~[\r\n]+~', '', File::get(base_path('version.md')));
|
||||
|
||||
return response()->json(Updater::checkForUpdate($version, $channel));
|
||||
}
|
||||
|
||||
public function download(Request $request): JsonResponse
|
||||
{
|
||||
$this->ensureSuperAdmin();
|
||||
|
||||
$request->validate(['version' => 'required']);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'path' => Updater::download($request->version),
|
||||
]);
|
||||
}
|
||||
|
||||
public function unzip(Request $request): JsonResponse
|
||||
{
|
||||
$this->ensureSuperAdmin();
|
||||
|
||||
$request->validate(['path' => 'required']);
|
||||
|
||||
try {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'path' => Updater::unzip($request->path),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function copy(Request $request): JsonResponse
|
||||
{
|
||||
$this->ensureSuperAdmin();
|
||||
|
||||
$request->validate(['path' => 'required']);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'path' => Updater::copyFiles($request->path),
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(Request $request): JsonResponse
|
||||
{
|
||||
return $this->clean($request);
|
||||
}
|
||||
|
||||
public function clean(Request $request): JsonResponse
|
||||
{
|
||||
$this->ensureSuperAdmin();
|
||||
|
||||
// Backward compatibility: use deleted_files when no manifest exists
|
||||
if (! File::exists(base_path('manifest.json'))
|
||||
&& isset($request->deleted_files)
|
||||
&& ! empty($request->deleted_files)) {
|
||||
Updater::deleteFiles($request->deleted_files);
|
||||
|
||||
return response()->json(['success' => true, 'cleaned' => 0]);
|
||||
}
|
||||
|
||||
$result = Updater::cleanStaleFiles();
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
public function migrate(Request $request): JsonResponse
|
||||
{
|
||||
$this->ensureSuperAdmin();
|
||||
|
||||
Updater::migrateUpdate();
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
public function finish(Request $request): JsonResponse
|
||||
{
|
||||
$this->ensureSuperAdmin();
|
||||
|
||||
$request->validate([
|
||||
'installed' => 'required',
|
||||
'version' => 'required',
|
||||
]);
|
||||
|
||||
return response()->json(Updater::finishUpdate($request->installed, $request->version));
|
||||
}
|
||||
|
||||
private function ensureSuperAdmin(): void
|
||||
{
|
||||
$this->authorize('manage update app');
|
||||
}
|
||||
}
|
||||
101
app/Http/Controllers/Admin/UsersController.php
Normal file
101
app/Http/Controllers/Admin/UsersController.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\AdminUserUpdateRequest;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\ImpersonationLog;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
|
||||
class UsersController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$limit = $request->has('limit') ? $request->limit : 10;
|
||||
|
||||
$users = User::with('companies')
|
||||
->applyFilters($request->all())
|
||||
->latest()
|
||||
->paginate($limit);
|
||||
|
||||
return UserResource::collection($users);
|
||||
}
|
||||
|
||||
public function show(User $user)
|
||||
{
|
||||
$user->load('companies');
|
||||
|
||||
return new UserResource($user);
|
||||
}
|
||||
|
||||
public function update(AdminUserUpdateRequest $request, User $user)
|
||||
{
|
||||
$data = $request->only(['name', 'email', 'phone']);
|
||||
|
||||
if ($request->filled('password')) {
|
||||
$data['password'] = $request->password;
|
||||
}
|
||||
|
||||
$user->update($data);
|
||||
|
||||
return new UserResource($user);
|
||||
}
|
||||
|
||||
public function impersonate(Request $request, User $user)
|
||||
{
|
||||
$admin = $request->user();
|
||||
|
||||
if ($admin->id === $user->id) {
|
||||
return response()->json([
|
||||
'error' => 'cannot_impersonate_self',
|
||||
'message' => 'You cannot impersonate yourself.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$token = $user->createToken(
|
||||
'impersonation-by-'.$admin->id,
|
||||
['*'],
|
||||
now()->addHours(2),
|
||||
);
|
||||
|
||||
$log = ImpersonationLog::create([
|
||||
'admin_id' => $admin->id,
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => $request->ip(),
|
||||
'token_id' => $token->accessToken->id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'token' => $token->plainTextToken,
|
||||
'impersonation_log_id' => $log->id,
|
||||
'user' => new UserResource($user),
|
||||
]);
|
||||
}
|
||||
|
||||
public function stopImpersonating(Request $request)
|
||||
{
|
||||
$token = $request->user()->currentAccessToken();
|
||||
|
||||
if ($token instanceof PersonalAccessToken && str_starts_with($token->name, 'impersonation-by-')) {
|
||||
$log = ImpersonationLog::where('token_id', $token->id)
|
||||
->whereNull('stopped_at')
|
||||
->first();
|
||||
|
||||
if ($log) {
|
||||
$log->update(['stopped_at' => now()]);
|
||||
}
|
||||
|
||||
$token->delete();
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => 'not_impersonating',
|
||||
'message' => 'No active impersonation session.',
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Mobile;
|
||||
namespace App\Http\Controllers\Company\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\LoginRequest;
|
||||
use App\Models\CompanyInvitation;
|
||||
use App\Models\User;
|
||||
use App\Services\InvitationService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
@@ -14,7 +16,7 @@ class AuthController extends Controller
|
||||
{
|
||||
public function login(LoginRequest $request)
|
||||
{
|
||||
$user = User::where('email', $request->username)->first();
|
||||
$user = User::whereRaw('LOWER(email) = ?', [strtolower($request->username)])->first();
|
||||
|
||||
if (! $user || ! Hash::check($request->password, $user->password)) {
|
||||
throw ValidationException::withMessages([
|
||||
@@ -22,6 +24,17 @@ class AuthController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
// Auto-accept invitation if token is provided
|
||||
if ($request->has('invitation_token') && $request->invitation_token) {
|
||||
$invitation = CompanyInvitation::where('token', $request->invitation_token)
|
||||
->pending()
|
||||
->first();
|
||||
|
||||
if ($invitation) {
|
||||
app(InvitationService::class)->accept($invitation, $user);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'type' => 'Bearer',
|
||||
'token' => $user->createToken($request->device_name)->plainTextToken,
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Auth;
|
||||
namespace App\Http\Controllers\Company\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\AppServiceProvider;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Auth;
|
||||
namespace App\Http\Controllers\Company\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CompanyInvitation;
|
||||
use App\Models\User;
|
||||
use App\Services\InvitationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class InvitationRegistrationController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InvitationService $invitationService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get invitation details by token (public endpoint for registration page).
|
||||
*/
|
||||
public function details(string $token): JsonResponse
|
||||
{
|
||||
$invitation = CompanyInvitation::where('token', $token)
|
||||
->pending()
|
||||
->with(['company', 'role'])
|
||||
->first();
|
||||
|
||||
if (! $invitation) {
|
||||
return response()->json([
|
||||
'error' => 'Invitation not found or expired.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'email' => $invitation->email,
|
||||
'company_name' => $invitation->company->name,
|
||||
'role_name' => $invitation->role->title,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new user and auto-accept the invitation.
|
||||
*/
|
||||
public function register(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
'invitation_token' => 'required|string',
|
||||
]);
|
||||
|
||||
$invitation = CompanyInvitation::where('token', $request->invitation_token)
|
||||
->pending()
|
||||
->first();
|
||||
|
||||
if (! $invitation) {
|
||||
throw ValidationException::withMessages([
|
||||
'invitation_token' => ['Invitation not found or expired.'],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($invitation->email !== $request->email) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => ['Email does not match the invitation.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => $request->password,
|
||||
]);
|
||||
|
||||
$user->setSettings(['language' => 'default']);
|
||||
|
||||
$this->invitationService->accept($invitation, $user);
|
||||
|
||||
return response()->json([
|
||||
'type' => 'Bearer',
|
||||
'token' => $user->createToken('web')->plainTextToken,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Auth;
|
||||
namespace App\Http\Controllers\Company\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\AppServiceProvider;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Auth;
|
||||
namespace App\Http\Controllers\Company\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
@@ -61,10 +61,14 @@ class RegisterController extends Controller
|
||||
*/
|
||||
protected function create(array $data)
|
||||
{
|
||||
return User::create([
|
||||
$user = User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => $data['password'],
|
||||
]);
|
||||
|
||||
$user->setSettings(['language' => 'default']);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Auth;
|
||||
namespace App\Http\Controllers\Company\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\AppServiceProvider;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Auth;
|
||||
namespace App\Http\Controllers\Company\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\AppServiceProvider;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Config;
|
||||
namespace App\Http\Controllers\Company\Config;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Config;
|
||||
namespace App\Http\Controllers\Company\Config;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Config;
|
||||
namespace App\Http\Controllers\Company\Config;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -1,16 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\CustomField;
|
||||
namespace App\Http\Controllers\Company\CustomField;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\CustomFieldRequest;
|
||||
use App\Http\Resources\CustomFieldResource;
|
||||
use App\Models\CustomField;
|
||||
use App\Services\CustomFieldService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class CustomFieldsController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CustomFieldService $customFieldService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
@@ -40,7 +45,7 @@ class CustomFieldsController extends Controller
|
||||
{
|
||||
$this->authorize('create', CustomField::class);
|
||||
|
||||
$customField = CustomField::createCustomField($request);
|
||||
$customField = $this->customFieldService->create($request);
|
||||
|
||||
return new CustomFieldResource($customField);
|
||||
}
|
||||
@@ -69,7 +74,7 @@ class CustomFieldsController extends Controller
|
||||
{
|
||||
$this->authorize('update', $customField);
|
||||
|
||||
$customField->updateCustomField($request);
|
||||
$this->customFieldService->update($customField, $request);
|
||||
|
||||
return new CustomFieldResource($customField);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\Customer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\CustomerResource;
|
||||
use App\Models\Customer;
|
||||
use App\Services\CustomerService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CustomerStatsController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CustomerService $customerService,
|
||||
) {}
|
||||
|
||||
public function __invoke(Request $request, Customer $customer)
|
||||
{
|
||||
$this->authorize('view', $customer);
|
||||
|
||||
$chartData = $this->customerService->getStats(
|
||||
$customer,
|
||||
$request->header('company'),
|
||||
$request->has('previous_year')
|
||||
);
|
||||
|
||||
$customer = Customer::find($customer->id);
|
||||
|
||||
return (new CustomerResource($customer))
|
||||
->additional(['meta' => [
|
||||
'chartData' => $chartData,
|
||||
]]);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Customer;
|
||||
namespace App\Http\Controllers\Company\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 App\Services\CustomerService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CustomersController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CustomerService $customerService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
@@ -46,7 +51,7 @@ class CustomersController extends Controller
|
||||
{
|
||||
$this->authorize('create', Customer::class);
|
||||
|
||||
$customer = Customer::createCustomer($request);
|
||||
$customer = $this->customerService->create($request);
|
||||
|
||||
return new CustomerResource($customer);
|
||||
}
|
||||
@@ -73,11 +78,7 @@ class CustomersController extends Controller
|
||||
{
|
||||
$this->authorize('update', $customer);
|
||||
|
||||
$customer = Customer::updateCustomer($request, $customer);
|
||||
|
||||
if (is_string($customer)) {
|
||||
return respondJson('you_cannot_edit_currency', 'Cannot change currency once transactions created');
|
||||
}
|
||||
$customer = $this->customerService->update($request, $customer);
|
||||
|
||||
return new CustomerResource($customer);
|
||||
}
|
||||
@@ -96,7 +97,7 @@ class CustomersController extends Controller
|
||||
->whereIn('id', $request->ids)
|
||||
->pluck('id');
|
||||
|
||||
Customer::deleteCustomers($ids);
|
||||
$this->customerService->delete($ids);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Dashboard;
|
||||
namespace App\Http\Controllers\Company\Dashboard;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Company;
|
||||
@@ -60,39 +60,27 @@ class DashboardController extends Controller
|
||||
}
|
||||
|
||||
while ($monthCounter < 12) {
|
||||
array_push(
|
||||
$invoice_totals,
|
||||
Invoice::whereBetween(
|
||||
'invoice_date',
|
||||
[$start->format('Y-m-d'), $end->format('Y-m-d')]
|
||||
)
|
||||
->whereCompany()
|
||||
->sum('base_total')
|
||||
);
|
||||
array_push(
|
||||
$expense_totals,
|
||||
Expense::whereBetween(
|
||||
'expense_date',
|
||||
[$start->format('Y-m-d'), $end->format('Y-m-d')]
|
||||
)
|
||||
->whereCompany()
|
||||
->sum('base_amount')
|
||||
);
|
||||
array_push(
|
||||
$receipt_totals,
|
||||
Payment::whereBetween(
|
||||
'payment_date',
|
||||
[$start->format('Y-m-d'), $end->format('Y-m-d')]
|
||||
)
|
||||
->whereCompany()
|
||||
->sum('base_amount')
|
||||
);
|
||||
array_push(
|
||||
$net_income_totals,
|
||||
($receipt_totals[$i] - $expense_totals[$i])
|
||||
);
|
||||
$invoice_totals[] = Invoice::whereBetween(
|
||||
'invoice_date',
|
||||
[$start->format('Y-m-d'), $end->format('Y-m-d')]
|
||||
)
|
||||
->whereCompany()
|
||||
->sum('base_total');
|
||||
$expense_totals[] = Expense::whereBetween(
|
||||
'expense_date',
|
||||
[$start->format('Y-m-d'), $end->format('Y-m-d')]
|
||||
)
|
||||
->whereCompany()
|
||||
->sum('base_amount');
|
||||
$receipt_totals[] = Payment::whereBetween(
|
||||
'payment_date',
|
||||
[$start->format('Y-m-d'), $end->format('Y-m-d')]
|
||||
)
|
||||
->whereCompany()
|
||||
->sum('base_amount');
|
||||
$net_income_totals[] = ($receipt_totals[$i] - $expense_totals[$i]);
|
||||
$i++;
|
||||
array_push($months, $start->translatedFormat('M'));
|
||||
$months[] = $start->translatedFormat('M');
|
||||
$monthCounter++;
|
||||
$end->startOfMonth();
|
||||
$start->addMonth()->startOfMonth();
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Estimate;
|
||||
namespace App\Http\Controllers\Company\Estimate;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Estimate;
|
||||
use App\Space\PdfTemplateUtils;
|
||||
use App\Services\Pdf\PdfTemplateUtils;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
141
app/Http/Controllers/Company/Estimate/EstimatesController.php
Normal file
141
app/Http/Controllers/Company/Estimate/EstimatesController.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\Estimate;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\DeleteEstimatesRequest;
|
||||
use App\Http\Requests\EstimatesRequest;
|
||||
use App\Http\Requests\SendEstimatesRequest;
|
||||
use App\Http\Resources\EstimateResource;
|
||||
use App\Http\Resources\InvoiceResource;
|
||||
use App\Jobs\GenerateEstimatePdfJob;
|
||||
use App\Models\Estimate;
|
||||
use App\Models\Invoice;
|
||||
use App\Services\EstimateService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Mail\Markdown;
|
||||
|
||||
class EstimatesController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EstimateService $estimateService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('viewAny', Estimate::class);
|
||||
|
||||
$limit = $request->has('limit') ? $request->limit : 10;
|
||||
|
||||
$estimates = Estimate::whereCompany()
|
||||
->join('customers', 'customers.id', '=', 'estimates.customer_id')
|
||||
->applyFilters($request->all())
|
||||
->select('estimates.*', 'customers.name')
|
||||
->latest()
|
||||
->paginateData($limit);
|
||||
|
||||
return EstimateResource::collection($estimates)
|
||||
->additional(['meta' => [
|
||||
'estimate_total_count' => Estimate::whereCompany()->count(),
|
||||
]]);
|
||||
}
|
||||
|
||||
public function store(EstimatesRequest $request)
|
||||
{
|
||||
$this->authorize('create', Estimate::class);
|
||||
|
||||
$estimate = $this->estimateService->create($request);
|
||||
|
||||
if ($request->has('estimateSend')) {
|
||||
$this->estimateService->send($estimate, $request->only(['title', 'body']));
|
||||
}
|
||||
|
||||
GenerateEstimatePdfJob::dispatch($estimate);
|
||||
|
||||
return new EstimateResource($estimate);
|
||||
}
|
||||
|
||||
public function show(Request $request, Estimate $estimate)
|
||||
{
|
||||
$this->authorize('view', $estimate);
|
||||
|
||||
return new EstimateResource($estimate);
|
||||
}
|
||||
|
||||
public function update(EstimatesRequest $request, Estimate $estimate)
|
||||
{
|
||||
$this->authorize('update', $estimate);
|
||||
|
||||
$estimate = $this->estimateService->update($estimate, $request);
|
||||
|
||||
GenerateEstimatePdfJob::dispatch($estimate, true);
|
||||
|
||||
return new EstimateResource($estimate);
|
||||
}
|
||||
|
||||
public function delete(DeleteEstimatesRequest $request)
|
||||
{
|
||||
$this->authorize('delete multiple estimates');
|
||||
|
||||
$ids = Estimate::whereCompany()
|
||||
->whereIn('id', $request->ids)
|
||||
->pluck('id');
|
||||
|
||||
Estimate::destroy($ids);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function send(SendEstimatesRequest $request, Estimate $estimate)
|
||||
{
|
||||
$this->authorize('send estimate', $estimate);
|
||||
|
||||
$response = $this->estimateService->send($estimate, $request->all());
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
|
||||
public function sendPreview(SendEstimatesRequest $request, Estimate $estimate)
|
||||
{
|
||||
$this->authorize('send estimate', $estimate);
|
||||
|
||||
$markdown = new Markdown(view(), config('mail.markdown'));
|
||||
|
||||
$data = $this->estimateService->sendEstimateData($estimate, $request->all());
|
||||
$data['url'] = $estimate->estimatePdfUrl;
|
||||
|
||||
return $markdown->render('emails.send.estimate', ['data' => $data]);
|
||||
}
|
||||
|
||||
public function clone(Request $request, Estimate $estimate)
|
||||
{
|
||||
$this->authorize('view', $estimate);
|
||||
$this->authorize('create', Estimate::class);
|
||||
|
||||
$newEstimate = $this->estimateService->clone($estimate);
|
||||
|
||||
return new EstimateResource($newEstimate);
|
||||
}
|
||||
|
||||
public function convertToInvoice(Request $request, Estimate $estimate)
|
||||
{
|
||||
$this->authorize('create', Invoice::class);
|
||||
|
||||
$invoice = $this->estimateService->convertToInvoice($estimate);
|
||||
|
||||
return new InvoiceResource($invoice);
|
||||
}
|
||||
|
||||
public function changeStatus(Request $request, Estimate $estimate)
|
||||
{
|
||||
$this->authorize('send estimate', $estimate);
|
||||
|
||||
$this->estimateService->changeStatus($estimate, $request->status);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\ExchangeRate;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\BulkExchangeRateRequest;
|
||||
use App\Http\Requests\ExchangeRateProviderRequest;
|
||||
use App\Http\Resources\ExchangeRateProviderResource;
|
||||
use App\Models\CompanySetting;
|
||||
use App\Models\Currency;
|
||||
use App\Models\Estimate;
|
||||
use App\Models\ExchangeRateLog;
|
||||
use App\Models\ExchangeRateProvider;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Tax;
|
||||
use App\Services\ExchangeRateProviderService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ExchangeRateProviderController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ExchangeRateProviderService $exchangeRateProviderService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('viewAny', ExchangeRateProvider::class);
|
||||
|
||||
$limit = $request->has('limit') ? $request->limit : 5;
|
||||
|
||||
$exchangeRateProviders = ExchangeRateProvider::whereCompany()->paginate($limit);
|
||||
|
||||
return ExchangeRateProviderResource::collection($exchangeRateProviders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function store(ExchangeRateProviderRequest $request)
|
||||
{
|
||||
$this->authorize('create', ExchangeRateProvider::class);
|
||||
|
||||
$query = $this->exchangeRateProviderService->checkActiveCurrencies($request);
|
||||
|
||||
if (count($query) !== 0) {
|
||||
return respondJson('currency_used', 'Currency used.');
|
||||
}
|
||||
|
||||
$checkConverterApi = $this->exchangeRateProviderService->checkProviderStatus($request);
|
||||
|
||||
if ($checkConverterApi->status() == 200) {
|
||||
$exchangeRateProvider = $this->exchangeRateProviderService->create($request);
|
||||
|
||||
return new ExchangeRateProviderResource($exchangeRateProvider);
|
||||
}
|
||||
|
||||
return $checkConverterApi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function show(ExchangeRateProvider $exchangeRateProvider)
|
||||
{
|
||||
$this->authorize('view', $exchangeRateProvider);
|
||||
|
||||
return new ExchangeRateProviderResource($exchangeRateProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function update(ExchangeRateProviderRequest $request, ExchangeRateProvider $exchangeRateProvider)
|
||||
{
|
||||
$this->authorize('update', $exchangeRateProvider);
|
||||
|
||||
$query = $this->exchangeRateProviderService->checkUpdateActiveCurrencies($exchangeRateProvider, $request);
|
||||
|
||||
if (count($query) !== 0) {
|
||||
return respondJson('currency_used', 'Currency used.');
|
||||
}
|
||||
|
||||
$checkConverterApi = $this->exchangeRateProviderService->checkProviderStatus($request);
|
||||
|
||||
if ($checkConverterApi->status() == 200) {
|
||||
$this->exchangeRateProviderService->update($exchangeRateProvider, $request);
|
||||
|
||||
return new ExchangeRateProviderResource($exchangeRateProvider);
|
||||
}
|
||||
|
||||
return $checkConverterApi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function destroy(ExchangeRateProvider $exchangeRateProvider)
|
||||
{
|
||||
$this->authorize('delete', $exchangeRateProvider);
|
||||
|
||||
if ($exchangeRateProvider->active == true) {
|
||||
return respondJson('provider_active', 'Provider Active.');
|
||||
}
|
||||
|
||||
$exchangeRateProvider->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function activeProvider(Request $request, Currency $currency)
|
||||
{
|
||||
$query = ExchangeRateProvider::whereCompany()->whereJsonContains('currencies', $currency->code)
|
||||
->where('active', true)
|
||||
->get();
|
||||
|
||||
if (count($query) !== 0) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'provider_active',
|
||||
], 200);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => 'no_active_provider',
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function getRate(Request $request, Currency $currency)
|
||||
{
|
||||
$settings = CompanySetting::getSettings(['currency'], $request->header('company'));
|
||||
$baseCurrency = Currency::findOrFail($settings['currency']);
|
||||
|
||||
$query = ExchangeRateProvider::whereJsonContains('currencies', $currency->code)
|
||||
->where('active', true)
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$exchangeRate = ExchangeRateLog::where('base_currency_id', $currency->id)
|
||||
->where('currency_id', $baseCurrency->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->value('exchange_rate');
|
||||
|
||||
if ($query) {
|
||||
$filter = Arr::only($query[0], ['key', 'driver', 'driver_config']);
|
||||
$result = $this->exchangeRateProviderService->getExchangeRate(
|
||||
$filter['driver'],
|
||||
$filter['key'],
|
||||
$filter['driver_config'] ?? [],
|
||||
$currency->code,
|
||||
$baseCurrency->code
|
||||
);
|
||||
|
||||
if ($result->status() == 200) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
if ($exchangeRate) {
|
||||
return response()->json([
|
||||
'exchangeRate' => [$exchangeRate],
|
||||
], 200);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => 'no_exchange_rate_available',
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function supportedCurrencies(Request $request)
|
||||
{
|
||||
$this->authorize('viewAny', ExchangeRateProvider::class);
|
||||
|
||||
return $this->exchangeRateProviderService->getSupportedCurrencies(
|
||||
$request->driver,
|
||||
$request->key,
|
||||
$request->driver_config ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public function usedCurrencies(Request $request)
|
||||
{
|
||||
$this->authorize('viewAny', ExchangeRateProvider::class);
|
||||
|
||||
$providerId = $request->provider_id;
|
||||
|
||||
$activeExchangeRateProviders = ExchangeRateProvider::where('active', true)
|
||||
->whereCompany()
|
||||
->when($providerId, function ($query) use ($providerId) {
|
||||
return $query->where('id', '<>', $providerId);
|
||||
})
|
||||
->pluck('currencies');
|
||||
$activeExchangeRateProvider = [];
|
||||
|
||||
foreach ($activeExchangeRateProviders as $data) {
|
||||
if (is_array($data)) {
|
||||
for ($limit = 0; $limit < count($data); $limit++) {
|
||||
$activeExchangeRateProvider[] = $data[$limit];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$allExchangeRateProviders = ExchangeRateProvider::whereCompany()->pluck('currencies');
|
||||
$allExchangeRateProvider = [];
|
||||
|
||||
foreach ($allExchangeRateProviders as $data) {
|
||||
if (is_array($data)) {
|
||||
for ($limit = 0; $limit < count($data); $limit++) {
|
||||
$allExchangeRateProvider[] = $data[$limit];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'allUsedCurrencies' => $allExchangeRateProvider ? $allExchangeRateProvider : [],
|
||||
'activeUsedCurrencies' => $activeExchangeRateProvider ? $activeExchangeRateProvider : [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function usedCurrenciesWithoutRate(Request $request)
|
||||
{
|
||||
$invoices = Invoice::where('exchange_rate', null)->pluck('currency_id')->toArray();
|
||||
$taxes = Tax::where('exchange_rate', null)->pluck('currency_id')->toArray();
|
||||
$estimates = Estimate::where('exchange_rate', null)->pluck('currency_id')->toArray();
|
||||
$payments = Payment::where('exchange_rate', null)->pluck('currency_id')->toArray();
|
||||
|
||||
$currencies = array_merge($invoices, $taxes, $estimates, $payments);
|
||||
|
||||
return response()->json([
|
||||
'currencies' => Currency::whereIn('id', $currencies)->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function bulkUpdate(BulkExchangeRateRequest $request)
|
||||
{
|
||||
$bulkExchangeRate = CompanySetting::getSetting('bulk_exchange_rate_configured', $request->header('company'));
|
||||
|
||||
if ($bulkExchangeRate == 'NO') {
|
||||
if ($request->currencies) {
|
||||
foreach ($request->currencies as $currency) {
|
||||
$currency['exchange_rate'] = $currency['exchange_rate'] ?? 1;
|
||||
|
||||
$invoices = Invoice::where('currency_id', $currency['id'])->get();
|
||||
|
||||
if ($invoices) {
|
||||
foreach ($invoices as $invoice) {
|
||||
$invoice->update([
|
||||
'exchange_rate' => $currency['exchange_rate'],
|
||||
'base_discount_val' => $invoice->sub_total * $currency['exchange_rate'],
|
||||
'base_sub_total' => $invoice->sub_total * $currency['exchange_rate'],
|
||||
'base_total' => $invoice->total * $currency['exchange_rate'],
|
||||
'base_tax' => $invoice->tax * $currency['exchange_rate'],
|
||||
'base_due_amount' => $invoice->due_amount * $currency['exchange_rate'],
|
||||
]);
|
||||
|
||||
$this->updateItemsExchangeRate($invoice);
|
||||
}
|
||||
}
|
||||
|
||||
$estimates = Estimate::where('currency_id', $currency['id'])->get();
|
||||
|
||||
if ($estimates) {
|
||||
foreach ($estimates as $estimate) {
|
||||
$estimate->update([
|
||||
'exchange_rate' => $currency['exchange_rate'],
|
||||
'base_discount_val' => $estimate->sub_total * $currency['exchange_rate'],
|
||||
'base_sub_total' => $estimate->sub_total * $currency['exchange_rate'],
|
||||
'base_total' => $estimate->total * $currency['exchange_rate'],
|
||||
'base_tax' => $estimate->tax * $currency['exchange_rate'],
|
||||
]);
|
||||
|
||||
$this->updateItemsExchangeRate($estimate);
|
||||
}
|
||||
}
|
||||
|
||||
$taxes = Tax::where('currency_id', $currency['id'])->get();
|
||||
|
||||
if ($taxes) {
|
||||
foreach ($taxes as $tax) {
|
||||
$tax->base_amount = $tax->base_amount * $currency['exchange_rate'];
|
||||
$tax->save();
|
||||
}
|
||||
}
|
||||
|
||||
$payments = Payment::where('currency_id', $currency['id'])->get();
|
||||
|
||||
if ($payments) {
|
||||
foreach ($payments as $payment) {
|
||||
$payment->exchange_rate = $currency['exchange_rate'];
|
||||
$payment->base_amount = $payment->amount * $currency['exchange_rate'];
|
||||
$payment->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$settings = [
|
||||
'bulk_exchange_rate_configured' => 'YES',
|
||||
];
|
||||
|
||||
CompanySetting::setSettings($settings, $request->header('company'));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
private function updateItemsExchangeRate($model): void
|
||||
{
|
||||
foreach ($model->items as $item) {
|
||||
$item->update([
|
||||
'exchange_rate' => $model->exchange_rate,
|
||||
'base_discount_val' => $item->discount_val * $model->exchange_rate,
|
||||
'base_price' => $item->price * $model->exchange_rate,
|
||||
'base_tax' => $item->tax * $model->exchange_rate,
|
||||
'base_total' => $item->total * $model->exchange_rate,
|
||||
]);
|
||||
|
||||
$this->updateTaxesExchangeRate($item);
|
||||
}
|
||||
|
||||
$this->updateTaxesExchangeRate($model);
|
||||
}
|
||||
|
||||
private function updateTaxesExchangeRate($model): void
|
||||
{
|
||||
if ($model->taxes()->exists()) {
|
||||
$model->taxes->map(function ($tax) use ($model) {
|
||||
$tax->update([
|
||||
'exchange_rate' => $model->exchange_rate,
|
||||
'base_amount' => $tax->amount * $model->exchange_rate,
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Expense;
|
||||
namespace App\Http\Controllers\Company\Expense;
|
||||
|
||||
use App\ExpensesCategory;
|
||||
use App\Http\Controllers\Controller;
|
||||
@@ -1,17 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Expense;
|
||||
namespace App\Http\Controllers\Company\Expense;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\DeleteExpensesRequest;
|
||||
use App\Http\Requests\ExpenseRequest;
|
||||
use App\Http\Requests\UploadExpenseReceiptRequest;
|
||||
use App\Http\Resources\ExpenseResource;
|
||||
use App\Models\Expense;
|
||||
use App\Services\ExpenseService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ExpensesController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ExpenseService $expenseService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
@@ -46,7 +52,7 @@ class ExpensesController extends Controller
|
||||
{
|
||||
$this->authorize('create', Expense::class);
|
||||
|
||||
$expense = Expense::createExpense($request);
|
||||
$expense = $this->expenseService->create($request);
|
||||
|
||||
return new ExpenseResource($expense);
|
||||
}
|
||||
@@ -72,7 +78,7 @@ class ExpensesController extends Controller
|
||||
{
|
||||
$this->authorize('update', $expense);
|
||||
|
||||
$expense->updateExpense($request);
|
||||
$this->expenseService->update($expense, $request);
|
||||
|
||||
return new ExpenseResource($expense);
|
||||
}
|
||||
@@ -91,4 +97,62 @@ class ExpensesController extends Controller
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function showReceipt(Expense $expense)
|
||||
{
|
||||
$this->authorize('view', $expense);
|
||||
|
||||
if ($expense) {
|
||||
$media = $expense->getFirstMedia('receipts');
|
||||
|
||||
if ($media) {
|
||||
return response()->file($media->getPath());
|
||||
}
|
||||
|
||||
return respondJson('receipt_does_not_exist', 'Receipt does not exist.');
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadReceipt(UploadExpenseReceiptRequest $request, Expense $expense)
|
||||
{
|
||||
$this->authorize('update', $expense);
|
||||
|
||||
$data = json_decode($request->attachment_receipt);
|
||||
|
||||
if ($data) {
|
||||
if ($request->type === 'edit') {
|
||||
$expense->clearMediaCollection('receipts');
|
||||
}
|
||||
|
||||
$expense->addMediaFromBase64($data->data)
|
||||
->usingFileName($data->name)
|
||||
->toMediaCollection('receipts');
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => 'Expense receipts uploaded successfully',
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function downloadReceipt(Expense $expense)
|
||||
{
|
||||
$this->authorize('view', $expense);
|
||||
|
||||
if ($expense) {
|
||||
$media = $expense->getFirstMedia('receipts');
|
||||
if ($media) {
|
||||
$imagePath = $media->getPath();
|
||||
$response = \Response::download($imagePath, $media->file_name);
|
||||
if (ob_get_contents()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => 'receipt_not_found',
|
||||
]);
|
||||
}
|
||||
}
|
||||
156
app/Http/Controllers/Company/General/BootstrapController.php
Normal file
156
app/Http/Controllers/Company/General/BootstrapController.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\General;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\CompanyInvitationResource;
|
||||
use App\Http\Resources\CompanyResource;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\Company;
|
||||
use App\Models\CompanyInvitation;
|
||||
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\Modules\Registry as ModuleRegistry;
|
||||
use Silber\Bouncer\BouncerFacade;
|
||||
|
||||
class BootstrapController extends Controller
|
||||
{
|
||||
use GeneratesMenuTrait;
|
||||
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$current_user = $request->user();
|
||||
$current_user_settings = $current_user->getAllSettings();
|
||||
$companies = $current_user->companies;
|
||||
|
||||
$pendingInvitations = CompanyInvitation::forUser($current_user)
|
||||
->pending()
|
||||
->with(['company', 'role', 'invitedBy'])
|
||||
->get();
|
||||
|
||||
$global_settings = Setting::getSettings([
|
||||
'api_token',
|
||||
'admin_portal_theme',
|
||||
'admin_portal_logo',
|
||||
'login_page_logo',
|
||||
'login_page_heading',
|
||||
'login_page_description',
|
||||
'admin_page_title',
|
||||
'copyright_text',
|
||||
'save_pdf_to_disk',
|
||||
'show_sidebar_group_labels',
|
||||
]);
|
||||
|
||||
// Super admin mode — return admin-only menu with all companies listed
|
||||
if ($current_user->isSuperAdmin() && $request->has('admin_mode')) {
|
||||
return response()->json([
|
||||
'current_user' => new UserResource($current_user),
|
||||
'current_user_settings' => $current_user_settings,
|
||||
'current_user_abilities' => [],
|
||||
'companies' => CompanyResource::collection($companies),
|
||||
'current_company' => null,
|
||||
'current_company_settings' => [],
|
||||
'current_company_currency' => Currency::first(),
|
||||
'config' => config('invoiceshelf'),
|
||||
'global_settings' => $global_settings,
|
||||
'main_menu' => $this->generateMenu('admin_menu', $current_user),
|
||||
'setting_menu' => [],
|
||||
'modules' => [],
|
||||
'admin_mode' => true,
|
||||
'pending_invitations' => CompanyInvitationResource::collection($pendingInvitations),
|
||||
]);
|
||||
}
|
||||
|
||||
// User has no companies — return minimal bootstrap
|
||||
if ($companies->isEmpty()) {
|
||||
return response()->json([
|
||||
'current_user' => new UserResource($current_user),
|
||||
'current_user_settings' => $current_user_settings,
|
||||
'current_user_abilities' => [],
|
||||
'companies' => [],
|
||||
'current_company' => null,
|
||||
'current_company_settings' => [],
|
||||
'current_company_currency' => Currency::first(),
|
||||
'config' => config('invoiceshelf'),
|
||||
'global_settings' => $global_settings,
|
||||
'main_menu' => [],
|
||||
'setting_menu' => [],
|
||||
'modules' => [],
|
||||
'pending_invitations' => CompanyInvitationResource::collection($pendingInvitations),
|
||||
]);
|
||||
}
|
||||
|
||||
$main_menu = $this->generateMenu('main_menu', $current_user);
|
||||
$setting_menu = $this->generateMenu('setting_menu', $current_user);
|
||||
|
||||
// Merge module-registered menu items into the main menu so they
|
||||
// participate in the unified group + priority ordering.
|
||||
foreach (ModuleRegistry::allMenu() as $slug => $item) {
|
||||
$main_menu[] = [
|
||||
'title' => __($item['title']),
|
||||
'link' => $item['link'],
|
||||
'icon' => $item['icon'],
|
||||
'name' => 'module-'.$slug,
|
||||
'group' => $item['group'] ?? 'modules',
|
||||
'group_label' => $item['group_label'] ?? 'navigation.modules',
|
||||
'priority' => $item['priority'] ?? 100,
|
||||
];
|
||||
}
|
||||
|
||||
$current_company = Company::find($request->header('company'));
|
||||
|
||||
if ((! $current_company) || ($current_company && ! $current_user->hasCompany($current_company->id))) {
|
||||
$current_company = $current_user->companies()->first();
|
||||
}
|
||||
|
||||
$current_company_settings = CompanySetting::getAllSettings($current_company->id);
|
||||
|
||||
$current_company_currency = $current_company_settings->has('currency')
|
||||
? Currency::find($current_company_settings->get('currency'))
|
||||
: Currency::first();
|
||||
|
||||
BouncerFacade::refreshFor($current_user);
|
||||
|
||||
return response()->json([
|
||||
'current_user' => new UserResource($current_user),
|
||||
'current_user_settings' => $current_user_settings,
|
||||
'current_user_abilities' => $current_user->getAbilities(),
|
||||
'companies' => CompanyResource::collection($companies),
|
||||
'current_company' => new CompanyResource($current_company),
|
||||
'current_company_settings' => $current_company_settings,
|
||||
'current_company_currency' => $current_company_currency,
|
||||
'config' => config('invoiceshelf'),
|
||||
'global_settings' => $global_settings,
|
||||
'main_menu' => $main_menu,
|
||||
'setting_menu' => $setting_menu,
|
||||
'modules' => Module::where('enabled', true)->pluck('name'),
|
||||
'user_menu' => collect(ModuleRegistry::allUserMenu())
|
||||
->map(fn (array $item, string $slug) => [
|
||||
...$item,
|
||||
'title' => __($item['title']),
|
||||
'name' => 'module-'.$slug,
|
||||
])
|
||||
->sortBy('priority')
|
||||
->values()
|
||||
->all(),
|
||||
'pending_invitations' => CompanyInvitationResource::collection($pendingInvitations),
|
||||
]);
|
||||
}
|
||||
|
||||
public function currentCompany(Request $request)
|
||||
{
|
||||
$company = Company::find($request->header('company'));
|
||||
|
||||
return new CompanyResource($company);
|
||||
}
|
||||
}
|
||||
49
app/Http/Controllers/Company/General/ConfigController.php
Normal file
49
app/Http/Controllers/Company/General/ConfigController.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\General;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use InvoiceShelf\Modules\Registry;
|
||||
|
||||
class ConfigController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*/
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
if ($request->key === 'exchange_rate_drivers') {
|
||||
return response()->json([
|
||||
'exchange_rate_drivers' => $this->exchangeRateDrivers(),
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
$request->key => config('invoiceshelf.'.$request->key),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the exchange rate driver list from the module Registry.
|
||||
*
|
||||
* Returns enriched objects (with label, website, and config_fields) so the
|
||||
* frontend can render driver-specific configuration forms without hardcoding
|
||||
* any per-driver UI.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
protected function exchangeRateDrivers(): array
|
||||
{
|
||||
return collect(Registry::allDrivers('exchange_rate'))
|
||||
->map(fn (array $meta, string $name) => [
|
||||
'value' => $name,
|
||||
'label' => $meta['label'] ?? $name,
|
||||
'website' => $meta['website'] ?? '',
|
||||
'config_fields' => $meta['config_fields'] ?? [],
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
}
|
||||
33
app/Http/Controllers/Company/General/FormatsController.php
Normal file
33
app/Http/Controllers/Company/General/FormatsController.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\General;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\Formatters\DateFormatter;
|
||||
use App\Support\Formatters\TimeFormatter;
|
||||
use App\Support\Formatters\TimeZones;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class FormatsController extends Controller
|
||||
{
|
||||
public function dateFormats(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'date_formats' => DateFormatter::get_list(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function timeFormats(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'time_formats' => TimeFormatter::get_list(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function timezones(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'time_zones' => TimeZones::get_list(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\General;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\CompanyInvitationResource;
|
||||
use App\Models\CompanyInvitation;
|
||||
use App\Services\InvitationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class InvitationResponseController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InvitationService $invitationService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get pending invitations for the authenticated user.
|
||||
*/
|
||||
public function pending(Request $request): JsonResponse
|
||||
{
|
||||
$invitations = $this->invitationService->getPendingForUser($request->user());
|
||||
|
||||
return response()->json([
|
||||
'invitations' => CompanyInvitationResource::collection($invitations),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a company invitation.
|
||||
*/
|
||||
public function accept(Request $request, CompanyInvitation $invitation): JsonResponse
|
||||
{
|
||||
$this->invitationService->accept($invitation, $request->user());
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decline a company invitation.
|
||||
*/
|
||||
public function decline(Request $request, CompanyInvitation $invitation): JsonResponse
|
||||
{
|
||||
$this->invitationService->decline($invitation, $request->user());
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\General;
|
||||
namespace App\Http\Controllers\Company\General;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\NotesRequest;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\General;
|
||||
namespace App\Http\Controllers\Company\General;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Customer;
|
||||
@@ -36,4 +36,15 @@ class SearchController extends Controller
|
||||
'users' => $users ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function users(Request $request)
|
||||
{
|
||||
$this->authorize('create', User::class);
|
||||
|
||||
$users = User::whereEmail($request->email)
|
||||
->latest()
|
||||
->paginate(10);
|
||||
|
||||
return response()->json(['users' => $users]);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\General;
|
||||
namespace App\Http\Controllers\Company\General;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Estimate;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Services\SerialNumberFormatter;
|
||||
use App\Services\SerialNumberService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class NextNumberController extends Controller
|
||||
class SerialNumberController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function __invoke(Request $request, Invoice $invoice, Estimate $estimate, Payment $payment)
|
||||
public function nextNumber(Request $request, Invoice $invoice, Estimate $estimate, Payment $payment): JsonResponse
|
||||
{
|
||||
$key = $request->key;
|
||||
$nextNumber = null;
|
||||
$serial = (new SerialNumberFormatter)
|
||||
$serial = (new SerialNumberService)
|
||||
->setCompany($request->header('company'))
|
||||
->setCustomer($request->userId);
|
||||
|
||||
@@ -49,7 +44,9 @@ class NextNumberController extends Controller
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
return response()->json([
|
||||
@@ -63,4 +60,18 @@ class NextNumberController extends Controller
|
||||
'nextNumber' => $nextNumber,
|
||||
]);
|
||||
}
|
||||
|
||||
public function placeholders(Request $request): JsonResponse
|
||||
{
|
||||
if ($request->input('format')) {
|
||||
$placeholders = SerialNumberService::getPlaceholders($request->input('format'));
|
||||
} else {
|
||||
$placeholders = [];
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'placeholders' => $placeholders,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Invoice;
|
||||
namespace App\Http\Controllers\Company\Invoice;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Invoice;
|
||||
use App\Space\PdfTemplateUtils;
|
||||
use App\Services\Pdf\PdfTemplateUtils;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -1,18 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Invoice;
|
||||
namespace App\Http\Controllers\Company\Invoice;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests;
|
||||
use App\Http\Requests\DeleteInvoiceRequest;
|
||||
use App\Http\Requests\SendInvoiceRequest;
|
||||
use App\Http\Resources\EstimateResource;
|
||||
use App\Http\Resources\InvoiceResource;
|
||||
use App\Jobs\GenerateInvoicePdfJob;
|
||||
use App\Models\Estimate;
|
||||
use App\Models\Invoice;
|
||||
use App\Services\InvoiceService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Mail\Markdown;
|
||||
|
||||
class InvoicesController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InvoiceService $invoiceService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
@@ -46,10 +55,10 @@ class InvoicesController extends Controller
|
||||
{
|
||||
$this->authorize('create', Invoice::class);
|
||||
|
||||
$invoice = Invoice::createInvoice($request);
|
||||
$invoice = $this->invoiceService->create($request);
|
||||
|
||||
if ($request->has('invoiceSend')) {
|
||||
$invoice->send($request->subject, $request->body);
|
||||
$this->invoiceService->send($invoice, $request->only(['subject', 'body']));
|
||||
}
|
||||
|
||||
GenerateInvoicePdfJob::dispatch($invoice);
|
||||
@@ -79,11 +88,7 @@ class InvoicesController extends Controller
|
||||
{
|
||||
$this->authorize('update', $invoice);
|
||||
|
||||
$invoice = $invoice->updateInvoice($request);
|
||||
|
||||
if (is_string($invoice)) {
|
||||
return respondJson($invoice, $invoice);
|
||||
}
|
||||
$invoice = $this->invoiceService->update($invoice, $request);
|
||||
|
||||
GenerateInvoicePdfJob::dispatch($invoice, true);
|
||||
|
||||
@@ -104,7 +109,60 @@ class InvoicesController extends Controller
|
||||
->whereIn('id', $request->ids)
|
||||
->pluck('id');
|
||||
|
||||
Invoice::deleteInvoices($ids);
|
||||
$this->invoiceService->delete($ids);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function send(SendInvoiceRequest $request, Invoice $invoice)
|
||||
{
|
||||
$this->authorize('send invoice', $invoice);
|
||||
|
||||
$this->invoiceService->send($invoice, $request->all());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendPreview(SendInvoiceRequest $request, Invoice $invoice)
|
||||
{
|
||||
$this->authorize('send invoice', $invoice);
|
||||
|
||||
$markdown = new Markdown(view(), config('mail.markdown'));
|
||||
|
||||
$data = $this->invoiceService->sendInvoiceData($invoice, $request->all());
|
||||
$data['url'] = $invoice->invoicePdfUrl;
|
||||
|
||||
return $markdown->render('emails.send.invoice', ['data' => $data]);
|
||||
}
|
||||
|
||||
public function clone(Request $request, Invoice $invoice)
|
||||
{
|
||||
$this->authorize('view', $invoice);
|
||||
$this->authorize('create', Invoice::class);
|
||||
|
||||
$newInvoice = $this->invoiceService->clone($invoice);
|
||||
|
||||
return new InvoiceResource($newInvoice);
|
||||
}
|
||||
|
||||
public function convertToEstimate(Request $request, Invoice $invoice)
|
||||
{
|
||||
$this->authorize('create', Estimate::class);
|
||||
|
||||
$estimate = $this->invoiceService->convertToEstimate($invoice);
|
||||
|
||||
return new EstimateResource($estimate);
|
||||
}
|
||||
|
||||
public function changeStatus(Request $request, Invoice $invoice)
|
||||
{
|
||||
$this->authorize('send invoice', $invoice);
|
||||
|
||||
$this->invoiceService->changeStatus($invoice, $request->status);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Item;
|
||||
namespace App\Http\Controllers\Company\Item;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests;
|
||||
@@ -8,11 +8,16 @@ use App\Http\Requests\DeleteItemsRequest;
|
||||
use App\Http\Resources\ItemResource;
|
||||
use App\Models\Item;
|
||||
use App\Models\TaxType;
|
||||
use App\Services\ItemService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ItemsController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ItemService $itemService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve a list of existing Items.
|
||||
*
|
||||
@@ -48,7 +53,7 @@ class ItemsController extends Controller
|
||||
{
|
||||
$this->authorize('create', Item::class);
|
||||
|
||||
$item = Item::createItem($request);
|
||||
$item = $this->itemService->create($request);
|
||||
|
||||
return new ItemResource($item);
|
||||
}
|
||||
@@ -75,7 +80,7 @@ class ItemsController extends Controller
|
||||
{
|
||||
$this->authorize('update', $item);
|
||||
|
||||
$item = $item->updateItem($request);
|
||||
$item = $this->itemService->update($item, $request);
|
||||
|
||||
return new ItemResource($item);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Item;
|
||||
namespace App\Http\Controllers\Company\Item;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\UnitRequest;
|
||||
@@ -1,17 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Users;
|
||||
namespace App\Http\Controllers\Company\Members;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\DeleteUserRequest;
|
||||
use App\Http\Requests\UserRequest;
|
||||
use App\Http\Requests\DeleteMemberRequest;
|
||||
use App\Http\Requests\MemberRequest;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\User;
|
||||
use App\Services\MemberService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UsersController extends Controller
|
||||
class MembersController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MemberService $memberService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
@@ -40,14 +45,13 @@ class UsersController extends Controller
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\UserRequest $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function store(UserRequest $request)
|
||||
public function store(MemberRequest $request)
|
||||
{
|
||||
$this->authorize('create', User::class);
|
||||
|
||||
$user = User::createFromRequest($request);
|
||||
$user = $this->memberService->create($request);
|
||||
|
||||
return new UserResource($user);
|
||||
}
|
||||
@@ -67,14 +71,13 @@ class UsersController extends Controller
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\UserRequest $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function update(UserRequest $request, User $user)
|
||||
public function update(MemberRequest $request, User $user)
|
||||
{
|
||||
$this->authorize('update', $user);
|
||||
|
||||
$user->updateFromRequest($request);
|
||||
$this->memberService->update($user, $request);
|
||||
|
||||
return new UserResource($user);
|
||||
}
|
||||
@@ -85,12 +88,12 @@ class UsersController extends Controller
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function delete(DeleteUserRequest $request)
|
||||
public function delete(DeleteMemberRequest $request)
|
||||
{
|
||||
$this->authorize('delete multiple users', User::class);
|
||||
|
||||
if ($request->users) {
|
||||
User::deleteUsers($request->users);
|
||||
$this->memberService->delete($request->users);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\Modules;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Module;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Str;
|
||||
use InvoiceShelf\Modules\Registry as ModuleRegistry;
|
||||
|
||||
/**
|
||||
* Read-only company-context Active Modules index.
|
||||
*
|
||||
* Lists every module the super admin has activated on this instance
|
||||
* (Module::enabled = true) and reports whether each one has registered a
|
||||
* settings schema. The frontend uses this to render the company-context
|
||||
* "Modules" landing page with a Settings button per active module.
|
||||
*
|
||||
* Activation is instance-global; per-company customization happens through
|
||||
* settings (per CompanySetting under the module.{slug}.* prefix).
|
||||
*
|
||||
* Slug convention: nwidart stores the module's PascalCase class name in
|
||||
* `modules.name` (e.g. "HelloWorld"), but URLs and registry keys use the
|
||||
* kebab-case form ("hello-world") for readability. We normalize via
|
||||
* Str::kebab() so module authors can call Registry::registerMenu('hello-world')
|
||||
* naturally without thinking about the storage format.
|
||||
*/
|
||||
class CompanyModulesController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$this->authorize('manage modules');
|
||||
|
||||
$modules = Module::query()
|
||||
->where('enabled', true)
|
||||
->get()
|
||||
->map(function (Module $module) {
|
||||
$slug = Str::kebab($module->name);
|
||||
$menu = ModuleRegistry::menuFor($slug);
|
||||
$translatedMenuTitle = $this->translateMenuTitle($menu['title'] ?? null);
|
||||
$displayName = $translatedMenuTitle ?? Str::headline($module->name);
|
||||
|
||||
return [
|
||||
'slug' => $slug,
|
||||
'name' => $module->name,
|
||||
'display_name' => $displayName,
|
||||
'version' => $module->version,
|
||||
'has_settings' => ModuleRegistry::settingsFor($slug) !== null,
|
||||
'menu' => $menu === null
|
||||
? null
|
||||
: [
|
||||
...$menu,
|
||||
'title' => $translatedMenuTitle ?? $menu['title'],
|
||||
],
|
||||
];
|
||||
})
|
||||
->values();
|
||||
|
||||
return response()->json(['data' => $modules]);
|
||||
}
|
||||
|
||||
private function translateMenuTitle(?string $title): ?string
|
||||
{
|
||||
if ($title === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$translatedTitle = __($title);
|
||||
|
||||
if (! is_string($translatedTitle) || $translatedTitle === $title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $translatedTitle;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\Modules;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CompanySetting;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use InvoiceShelf\Modules\Registry as ModuleRegistry;
|
||||
use InvoiceShelf\Modules\Settings\Schema;
|
||||
|
||||
/**
|
||||
* Schema-driven module settings backend.
|
||||
*
|
||||
* Each active module's ServiceProvider::boot() calls
|
||||
* Registry::registerSettings($slug, $schema) once at app boot. This controller
|
||||
* exposes that schema to the frontend, validates submitted values against the
|
||||
* schema's per-field rules, and persists per-company values into CompanySetting
|
||||
* under the key prefix `module.{slug}.{field_key}`.
|
||||
*
|
||||
* Activation is instance-global, but settings are per-company — two companies
|
||||
* on the same instance can configure the same activated module differently.
|
||||
*/
|
||||
class ModuleSettingsController extends Controller
|
||||
{
|
||||
public function show(Request $request, string $slug): JsonResponse
|
||||
{
|
||||
$this->authorize('manage modules');
|
||||
|
||||
$schema = ModuleRegistry::settingsFor($slug);
|
||||
|
||||
if ($schema === null) {
|
||||
abort(404, "Module '{$slug}' has not registered a settings schema.");
|
||||
}
|
||||
|
||||
$values = collect($schema->fields())
|
||||
->mapWithKeys(fn (array $field) => [
|
||||
$field['key'] => CompanySetting::getSetting(
|
||||
"module.{$slug}.{$field['key']}",
|
||||
$request->header('company')
|
||||
) ?? $field['default'],
|
||||
])
|
||||
->all();
|
||||
|
||||
return response()->json([
|
||||
'schema' => $this->translateSchema($schema->toArray()),
|
||||
'values' => $values,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, string $slug): JsonResponse
|
||||
{
|
||||
$this->authorize('manage modules');
|
||||
|
||||
$schema = ModuleRegistry::settingsFor($slug);
|
||||
|
||||
if ($schema === null) {
|
||||
abort(404, "Module '{$slug}' has not registered a settings schema.");
|
||||
}
|
||||
|
||||
$rules = $this->buildRules($schema);
|
||||
$allowedKeys = array_keys($rules);
|
||||
|
||||
$validated = $request->validate($rules);
|
||||
|
||||
$companyId = $request->header('company');
|
||||
|
||||
// Only persist keys the schema knows about — silently drop unknown keys
|
||||
// rather than letting modules write arbitrary settings.
|
||||
$settingsToWrite = [];
|
||||
foreach ($allowedKeys as $key) {
|
||||
if (array_key_exists($key, $validated)) {
|
||||
$settingsToWrite["module.{$slug}.{$key}"] = $this->normalizeForStorage($validated[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($settingsToWrite !== []) {
|
||||
CompanySetting::setSettings($settingsToWrite, $companyId);
|
||||
}
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Schema's field rule arrays into a flat Laravel validator rules array.
|
||||
*
|
||||
* Field rules are passed through verbatim — a field declared as
|
||||
* `'rules' => ['required', 'string', 'max:255']` becomes
|
||||
* `['my_field' => ['required', 'string', 'max:255']]`. The frontend's
|
||||
* BaseSchemaForm.vue understands a subset of these for client-side validation;
|
||||
* the backend validator is the source of truth.
|
||||
*
|
||||
* @return array<string, array<int, string>>
|
||||
*/
|
||||
private function buildRules(Schema $schema): array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
foreach ($schema->fields() as $field) {
|
||||
$rules[$field['key']] = $this->withTypeRule($field);
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend a sensible per-type validation rule so booleans must be booleans,
|
||||
* numbers must be numeric, etc., even if the module didn't declare it.
|
||||
*
|
||||
* @param array<string, mixed> $field
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function withTypeRule(array $field): array
|
||||
{
|
||||
/** @var array<int, string> $declared */
|
||||
$declared = $field['rules'] ?? [];
|
||||
|
||||
$typeRule = match ($field['type']) {
|
||||
'switch' => 'boolean',
|
||||
'number' => 'numeric',
|
||||
'multiselect' => 'array',
|
||||
default => 'nullable',
|
||||
};
|
||||
|
||||
// Avoid duplicating the type rule if the module already declared it
|
||||
if (in_array($typeRule, $declared, true)) {
|
||||
return $declared;
|
||||
}
|
||||
|
||||
return array_merge([$typeRule], $declared);
|
||||
}
|
||||
|
||||
/**
|
||||
* CompanySetting stores everything as strings. Cast booleans, ints, and
|
||||
* arrays to a representation that round-trips through getSetting/setSetting
|
||||
* without losing information. Reads happen in show() above and naturally
|
||||
* return strings; the frontend handles re-coercion in BaseSchemaForm.vue.
|
||||
*/
|
||||
/**
|
||||
* Translate section titles and field labels in the schema so the
|
||||
* frontend receives ready-to-display strings instead of Laravel
|
||||
* translation keys it cannot resolve (e.g. `helloworld::settings.greeting`).
|
||||
*
|
||||
* @param array{sections: list<array<string, mixed>>} $schema
|
||||
* @return array{sections: list<array<string, mixed>>}
|
||||
*/
|
||||
private function translateSchema(array $schema): array
|
||||
{
|
||||
foreach ($schema['sections'] as &$section) {
|
||||
if (isset($section['title'])) {
|
||||
$section['title'] = __($section['title']);
|
||||
}
|
||||
|
||||
foreach ($section['fields'] as &$field) {
|
||||
if (isset($field['label'])) {
|
||||
$field['label'] = __($field['label']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
private function normalizeForStorage(mixed $value): string
|
||||
{
|
||||
if (is_bool($value)) {
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return json_encode($value, JSON_UNESCAPED_SLASHES) ?: '[]';
|
||||
}
|
||||
|
||||
return (string) ($value ?? '');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Payment;
|
||||
namespace App\Http\Controllers\Company\Payment;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\PaymentMethodRequest;
|
||||
@@ -1,17 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Payment;
|
||||
namespace App\Http\Controllers\Company\Payment;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\DeletePaymentsRequest;
|
||||
use App\Http\Requests\PaymentRequest;
|
||||
use App\Http\Requests\SendPaymentRequest;
|
||||
use App\Http\Resources\PaymentResource;
|
||||
use App\Models\Payment;
|
||||
use App\Services\PaymentService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Mail\Markdown;
|
||||
|
||||
class PaymentsController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PaymentService $paymentService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
@@ -48,7 +55,7 @@ class PaymentsController extends Controller
|
||||
{
|
||||
$this->authorize('create', Payment::class);
|
||||
|
||||
$payment = Payment::createPayment($request);
|
||||
$payment = $this->paymentService->create($request);
|
||||
|
||||
return new PaymentResource($payment);
|
||||
}
|
||||
@@ -64,7 +71,7 @@ class PaymentsController extends Controller
|
||||
{
|
||||
$this->authorize('update', $payment);
|
||||
|
||||
$payment = $payment->updatePayment($request);
|
||||
$payment = $this->paymentService->update($payment, $request);
|
||||
|
||||
return new PaymentResource($payment);
|
||||
}
|
||||
@@ -77,10 +84,31 @@ class PaymentsController extends Controller
|
||||
->whereIn('id', $request->ids)
|
||||
->pluck('id');
|
||||
|
||||
Payment::deletePayments($ids);
|
||||
$this->paymentService->delete($ids);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function send(SendPaymentRequest $request, Payment $payment)
|
||||
{
|
||||
$this->authorize('send payment', $payment);
|
||||
|
||||
$response = $this->paymentService->send($payment, $request->all());
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
|
||||
public function sendPreview(Request $request, Payment $payment)
|
||||
{
|
||||
$this->authorize('send payment', $payment);
|
||||
|
||||
$markdown = new Markdown(view(), config('mail.markdown'));
|
||||
|
||||
$data = $this->paymentService->sendPaymentData($payment, $request->all());
|
||||
$data['url'] = $payment->paymentPdfUrl;
|
||||
|
||||
return $markdown->render('emails.send.payment', ['data' => $data]);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\RecurringInvoice;
|
||||
namespace App\Http\Controllers\Company\RecurringInvoice;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\RecurringInvoiceRequest;
|
||||
use App\Http\Resources\RecurringInvoiceResource;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Services\RecurringInvoiceService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class RecurringInvoiceController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RecurringInvoiceService $recurringInvoiceService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
@@ -42,7 +47,7 @@ class RecurringInvoiceController extends Controller
|
||||
{
|
||||
$this->authorize('create', RecurringInvoice::class);
|
||||
|
||||
$recurringInvoice = RecurringInvoice::createFromRequest($request);
|
||||
$recurringInvoice = $this->recurringInvoiceService->create($request);
|
||||
|
||||
return new RecurringInvoiceResource($recurringInvoice);
|
||||
}
|
||||
@@ -69,7 +74,7 @@ class RecurringInvoiceController extends Controller
|
||||
{
|
||||
$this->authorize('update', $recurringInvoice);
|
||||
|
||||
$recurringInvoice->updateFromRequest($request);
|
||||
$this->recurringInvoiceService->update($recurringInvoice, $request);
|
||||
|
||||
return new RecurringInvoiceResource($recurringInvoice);
|
||||
}
|
||||
@@ -88,7 +93,7 @@ class RecurringInvoiceController extends Controller
|
||||
->whereIn('id', $request->ids)
|
||||
->pluck('id');
|
||||
|
||||
RecurringInvoice::deleteRecurringInvoice($ids);
|
||||
$this->recurringInvoiceService->delete($ids);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\RecurringInvoice;
|
||||
namespace App\Http\Controllers\Company\RecurringInvoice;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\RecurringInvoice;
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Report;
|
||||
namespace App\Http\Controllers\Company\Report;
|
||||
|
||||
use App\Facades\Pdf;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Company;
|
||||
use App\Models\CompanySetting;
|
||||
@@ -11,7 +12,6 @@ use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use PDF;
|
||||
|
||||
class CustomerSalesReportController extends Controller
|
||||
{
|
||||
@@ -85,7 +85,7 @@ class CustomerSalesReportController extends Controller
|
||||
'currency' => $currency,
|
||||
]);
|
||||
|
||||
$pdf = PDF::loadView('app.pdf.reports.sales-customers');
|
||||
$pdf = Pdf::loadView('app.pdf.reports.sales-customers');
|
||||
|
||||
if ($request->has('preview')) {
|
||||
return view('app.pdf.reports.sales-customers');
|
||||
@@ -1,17 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Report;
|
||||
namespace App\Http\Controllers\Company\Report;
|
||||
|
||||
use App\Facades\Pdf;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Company;
|
||||
use App\Models\CompanySetting;
|
||||
use App\Models\Currency;
|
||||
use App\Models\Expense;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use PDF;
|
||||
|
||||
class ExpensesReportController extends Controller
|
||||
{
|
||||
@@ -19,7 +20,7 @@ class ExpensesReportController extends Controller
|
||||
* Handle the incoming request.
|
||||
*
|
||||
* @param string $hash
|
||||
* @return JsonResponse
|
||||
* @return View|Response
|
||||
*/
|
||||
public function __invoke(Request $request, $hash)
|
||||
{
|
||||
@@ -31,14 +32,26 @@ class ExpensesReportController extends Controller
|
||||
|
||||
App::setLocale($locale);
|
||||
|
||||
$expenseCategories = Expense::with('category')
|
||||
// Fetch individual expenses (filtered and ordered by date), then group by category
|
||||
$expenses = Expense::with('category')
|
||||
->whereCompanyId($company->id)
|
||||
->applyFilters($request->only(['from_date', 'to_date']))
|
||||
->expensesAttributes()
|
||||
->applyFilters($request->only(['from_date', 'to_date', 'expense_category_id']))
|
||||
->orderBy('expense_date', 'asc')
|
||||
->get();
|
||||
$totalAmount = 0;
|
||||
foreach ($expenseCategories as $category) {
|
||||
$totalAmount += $category->total_amount;
|
||||
|
||||
$totalAmount = $expenses->sum('base_amount');
|
||||
|
||||
$grouped = $expenses->groupBy(function ($item) {
|
||||
return $item->category ? $item->category->name : trans('expenses.uncategorized');
|
||||
});
|
||||
|
||||
$expenseGroups = collect();
|
||||
foreach ($grouped as $categoryName => $group) {
|
||||
$expenseGroups->push([
|
||||
'name' => $categoryName,
|
||||
'expenses' => $group,
|
||||
'total' => $group->sum('base_amount'),
|
||||
]);
|
||||
}
|
||||
|
||||
$dateFormat = CompanySetting::getSetting('carbon_date_format', $company->id);
|
||||
@@ -62,7 +75,7 @@ class ExpensesReportController extends Controller
|
||||
->get();
|
||||
|
||||
view()->share([
|
||||
'expenseCategories' => $expenseCategories,
|
||||
'expenseGroups' => $expenseGroups,
|
||||
'colorSettings' => $colorSettings,
|
||||
'totalExpense' => $totalAmount,
|
||||
'company' => $company,
|
||||
@@ -70,7 +83,7 @@ class ExpensesReportController extends Controller
|
||||
'to_date' => $to_date,
|
||||
'currency' => $currency,
|
||||
]);
|
||||
$pdf = PDF::loadView('app.pdf.reports.expenses');
|
||||
$pdf = Pdf::loadView('app.pdf.reports.expenses');
|
||||
|
||||
if ($request->has('preview')) {
|
||||
return view('app.pdf.reports.expenses');
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Report;
|
||||
namespace App\Http\Controllers\Company\Report;
|
||||
|
||||
use App\Facades\Pdf;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Company;
|
||||
use App\Models\CompanySetting;
|
||||
@@ -11,7 +12,6 @@ use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use PDF;
|
||||
|
||||
class ItemSalesReportController extends Controller
|
||||
{
|
||||
@@ -70,7 +70,7 @@ class ItemSalesReportController extends Controller
|
||||
'to_date' => $to_date,
|
||||
'currency' => $currency,
|
||||
]);
|
||||
$pdf = PDF::loadView('app.pdf.reports.sales-items');
|
||||
$pdf = Pdf::loadView('app.pdf.reports.sales-items');
|
||||
|
||||
if ($request->has('preview')) {
|
||||
return view('app.pdf.reports.sales-items');
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Report;
|
||||
namespace App\Http\Controllers\Company\Report;
|
||||
|
||||
use App\Facades\Pdf;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Company;
|
||||
use App\Models\CompanySetting;
|
||||
@@ -12,7 +13,6 @@ use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use PDF;
|
||||
|
||||
class ProfitLossReportController extends Controller
|
||||
{
|
||||
@@ -78,7 +78,7 @@ class ProfitLossReportController extends Controller
|
||||
'to_date' => $to_date,
|
||||
'currency' => $currency,
|
||||
]);
|
||||
$pdf = PDF::loadView('app.pdf.reports.profit-loss');
|
||||
$pdf = Pdf::loadView('app.pdf.reports.profit-loss');
|
||||
|
||||
if ($request->has('preview')) {
|
||||
return view('app.pdf.reports.profit-loss');
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Report;
|
||||
namespace App\Http\Controllers\Company\Report;
|
||||
|
||||
use App\Facades\Pdf;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Company;
|
||||
use App\Models\CompanySetting;
|
||||
@@ -11,7 +12,6 @@ use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use PDF;
|
||||
|
||||
class TaxSummaryReportController extends Controller
|
||||
{
|
||||
@@ -73,7 +73,7 @@ class TaxSummaryReportController extends Controller
|
||||
'currency' => $currency,
|
||||
]);
|
||||
|
||||
$pdf = PDF::loadView('app.pdf.reports.tax-summary');
|
||||
$pdf = Pdf::loadView('app.pdf.reports.tax-summary');
|
||||
|
||||
if ($request->has('preview')) {
|
||||
return view('app.pdf.reports.tax-summary');
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Role;
|
||||
namespace App\Http\Controllers\Company\Role;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Role;
|
||||
namespace App\Http\Controllers\Company\Role;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\RoleRequest;
|
||||
53
app/Http/Controllers/Company/Settings/CompanyController.php
Normal file
53
app/Http/Controllers/Company/Settings/CompanyController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\CompanyLogoRequest;
|
||||
use App\Http\Requests\CompanyRequest;
|
||||
use App\Http\Resources\CompanyResource;
|
||||
use App\Models\Company;
|
||||
|
||||
class CompanyController extends Controller
|
||||
{
|
||||
public function updateCompany(CompanyRequest $request)
|
||||
{
|
||||
$company = Company::find($request->header('company'));
|
||||
|
||||
$this->authorize('manage company', $company);
|
||||
|
||||
$company->update($request->getCompanyPayload());
|
||||
|
||||
$company->address()->updateOrCreate(['company_id' => $company->id], $request->address);
|
||||
|
||||
return new CompanyResource($company);
|
||||
}
|
||||
|
||||
public function uploadCompanyLogo(CompanyLogoRequest $request)
|
||||
{
|
||||
$company = Company::find($request->header('company'));
|
||||
|
||||
$this->authorize('manage company', $company);
|
||||
|
||||
$data = json_decode($request->company_logo);
|
||||
|
||||
if (isset($request->is_company_logo_removed) && (bool) $request->is_company_logo_removed) {
|
||||
$company->clearMediaCollection('logo');
|
||||
}
|
||||
if ($data) {
|
||||
$company = Company::find($request->header('company'));
|
||||
|
||||
if ($company) {
|
||||
$company->clearMediaCollection('logo');
|
||||
|
||||
$company->addMediaFromBase64($data->data)
|
||||
->usingFileName($data->name)
|
||||
->toMediaCollection('logo');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\CompanyMailConfigurationRequest;
|
||||
use App\Mail\TestMail;
|
||||
use App\Services\CompanyMailConfigService;
|
||||
use App\Services\MailConfigurationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class CompanyMailConfigurationController extends Controller
|
||||
{
|
||||
public function __construct(private readonly MailConfigurationService $mailConfigurationService) {}
|
||||
|
||||
public function getDefaultConfig(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json($this->mailConfigurationService->getDefaultConfig());
|
||||
}
|
||||
|
||||
public function getMailConfig(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json(
|
||||
$this->mailConfigurationService->getCompanyConfig($request->header('company'))
|
||||
);
|
||||
}
|
||||
|
||||
public function saveMailConfig(CompanyMailConfigurationRequest $request): JsonResponse
|
||||
{
|
||||
$this->authorize('owner only');
|
||||
|
||||
$this->mailConfigurationService->saveCompanyConfig(
|
||||
$request->header('company'),
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
public function testMailConfig(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('owner only');
|
||||
|
||||
$this->validate($request, [
|
||||
'to' => 'required|email',
|
||||
'subject' => 'required',
|
||||
'message' => 'required',
|
||||
]);
|
||||
|
||||
CompanyMailConfigService::apply($request->header('company'));
|
||||
|
||||
Mail::to($request->to)->send(new TestMail($request->subject, $request->message));
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\GetSettingsRequest;
|
||||
use App\Http\Requests\UpdateSettingsRequest;
|
||||
use App\Models\Company;
|
||||
use App\Models\CompanySetting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Silber\Bouncer\BouncerFacade;
|
||||
|
||||
class CompanySettingsController extends Controller
|
||||
{
|
||||
public function show(GetSettingsRequest $request): JsonResponse
|
||||
{
|
||||
$settings = CompanySetting::getSettings((array) $request->settings, $request->header('company'));
|
||||
|
||||
return response()->json($settings);
|
||||
}
|
||||
|
||||
public function update(UpdateSettingsRequest $request): JsonResponse
|
||||
{
|
||||
$company = Company::find($request->header('company'));
|
||||
$this->authorize('manage company', $company);
|
||||
|
||||
$data = $request->settings;
|
||||
|
||||
if (
|
||||
Arr::exists($data, 'currency') &&
|
||||
(CompanySetting::getSetting('currency', $company->id) !== $data['currency']) &&
|
||||
$company->hasTransactions()
|
||||
) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Cannot update company currency after transactions are created.',
|
||||
]);
|
||||
}
|
||||
|
||||
CompanySetting::setSettings($data, $request->header('company'));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function checkTransactions(Request $request): JsonResponse
|
||||
{
|
||||
$company = Company::find($request->header('company'));
|
||||
|
||||
$this->authorize('manage company', $company);
|
||||
|
||||
return response()->json([
|
||||
'has_transactions' => $company->hasTransactions(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function transferOwnership(Request $request, User $user): JsonResponse
|
||||
{
|
||||
$company = Company::find($request->header('company'));
|
||||
$this->authorize('transfer company ownership', $company);
|
||||
|
||||
if (! $user->hasCompany($company->id)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'User does not belong to this company.',
|
||||
]);
|
||||
}
|
||||
|
||||
$company->update(['owner_id' => $user->id]);
|
||||
BouncerFacade::scope()->to($company->id);
|
||||
BouncerFacade::sync($user)->roles(['owner']);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\CompanyInvitationResource;
|
||||
use App\Models\Company;
|
||||
use App\Models\CompanyInvitation;
|
||||
use App\Services\InvitationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class InvitationController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InvitationService $invitationService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$company = Company::find($request->header('company'));
|
||||
|
||||
$invitations = CompanyInvitation::where('company_id', $company->id)
|
||||
->pending()
|
||||
->with(['role', 'invitedBy'])
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'invitations' => CompanyInvitationResource::collection($invitations),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
'role_id' => 'required|exists:roles,id',
|
||||
]);
|
||||
|
||||
$company = Company::find($request->header('company'));
|
||||
|
||||
$invitation = $this->invitationService->invite(
|
||||
$company,
|
||||
$request->email,
|
||||
$request->role_id,
|
||||
$request->user()
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'invitation' => new CompanyInvitationResource($invitation->load(['company', 'role', 'invitedBy'])),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(CompanyInvitation $companyInvitation): JsonResponse
|
||||
{
|
||||
if ($companyInvitation->status !== CompanyInvitation::STATUS_PENDING) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Only pending invitations can be cancelled.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$companyInvitation->delete();
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Settings;
|
||||
namespace App\Http\Controllers\Company\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\TaxTypeRequest;
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Company\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\AvatarRequest;
|
||||
use App\Http\Requests\GetSettingsRequest;
|
||||
use App\Http\Requests\ProfileRequest;
|
||||
use App\Http\Requests\UpdateSettingsRequest;
|
||||
use App\Http\Resources\UserResource;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserProfileController extends Controller
|
||||
{
|
||||
public function show(Request $request)
|
||||
{
|
||||
return new UserResource($request->user());
|
||||
}
|
||||
|
||||
public function update(ProfileRequest $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$user->update($request->validated());
|
||||
|
||||
return new UserResource($user);
|
||||
}
|
||||
|
||||
public function uploadAvatar(AvatarRequest $request)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (isset($request->is_admin_avatar_removed) && (bool) $request->is_admin_avatar_removed) {
|
||||
$user->clearMediaCollection('admin_avatar');
|
||||
}
|
||||
if ($user && $request->hasFile('admin_avatar')) {
|
||||
$user->clearMediaCollection('admin_avatar');
|
||||
|
||||
$user->addMediaFromRequest('admin_avatar')
|
||||
->toMediaCollection('admin_avatar');
|
||||
}
|
||||
|
||||
if ($user && $request->has('avatar')) {
|
||||
$data = json_decode($request->avatar);
|
||||
$user->clearMediaCollection('admin_avatar');
|
||||
|
||||
$user->addMediaFromBase64($data->data)
|
||||
->usingFileName($data->name)
|
||||
->toMediaCollection('admin_avatar');
|
||||
}
|
||||
|
||||
return new UserResource($user);
|
||||
}
|
||||
|
||||
public function showSettings(GetSettingsRequest $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
return response()->json($user->getSettings((array) $request->settings));
|
||||
}
|
||||
|
||||
public function updateSettings(UpdateSettingsRequest $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$user->setSettings($request->settings);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer\Auth;
|
||||
namespace App\Http\Controllers\CustomerPortal\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer\Auth;
|
||||
namespace App\Http\Controllers\CustomerPortal\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Customer\CustomerLoginRequest;
|
||||
@@ -20,7 +20,7 @@ class LoginController extends Controller
|
||||
*/
|
||||
public function __invoke(CustomerLoginRequest $request, Company $company)
|
||||
{
|
||||
$user = Customer::where('email', $request->email)
|
||||
$user = Customer::whereRaw('LOWER(email) = ?', [strtolower($request->email)])
|
||||
->where('company_id', $company->id)
|
||||
->first();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer\Auth;
|
||||
namespace App\Http\Controllers\CustomerPortal\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer\Estimate;
|
||||
namespace App\Http\Controllers\CustomerPortal\Estimate;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Customer\EstimateResource;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer\Estimate;
|
||||
namespace App\Http\Controllers\CustomerPortal\Estimate;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Customer\EstimateResource;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer;
|
||||
namespace App\Http\Controllers\CustomerPortal;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\EstimateResource;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer\Expense;
|
||||
namespace App\Http\Controllers\CustomerPortal\Expense;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Customer\ExpenseResource;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer\General;
|
||||
namespace App\Http\Controllers\CustomerPortal\General;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Customer\CustomerResource;
|
||||
@@ -31,10 +31,13 @@ class BootstrapController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$companyCurrencyId = CompanySetting::getSetting('currency', $customer->company_id);
|
||||
|
||||
return (new CustomerResource($customer))
|
||||
->additional(['meta' => [
|
||||
'menu' => $menu,
|
||||
'current_customer_currency' => Currency::find($customer->currency_id),
|
||||
'current_company_currency' => $companyCurrencyId ? Currency::find($companyCurrencyId) : null,
|
||||
'modules' => Module::where('enabled', true)->pluck('name'),
|
||||
'current_company_language' => CompanySetting::getSetting('language', $customer->company_id),
|
||||
]]);
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer\General;
|
||||
namespace App\Http\Controllers\CustomerPortal\General;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Estimate;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer\General;
|
||||
namespace App\Http\Controllers\CustomerPortal\General;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Customer\CustomerProfileRequest;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer\Invoice;
|
||||
namespace App\Http\Controllers\CustomerPortal\Invoice;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Customer\InvoiceResource;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer;
|
||||
namespace App\Http\Controllers\CustomerPortal;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Customer\InvoiceResource as CustomerInvoiceResource;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer\Payment;
|
||||
namespace App\Http\Controllers\CustomerPortal\Payment;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Customer\PaymentMethodResource;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer\Payment;
|
||||
namespace App\Http\Controllers\CustomerPortal\Payment;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Customer\PaymentResource;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Customer;
|
||||
namespace App\Http\Controllers\CustomerPortal;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\PaymentResource;
|
||||
36
app/Http/Controllers/Modules/ScriptController.php
Normal file
36
app/Http/Controllers/Modules/ScriptController.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Modules;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use DateTime;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use InvoiceShelf\Modules\Registry as ModuleRegistry;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ScriptController extends Controller
|
||||
{
|
||||
/**
|
||||
* Serve the requested module-registered script.
|
||||
*
|
||||
* Modules call \InvoiceShelf\Modules\Registry::registerScript($name, $path)
|
||||
* from their ServiceProvider::boot() to inject custom JS into the host app.
|
||||
*
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
public function __invoke(Request $request, string $script): Response
|
||||
{
|
||||
$path = ModuleRegistry::scriptFor($script);
|
||||
|
||||
abort_if($path === null, 404);
|
||||
|
||||
return response(
|
||||
file_get_contents($path),
|
||||
200,
|
||||
[
|
||||
'Content-Type' => 'application/javascript',
|
||||
]
|
||||
)->setLastModified(DateTime::createFromFormat('U', (string) filemtime($path)));
|
||||
}
|
||||
}
|
||||
36
app/Http/Controllers/Modules/StyleController.php
Normal file
36
app/Http/Controllers/Modules/StyleController.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Modules;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use DateTime;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use InvoiceShelf\Modules\Registry as ModuleRegistry;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class StyleController extends Controller
|
||||
{
|
||||
/**
|
||||
* Serve the requested module-registered stylesheet.
|
||||
*
|
||||
* Modules call \InvoiceShelf\Modules\Registry::registerStyle($name, $path)
|
||||
* from their ServiceProvider::boot() to inject custom CSS into the host app.
|
||||
*
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
public function __invoke(Request $request, string $style): Response
|
||||
{
|
||||
$path = ModuleRegistry::styleFor($style);
|
||||
|
||||
abort_if($path === null, 404);
|
||||
|
||||
return response(
|
||||
file_get_contents($path),
|
||||
200,
|
||||
[
|
||||
'Content-Type' => 'text/css',
|
||||
]
|
||||
)->setLastModified(DateTime::createFromFormat('U', (string) filemtime($path)));
|
||||
}
|
||||
}
|
||||
46
app/Http/Controllers/Pdf/DocumentPdfController.php
Normal file
46
app/Http/Controllers/Pdf/DocumentPdfController.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Pdf;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Estimate;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Services\EstimateService;
|
||||
use App\Services\InvoiceService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DocumentPdfController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InvoiceService $invoiceService,
|
||||
private readonly EstimateService $estimateService,
|
||||
) {}
|
||||
|
||||
public function invoice(Request $request, Invoice $invoice)
|
||||
{
|
||||
if ($request->has('preview')) {
|
||||
return $this->invoiceService->getPdfData($invoice);
|
||||
}
|
||||
|
||||
return $invoice->getGeneratedPDFOrStream('invoice');
|
||||
}
|
||||
|
||||
public function estimate(Request $request, Estimate $estimate)
|
||||
{
|
||||
if ($request->has('preview')) {
|
||||
return $this->estimateService->getPdfData($estimate);
|
||||
}
|
||||
|
||||
return $estimate->getGeneratedPDFOrStream('estimate');
|
||||
}
|
||||
|
||||
public function payment(Request $request, Payment $payment)
|
||||
{
|
||||
if ($request->has('preview')) {
|
||||
return view('app.pdf.payment.payment');
|
||||
}
|
||||
|
||||
return $payment->getGeneratedPDFOrStream('payment');
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Installation;
|
||||
namespace App\Http\Controllers\Setup;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\DomainEnvironmentRequest;
|
||||
use App\Space\EnvironmentManager;
|
||||
use App\Services\Setup\EnvironmentManager;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class AppDomainController extends Controller
|
||||
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Installation;
|
||||
namespace App\Http\Controllers\Setup;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\DatabaseEnvironmentRequest;
|
||||
use App\Space\EnvironmentManager;
|
||||
use App\Space\InstallUtils;
|
||||
use App\Services\Setup\EnvironmentManager;
|
||||
use App\Services\Setup\InstallUtils;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
@@ -54,7 +54,7 @@ class DatabaseConfigurationController extends Controller
|
||||
case 'sqlite':
|
||||
$databaseData = [
|
||||
'database_connection' => 'sqlite',
|
||||
'database_name' => config('database.connections.sqlite.database', storage_path('database.sqlite')),
|
||||
'database_name' => config('database.connections.sqlite.database') ?: 'storage/app/database.sqlite',
|
||||
];
|
||||
|
||||
break;
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Installation;
|
||||
namespace App\Http\Controllers\Setup;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Space\FilePermissionChecker;
|
||||
use App\Services\Setup\FilePermissionChecker;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class FilePermissionsController extends Controller
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user