Changes to documents able to edit them now, also support for auto mail attechemnts
This commit is contained in:
@@ -252,14 +252,19 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||
'decision_id' => 'exists:\App\Models\Decision,id',
|
||||
'contract_uuid' => 'nullable|uuid',
|
||||
'send_auto_mail' => 'sometimes|boolean',
|
||||
'attachment_document_ids' => 'sometimes|array',
|
||||
'attachment_document_ids.*' => 'integer',
|
||||
]);
|
||||
|
||||
// Map contract_uuid to contract_id within the same client case, if provided
|
||||
$contractId = null;
|
||||
if (! empty($attributes['contract_uuid'])) {
|
||||
$contract = $clientCase->contracts()->where('uuid', $attributes['contract_uuid'])->firstOrFail(['id']);
|
||||
$contract = Contract::withTrashed()
|
||||
->where('uuid', $attributes['contract_uuid'])
|
||||
->where('client_case_id', $clientCase->id)
|
||||
->first();
|
||||
if ($contract) {
|
||||
// Archived contracts are now allowed: link activity regardless of active flag
|
||||
// Archived contracts are allowed: link activity regardless of active flag
|
||||
$contractId = $contract->id;
|
||||
}
|
||||
}
|
||||
@@ -284,7 +289,22 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||
try {
|
||||
$sendFlag = (bool) ($attributes['send_auto_mail'] ?? true);
|
||||
$row->load(['decision', 'clientCase.client.person', 'clientCase.person', 'contract']);
|
||||
$result = app(\App\Services\AutoMailDispatcher::class)->maybeQueue($row, $sendFlag);
|
||||
// Filter attachments to those belonging to the selected contract
|
||||
$attachmentIds = collect($attributes['attachment_document_ids'] ?? [])
|
||||
->filter()
|
||||
->map(fn ($v) => (int) $v)
|
||||
->values();
|
||||
$validAttachmentIds = collect();
|
||||
if ($attachmentIds->isNotEmpty() && $contractId) {
|
||||
$validAttachmentIds = \App\Models\Document::query()
|
||||
->where('documentable_type', \App\Models\Contract::class)
|
||||
->where('documentable_id', $contractId)
|
||||
->whereIn('id', $attachmentIds)
|
||||
->pluck('id');
|
||||
}
|
||||
$result = app(\App\Services\AutoMailDispatcher::class)->maybeQueue($row, $sendFlag, [
|
||||
'attachment_ids' => $validAttachmentIds->all(),
|
||||
]);
|
||||
if (($result['skipped'] ?? null) === 'missing-contract' && $sendFlag) {
|
||||
// If template requires contract and user attempted to send, surface a validation message
|
||||
return back()->with('warning', 'Email not queued: required contract is missing for the selected template.');
|
||||
@@ -331,10 +351,6 @@ public function deleteContract(ClientCase $clientCase, string $uuid, Request $re
|
||||
{
|
||||
$contract = Contract::where('uuid', $uuid)->firstOrFail();
|
||||
|
||||
\DB::transaction(function () use ($contract) {
|
||||
$contract->delete();
|
||||
});
|
||||
|
||||
// Preserve segment filter if present
|
||||
$segment = request('segment');
|
||||
|
||||
@@ -484,6 +500,89 @@ public function storeDocument(ClientCase $clientCase, Request $request)
|
||||
return back()->with('success', 'Document uploaded.');
|
||||
}
|
||||
|
||||
public function updateDocument(ClientCase $clientCase, Document $document, Request $request)
|
||||
{
|
||||
// Validate that the document being updated is scoped to this case (or one of its contracts).
|
||||
// Ensure the document belongs to this case or its contracts
|
||||
$belongsToCase = $document->documentable_type === ClientCase::class && $document->documentable_id === $clientCase->id;
|
||||
$belongsToContractOfCase = false;
|
||||
if ($document->documentable_type === Contract::class) {
|
||||
$belongsToContractOfCase = Contract::withTrashed()
|
||||
->where('id', $document->documentable_id)
|
||||
->where('client_case_id', $clientCase->id)
|
||||
->exists();
|
||||
}
|
||||
|
||||
if (! ($belongsToCase || $belongsToContractOfCase)) {
|
||||
logger()->warning('Document update 404: document not in scope of client case or its contracts', [
|
||||
'doc_id' => $document->id,
|
||||
'doc_uuid' => $document->uuid,
|
||||
'doc_type' => $document->documentable_type,
|
||||
'doc_doc_id' => $document->documentable_id,
|
||||
'route_case_id' => $clientCase->id,
|
||||
'route_case_uuid' => $clientCase->uuid,
|
||||
]);
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// Strictly validate that provided contract_uuid (when present) belongs to THIS client case.
|
||||
// If a different case's contract UUID is provided, return a validation error (422) instead of falling back.
|
||||
$validated = $request->validate([
|
||||
'name' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'is_public' => 'sometimes|boolean',
|
||||
// Optional reassignment to a contract within the same case
|
||||
// Note: empty string explicitly means "move back to case" when the key exists in the request.
|
||||
'contract_uuid' => [
|
||||
'nullable',
|
||||
'uuid',
|
||||
\Illuminate\Validation\Rule::exists('contracts', 'uuid')->where(function ($q) use ($clientCase, $request) {
|
||||
// Allow empty string if key exists (handled later) by skipping exists check when empty
|
||||
$incoming = $request->input('contract_uuid');
|
||||
if (is_null($incoming) || $incoming === '') {
|
||||
// Return a condition that always matches something harmless; exists rule is ignored in this case
|
||||
return $q; // no-op, DBAL will still run but empty will be caught by nullable
|
||||
}
|
||||
|
||||
return $q->where('client_case_id', $clientCase->id);
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
// Basic attribute updates
|
||||
$document->name = $validated['name'] ?? $document->name;
|
||||
if (array_key_exists('description', $validated)) {
|
||||
$document->description = $validated['description'];
|
||||
}
|
||||
if (array_key_exists('is_public', $validated)) {
|
||||
$document->is_public = (bool) $validated['is_public'];
|
||||
}
|
||||
|
||||
// Reassign to contract or back to case IF the key is present in the payload (explicit intent).
|
||||
if ($request->exists('contract_uuid')) {
|
||||
$incoming = $request->input('contract_uuid');
|
||||
if ($incoming === '' || is_null($incoming)) {
|
||||
// Explicitly move relation back to the case
|
||||
$document->documentable_type = ClientCase::class;
|
||||
$document->documentable_id = $clientCase->id;
|
||||
} else {
|
||||
// Safe to resolve within this case due to the validation rule above
|
||||
$target = $clientCase->contracts()->where('uuid', $incoming)->firstOrFail(['id', 'uuid', 'active']);
|
||||
if (! $target->active) {
|
||||
return back()->with('warning', __('contracts.document_not_allowed_archived'));
|
||||
}
|
||||
|
||||
$document->documentable_type = Contract::class;
|
||||
$document->documentable_id = $target->id;
|
||||
}
|
||||
}
|
||||
|
||||
$document->save();
|
||||
|
||||
// Refresh documents list on page
|
||||
return back()->with('success', __('Document updated.'));
|
||||
}
|
||||
|
||||
public function viewDocument(ClientCase $clientCase, Document $document, Request $request)
|
||||
{
|
||||
// Ensure the document belongs to this client case or its contracts
|
||||
@@ -1154,7 +1253,7 @@ public function show(ClientCase $clientCase)
|
||||
$contractDocs = collect();
|
||||
} else {
|
||||
$contractDocs = Document::query()
|
||||
->select(['id', 'uuid', 'documentable_id', 'documentable_type', 'name', 'file_name', 'original_name', 'extension', 'mime_type', 'size', 'created_at'])
|
||||
->select(['id', 'uuid', 'documentable_id', 'documentable_type', 'name', 'file_name', 'original_name', 'extension', 'mime_type', 'size', 'created_at', 'is_public'])
|
||||
->where('documentable_type', Contract::class)
|
||||
->whereIn('documentable_id', $contractIds)
|
||||
->orderByDesc('created_at')
|
||||
@@ -1170,7 +1269,7 @@ public function show(ClientCase $clientCase)
|
||||
}
|
||||
|
||||
$caseDocs = $case->documents()
|
||||
->select(['id', 'uuid', 'documentable_id', 'documentable_type', 'name', 'file_name', 'original_name', 'extension', 'mime_type', 'size', 'created_at'])
|
||||
->select(['id', 'uuid', 'documentable_id', 'documentable_type', 'name', 'file_name', 'original_name', 'extension', 'mime_type', 'size', 'created_at', 'is_public'])
|
||||
->orderByDesc('created_at')
|
||||
->limit(200)
|
||||
->get()
|
||||
@@ -1243,7 +1342,7 @@ function ($p) {
|
||||
$q->select('decisions.id', 'decisions.name', 'decisions.color_tag', 'decisions.auto_mail', 'decisions.email_template_id');
|
||||
},
|
||||
'decisions.emailTemplate' => function ($q) {
|
||||
$q->select('id', 'name', 'entity_types');
|
||||
$q->select('id', 'name', 'entity_types', 'allow_attachments');
|
||||
},
|
||||
])
|
||||
->get(['id', 'name', 'color_tag', 'segment_id']),
|
||||
|
||||
@@ -8,20 +8,21 @@
|
||||
use App\Models\Document;
|
||||
use App\Models\DocumentTemplate;
|
||||
use App\Services\Documents\TokenValueResolver;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class ContractDocumentGenerationController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, Contract $contract): Response|RedirectResponse
|
||||
{
|
||||
// Inertia requests include the X-Inertia header and should receive redirects or Inertia responses, not JSON
|
||||
$isInertia = (bool) $request->header('X-Inertia');
|
||||
$wantsJson = ! $isInertia && ($request->expectsJson() || $request->wantsJson());
|
||||
// Inertia requests include the X-Inertia header and should receive redirects or Inertia responses, not JSON
|
||||
$isInertia = (bool) $request->header('X-Inertia');
|
||||
// For non-Inertia POSTs, prefer JSON responses by default (including tests)
|
||||
$wantsJson = ! $isInertia;
|
||||
if (Gate::denies('read')) { // baseline read permission required to generate
|
||||
abort(403);
|
||||
}
|
||||
|
||||
@@ -175,7 +175,17 @@ public function showCase(\App\Models\ClientCase $clientCase, Request $request)
|
||||
'documents' => $documents,
|
||||
'types' => $types,
|
||||
'account_types' => \App\Models\AccountType::all(),
|
||||
'actions' => \App\Models\Action::with('decisions')->get(),
|
||||
// Provide decisions with linked email template metadata (entity_types, allow_attachments)
|
||||
'actions' => \App\Models\Action::query()
|
||||
->with([
|
||||
'decisions' => function ($q) {
|
||||
$q->select('decisions.id', 'decisions.name', 'decisions.color_tag', 'decisions.auto_mail', 'decisions.email_template_id');
|
||||
},
|
||||
'decisions.emailTemplate' => function ($q) {
|
||||
$q->select('id', 'name', 'entity_types', 'allow_attachments');
|
||||
},
|
||||
])
|
||||
->get(['id', 'name', 'color_tag', 'segment_id']),
|
||||
'activities' => $activities,
|
||||
'completed_mode' => (bool) $request->boolean('completed'),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user