Skip to content

Commit 35b0d90

Browse files
committed
feat: Phase 32 — File Attachments for invoices, bills, expenses, and projects
https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent d756b71 commit 35b0d90

22 files changed

Lines changed: 568 additions & 5 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
namespace App\Modules\Finance\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Finance\Models\Attachment;
7+
use Illuminate\Http\RedirectResponse;
8+
use Illuminate\Http\Request;
9+
use Illuminate\Http\Response;
10+
use Illuminate\Support\Facades\Storage;
11+
12+
class AttachmentController extends Controller
13+
{
14+
private array $allowedModels = [
15+
'invoices' => \App\Modules\Finance\Models\Invoice::class,
16+
'bills' => \App\Modules\Finance\Models\Bill::class,
17+
'expense-claims' => \App\Modules\HR\Models\ExpenseClaim::class,
18+
'projects' => \App\Modules\Finance\Models\Project::class,
19+
];
20+
21+
public function store(Request $request, string $modelType, int $modelId): RedirectResponse
22+
{
23+
abort_unless(array_key_exists($modelType, $this->allowedModels), 404);
24+
25+
$this->authorize('create', Attachment::class);
26+
27+
$request->validate([
28+
'file' => 'required|file|max:20480|mimes:pdf,png,jpg,jpeg,webp,gif,csv,xlsx,docx,doc',
29+
]);
30+
31+
$modelClass = $this->allowedModels[$modelType];
32+
$model = $modelClass::findOrFail($modelId);
33+
34+
$file = $request->file('file');
35+
$path = $file->store("attachments/{$modelType}/{$modelId}", 'local');
36+
37+
Attachment::create([
38+
'tenant_id' => auth()->user()->tenant_id,
39+
'attachable_type' => $modelClass,
40+
'attachable_id' => $modelId,
41+
'filename' => $file->getClientOriginalName(),
42+
'disk' => 'local',
43+
'path' => $path,
44+
'mime_type' => $file->getMimeType(),
45+
'size' => $file->getSize(),
46+
'uploaded_by' => auth()->id(),
47+
]);
48+
49+
return back()->with('success', 'File attached.');
50+
}
51+
52+
public function download(Attachment $attachment): Response|\Symfony\Component\HttpFoundation\StreamedResponse
53+
{
54+
$this->authorize('view', $attachment);
55+
56+
abort_unless(Storage::disk($attachment->disk)->exists($attachment->path), 404);
57+
58+
return Storage::disk($attachment->disk)->download($attachment->path, $attachment->filename);
59+
}
60+
61+
public function destroy(Attachment $attachment): RedirectResponse
62+
{
63+
$this->authorize('delete', $attachment);
64+
65+
Storage::disk($attachment->disk)->delete($attachment->path);
66+
$attachment->delete();
67+
68+
return back()->with('success', 'Attachment deleted.');
69+
}
70+
}

