Skip to content

Commit f7f2eb9

Browse files
committed
feat: Phase 3 Frontend Polish — TipTap editor, DomPDF payslips, drag-and-drop uploads, DataGrid — 18 tests passing
- RichTextEditor (TipTap): StarterKit (H1-H3, bold, italic, lists), Placeholder, CharacterCount; toolbar uses onMouseDown to preserve focus; integrated into KB Show and CRM Email Sequences step body - FileDropzone: drag-and-drop + click-to-browse, file size validation, image previews, multi-file support; used in Documents/Upload page - DataGrid: client-side sorting, checkbox selection, pagination, custom renderers; used in HR Payslips index - PayslipController: index, show, pdf (DomPDF inline stream), downloadAll; Blade template with earnings/deductions two-column layout - Documents upload: uploadPage (Inertia) + upload (multipart POST → JSON) controller methods; stores to public disk under documents/{tenant}/{year}/{month}/ - 8 payslip PDF tests: content-type, filename, body size, auth guard, inertia renders - 10 document upload tests: storage, folder assignment, validation, auth guard, response shape Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b51085a commit f7f2eb9

17 files changed

Lines changed: 2046 additions & 13 deletions

File tree

erp/app/Modules/Documents/Http/Controllers/DocumentController.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
use App\Http\Controllers\Controller;
66
use App\Modules\Documents\Models\Document;
77
use App\Modules\Documents\Models\DocumentFolder;
8+
use Illuminate\Http\JsonResponse;
89
use Illuminate\Http\RedirectResponse;
910
use Illuminate\Http\Request;
11+
use Illuminate\Support\Str;
1012
use Inertia\Inertia;
1113
use Inertia\Response;
1214

@@ -105,6 +107,49 @@ public function store(Request $request): RedirectResponse
105107
return redirect()->route('documents.show', $document)->with('success', 'Document uploaded.');
106108
}
107109