erp/app/Modules/Finance/Http/Controllers/BillController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public function show(Bill $bill): Response
105105
{
106106
$this->authorize('view', $bill);
107107

108-
$bill->load(['contact', 'items', 'payments', 'creator']);
108+
$bill->load(['contact', 'items', 'payments', 'creator', 'attachments']);
109109

110110
return Inertia::render('Finance/Bills/Show', [
111111
'bill' => new BillResource($bill),

erp/app/Modules/Finance/Http/Controllers/InvoiceController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public function show(Invoice $invoice): Response
106106
{
107107
$this->authorize('view', $invoice);
108108

109-
$invoice->load(['contact', 'items', 'payments', 'creator']);
109+
$invoice->load(['contact', 'items', 'payments', 'creator', 'attachments']);
110110

111111
return Inertia::render('Finance/Invoices/Show', [
112112
'invoice' => new InvoiceResource($invoice),
@@ -175,7 +175,7 @@ public function print(Invoice $invoice): Response
175175
{
176176
$this->authorize('view', $invoice);
177177

178-
$invoice->load(['contact', 'items', 'payments', 'creator']);
178+
$invoice->load(['contact', 'items', 'payments', 'creator', 'attachments']);
179179

180180
$tenantId = auth()->user()->tenant_id;
181181

erp/app/Modules/Finance/Http/Controllers/ProjectController.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public function show(Project $project): Response
8787
{
8888
$this->authorize('view', $project);
8989

90-
$project->load(['timeEntries.user', 'contact', 'invoice']);
90+
$project->load(['timeEntries.user', 'contact', 'invoice', 'attachments']);
9191

9292
return Inertia::render('Finance/Projects/Show', [
9393
'project' => [
@@ -104,6 +104,11 @@ public function show(Project $project): Response
104104
'invoice' => $project->invoice ? ['id' => $project->invoice->id, 'reference' => $project->invoice->number ?? '#' . $project->invoice->id] : null,
105105
'total_hours' => $project->total_hours,
106106
'billable_hours' => $project->billable_hours,
107+
'attachments' => $project->attachments->map(fn ($a) => [
108+
'id' => $a->id, 'filename' => $a->filename, 'disk' => $a->disk,
109+
'path' => $a->path, 'mime_type' => $a->mime_type, 'size' => $a->size,
110+
'uploaded_by' => $a->uploaded_by, 'created_at' => $a->created_at?->toIso8601String(),
111+
]),
107112
'time_entries' => $project->timeEntries->map(fn ($e) => [
108113
'id' => $e->id,
109114
'project_id' => $e->project_id,

erp/app/Modules/Finance/Http/Resources/BillResource.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public function toArray(Request $request): array
4949
fn () => $this->amount_due
5050
),
5151
'transitions' => $this->availableTransitions(),
52+
'attachments' => $this->whenLoaded('attachments', fn () => $this->attachments->map(fn ($a) => [
53+
'id' => $a->id, 'filename' => $a->filename, 'disk' => $a->disk,
54+
'path' => $a->path, 'mime_type' => $a->mime_type, 'size' => $a->size,
55+
'uploaded_by' => $a->uploaded_by, 'created_at' => $a->created_at?->toIso8601String(),
56+
])),
5257
'creator' => $this->whenLoaded('creator', fn () => $this->creator?->name),
5358
'created_at' => $this->created_at?->toDateString(),
5459
];

erp/app/Modules/Finance/Http/Resources/InvoiceResource.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public function toArray(Request $request): array
4949
fn () => $this->amount_due
5050
),
5151
'transitions' => $this->availableTransitions(),
52+
'attachments' => $this->whenLoaded('attachments', fn () => $this->attachments->map(fn ($a) => [
53+
'id' => $a->id, 'filename' => $a->filename, 'disk' => $a->disk,
54+
'path' => $a->path, 'mime_type' => $a->mime_type, 'size' => $a->size,
55+
'uploaded_by' => $a->uploaded_by, 'created_at' => $a->created_at?->toIso8601String(),
56+
])),
5257
'creator' => $this->whenLoaded('creator', fn () => $this->creator?->name),
5358
'created_at' => $this->created_at?->toDateString(),
5459
];
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace App\Modules\Finance\Models;
4+
5+
use App\Modules\Core\Traits\BelongsToTenant;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
8+
use Illuminate\Database\Eloquent\Relations\MorphTo;
9+
10+
class Attachment extends Model
11+
{
12+
use BelongsToTenant;
13+
14+
protected $fillable = [
15+
'tenant_id', 'attachable_type', 'attachable_id',
16+
'filename', 'disk', 'path', 'mime_type', 'size', 'uploaded_by',
17+
];
18+
19+
public function attachable(): MorphTo
20+
{
21+
return $this->morphTo();
22+
}
23+
24+
public function uploader(): BelongsTo
25+
{
26+
return $this->belongsTo(\App\Models\User::class, 'uploaded_by');
27+
}
28+
}

erp/app/Modules/Finance/Models/Bill.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use App\Modules\Core\Traits\BelongsToTenant;
77
use App\Modules\Core\Traits\HasAuditLog;
88
use App\Modules\Finance\Traits\HasLineItemTotals;
9+
use App\Modules\Finance\Traits\HasAttachments;
910
use App\Modules\Finance\Traits\HasStatusTransitions;
1011
use Illuminate\Database\Eloquent\Model;
1112
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -18,6 +19,7 @@ class Bill extends Model
1819
use HasAuditLog;
1920
use SoftDeletes;
2021
use HasLineItemTotals;
22+
use HasAttachments;
2123
use HasStatusTransitions;
2224

2325
protected $fillable = [

erp/app/Modules/Finance/Models/Invoice.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use App\Modules\Core\Traits\BelongsToTenant;
77
use App\Modules\Core\Traits\HasAuditLog;
88
use App\Modules\Finance\Traits\HasLineItemTotals;
9+
use App\Modules\Finance\Traits\HasAttachments;
910
use App\Modules\Finance\Traits\HasStatusTransitions;
1011
use Illuminate\Database\Eloquent\Model;
1112
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -18,6 +19,7 @@ class Invoice extends Model
1819
use HasAuditLog;
1920
use SoftDeletes;
2021
use HasLineItemTotals;
22+
use HasAttachments;
2123
use HasStatusTransitions;
2224

2325
protected $fillable = [

erp/app/Modules/Finance/Models/Project.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Modules\Finance\Models;
44

55
use App\Modules\Core\Traits\BelongsToTenant;
6+
use App\Modules\Finance\Traits\HasAttachments;
67
use Illuminate\Database\Eloquent\Builder;
78
use Illuminate\Database\Eloquent\Model;
89
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -12,6 +13,7 @@
1213
class Project extends Model
1314
{
1415
use BelongsToTenant;
16+
use HasAttachments;
1517
use SoftDeletes;
1618

1719
protected $fillable = [

0 commit comments

Comments
 (0)