110+
public function uploadPage(): Response
111+
{
112+
$folders = DocumentFolder::orderBy('name')->get(['id', 'name']);
113+
114+
return Inertia::render('Documents/Upload', [
115+
'folders' => $folders,
116+
]);
117+
}
118+
119+
public function upload(Request $request): JsonResponse
120+
{
121+
$request->validate([
122+
'file' => 'required|file|max:51200',
123+
'folder_id' => 'nullable|exists:document_folders,id',
124+
]);
125+
126+
$file = $request->file('file');
127+
$tenant = auth()->user()->tenant_id;
128+
$directory = "documents/{$tenant}/" . date('Y/m');
129+
$fileName = Str::uuid() . '.' . $file->getClientOriginalExtension();
130+
$path = $file->storeAs($directory, $fileName, 'public');
131+
132+
$document = Document::create([
133+
'tenant_id' => $tenant,
134+
'title' => pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME),
135+
'folder_id' => $request->folder_id,
136+
'file_path' => $path,
137+
'file_name' => $file->getClientOriginalName(),
138+
'file_size' => $file->getSize(),
139+
'mime_type' => $file->getMimeType(),
140+
'version' => 1,
141+
'uploaded_by' => auth()->id(),
142+
]);
143+
144+
return response()->json([
145+
'id' => $document->id,
146+
'title' => $document->title,
147+
'file_name' => $document->file_name,
148+
'file_size' => $document->file_size,
149+
'url' => route('documents.show', $document),
150+
]);
151+
}
152+
108153
public function update(Request $request, Document $document): RedirectResponse
109154
{
110155
$validated = $request->validate([

erp/app/Modules/Documents/routes/documents.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
Route::delete('folders/{folder}', [DocumentController::class, 'destroyFolder'])->name('folders.destroy');
99
Route::get('search', [DocumentController::class, 'search'])->name('search');
1010
Route::post('{document}/versions', [DocumentController::class, 'addVersion'])->name('versions.store');
11+
Route::get('upload', [DocumentController::class, 'uploadPage'])->name('upload.page');
12+
Route::post('upload', [DocumentController::class, 'upload'])->name('upload');
1113
Route::get('', [DocumentController::class, 'index'])->name('index');
1214
Route::post('', [DocumentController::class, 'store'])->name('store');
1315
Route::get('{document}', [DocumentController::class, 'show'])->name('show');
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace App\Modules\HR\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\HR\Models\Payslip;
7+
use App\Modules\HR\Models\PayrollRun;
8+
use Barryvdh\DomPDF\Facade\Pdf;
9+
use Illuminate\Http\Response;
10+
use Inertia\Inertia;
11+
12+
class PayslipController extends Controller
13+
{
14+
public function index(PayrollRun $payrollRun): \Inertia\Response
15+
{
16+
$payslips = $payrollRun->payslips()
17+
->with(['employee.department', 'lines'])
18+
->get();
19+
20+
return Inertia::render('HR/Payslips/Index', [
21+
'payrollRun' => $payrollRun,
22+
'payslips' => $payslips,
23+
]);
24+
}
25+
26+
public function show(Payslip $payslip): \Inertia\Response
27+
{
28+
$payslip->load(['employee.department', 'payrollRun', 'lines']);
29+
30+
return Inertia::render('HR/Payslips/Show', [
31+
'payslip' => $payslip,
32+
]);
33+
}
34+
35+
public function pdf(Payslip $payslip): Response
36+
{
37+
$payslip->load(['employee.department', 'payrollRun', 'lines']);
38+
39+
$company = $this->resolveCompanyName();
40+
41+
$pdf = Pdf::loadView('pdf.payslip', [
42+
'payslip' => $payslip,
43+
'company' => $company,
44+
]);
45+
46+
$employee = $payslip->employee;
47+
$name = $employee ? strtolower($employee->last_name . '-' . $employee->first_name) : 'employee';
48+
$filename = "payslip-{$name}-{$payslip->id}.pdf";
49+
50+
return response($pdf->output(), 200, [
51+
'Content-Type' => 'application/pdf',
52+
'Content-Disposition' => "inline; filename=\"{$filename}\"",
53+
]);
54+
}
55+
56+
public function downloadAll(PayrollRun $payrollRun): Response
57+
{
58+
$payrollRun->load(['payslips.employee', 'payslips.lines']);
59+
$company = $this->resolveCompanyName();
60+
61+
$pdf = Pdf::loadView('pdf.payroll-run-payslips', [
62+
'payrollRun' => $payrollRun,
63+
'company' => $company,
64+
]);
65+
66+
$filename = "payslips-{$payrollRun->id}.pdf";
67+
68+
return response($pdf->output(), 200, [
69+
'Content-Type' => 'application/pdf',
70+
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
71+
]);
72+
}
73+
74+
private function resolveCompanyName(): string
75+
{
76+
try {
77+
return app('tenant')->name;
78+
} catch (\Throwable) {
79+
return config('app.name', 'ERP');
80+
}
81+
}
82+
}

erp/app/Modules/HR/routes/hr.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use App\Modules\HR\Http\Controllers\OnboardingTemplateController;
2222
use App\Modules\HR\Http\Controllers\PayrollController;
2323
use App\Modules\HR\Http\Controllers\PayrollRunController;
24+
use App\Modules\HR\Http\Controllers\PayslipController;
2425
use App\Modules\HR\Http\Controllers\PerformanceReviewController;
2526
use App\Modules\HR\Http\Controllers\TrainingCourseController;
2627
use App\Modules\HR\Http\Controllers\TrainingEnrollmentController;
@@ -82,6 +83,11 @@
8283
->name('payroll-runs.process');
8384
Route::resource('payroll-runs', PayrollRunController::class)->except(['edit', 'update']);
8485

86+
// Payslips — PDF download
87+
Route::get('payslips/{payslip}/pdf', [PayslipController::class, 'pdf'])->name('payslips.pdf');
88+
Route::get('payroll-runs/{payrollRun}/payslips', [PayslipController::class, 'index'])->name('payroll-runs.payslips.index');
89+
Route::get('payslips/{payslip}', [PayslipController::class, 'show'])->name('payslips.show');
90+
8591
// Payroll — custom actions BEFORE resource
8692
Route::post('payroll/{payrollRun}/generate', [PayrollController::class, 'generate'])->name('payroll.generate');
8793
Route::post('payroll/{payrollRun}/approve', [PayrollController::class, 'approve'])->name('payroll.approve');

0 commit comments

Comments
 (0)