Changes to import and notifications
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Activity;
|
||||
use App\Models\ActivityNotificationRead;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ActivityNotificationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'activity_id' => ['required', 'integer', 'exists:activities,id'],
|
||||
]);
|
||||
|
||||
$userId = optional($request->user())->id;
|
||||
if (! $userId) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$activity = Activity::query()->select(['id', 'due_date'])->findOrFail($request->integer('activity_id'));
|
||||
$due = optional($activity->due_date) ? date('Y-m-d', strtotime($activity->due_date)) : now()->toDateString();
|
||||
|
||||
ActivityNotificationRead::query()->updateOrCreate(
|
||||
[
|
||||
'user_id' => $userId,
|
||||
'activity_id' => $activity->id,
|
||||
'due_date' => $due,
|
||||
],
|
||||
[
|
||||
'read_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
return response()->json(['status' => 'ok']);
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ public function index(ClientCase $clientCase, Request $request)
|
||||
|
||||
return Inertia::render('Cases/Index', [
|
||||
'client_cases' => $query
|
||||
->paginate(15, ['*'], 'client-cases-page')
|
||||
->paginate($request->integer('perPage', 15), ['*'], 'client-cases-page')
|
||||
->withQueryString(),
|
||||
'filters' => $request->only(['search']),
|
||||
]);
|
||||
|
||||
@@ -53,7 +53,7 @@ public function index(Client $client, Request $request)
|
||||
|
||||
return Inertia::render('Client/Index', [
|
||||
'clients' => $query
|
||||
->paginate(15)
|
||||
->paginate($request->integer('perPage', 15))
|
||||
->withQueryString(),
|
||||
'filters' => $request->only(['search']),
|
||||
]);
|
||||
@@ -104,7 +104,7 @@ public function show(Client $client, Request $request)
|
||||
])
|
||||
->where('active', 1)
|
||||
->orderByDesc('created_at')
|
||||
->paginate(15)
|
||||
->paginate($request->integer('perPage', 15))
|
||||
->withQueryString(),
|
||||
'types' => $types,
|
||||
'filters' => $request->only(['search']),
|
||||
@@ -158,7 +158,7 @@ public function contracts(Client $client, Request $request)
|
||||
|
||||
return Inertia::render('Client/Contracts', [
|
||||
'client' => $data,
|
||||
'contracts' => $contractsQuery->paginate(20)->withQueryString(),
|
||||
'contracts' => $contractsQuery->paginate($request->integer('perPage', 20))->withQueryString(),
|
||||
'filters' => $request->only(['from', 'to', 'search']),
|
||||
'types' => $types,
|
||||
]);
|
||||
|
||||
@@ -146,6 +146,7 @@ public function store(Request $request)
|
||||
'size' => $file->getSize(),
|
||||
'sheet_name' => $validated['sheet_name'] ?? null,
|
||||
'status' => 'uploaded',
|
||||
'show_missing' => false,
|
||||
'meta' => [
|
||||
'has_header' => $validated['has_header'] ?? true,
|
||||
],
|
||||
@@ -155,6 +156,7 @@ public function store(Request $request)
|
||||
'id' => $import->id,
|
||||
'uuid' => $import->uuid,
|
||||
'status' => $import->status,
|
||||
'show_missing' => (bool) ($import->show_missing ?? false),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -354,6 +356,116 @@ public function getMappings(Import $import)
|
||||
return response()->json(['mappings' => $rows]);
|
||||
}
|
||||
|
||||
/**
|
||||
* List active, non-archived contracts for the import's client that are NOT present
|
||||
* in the processed import file (based on mapped contract.reference values).
|
||||
* Only available when contract.reference mapping apply_mode is 'keyref'.
|
||||
*/
|
||||
public function missingContracts(Import $import)
|
||||
{
|
||||
// Ensure client context is available
|
||||
if (empty($import->client_id)) {
|
||||
return response()->json(['error' => 'Import has no client bound.'], 422);
|
||||
}
|
||||
|
||||
// Respect optional feature flag on import
|
||||
if (! (bool) ($import->show_missing ?? false)) {
|
||||
return response()->json(['error' => 'Missing contracts listing is disabled for this import.'], 422);
|
||||
}
|
||||
|
||||
// Check that this import's mappings set contract.reference to keyref mode
|
||||
$mappings = \DB::table('import_mappings')
|
||||
->where('import_id', $import->id)
|
||||
->get(['target_field', 'apply_mode']);
|
||||
$isKeyref = false;
|
||||
foreach ($mappings as $map) {
|
||||
$tf = strtolower((string) ($map->target_field ?? ''));
|
||||
$am = strtolower((string) ($map->apply_mode ?? ''));
|
||||
if (in_array($tf, ['contract.reference', 'contracts.reference'], true) && $am === 'keyref') {
|
||||
$isKeyref = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (! $isKeyref) {
|
||||
return response()->json(['error' => 'Missing contracts are only available for keyref mapping on contract.reference.'], 422);
|
||||
}
|
||||
|
||||
// Collect referenced contract references from processed rows
|
||||
$present = [];
|
||||
foreach (\App\Models\ImportRow::query()->where('import_id', $import->id)->get(['mapped_data']) as $row) {
|
||||
$md = $row->mapped_data ?? [];
|
||||
if (is_array($md) && isset($md['contract']['reference'])) {
|
||||
$ref = (string) $md['contract']['reference'];
|
||||
if ($ref !== '') {
|
||||
$present[] = preg_replace('/\s+/', '', trim($ref));
|
||||
}
|
||||
}
|
||||
}
|
||||
$present = array_values(array_unique(array_filter($present)));
|
||||
|
||||
// Query active, non-archived contracts for this client that were not in import
|
||||
// Include person full_name (owner of the client case) and aggregate active accounts' balance_amount
|
||||
$contractsQ = \App\Models\Contract::query()
|
||||
->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id')
|
||||
->join('person', 'person.id', '=', 'client_cases.person_id')
|
||||
->leftJoin('accounts', function ($join) {
|
||||
$join->on('accounts.contract_id', '=', 'contracts.id')
|
||||
->where('accounts.active', 1);
|
||||
})
|
||||
->where('client_cases.client_id', $import->client_id)
|
||||
->where('contracts.active', 1)
|
||||
->whereNull('contracts.deleted_at')
|
||||
->when(count($present) > 0, function ($q) use ($present) {
|
||||
$q->whereNotIn('contracts.reference', $present);
|
||||
})
|
||||
->groupBy('contracts.uuid', 'contracts.reference', 'client_cases.uuid', 'person.full_name')
|
||||
->orderBy('contracts.reference')
|
||||
->get([
|
||||
'contracts.uuid as uuid',
|
||||
'contracts.reference as reference',
|
||||
'client_cases.uuid as case_uuid',
|
||||
'person.full_name as full_name',
|
||||
\DB::raw('COALESCE(SUM(accounts.balance_amount), 0) as balance_amount'),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'missing' => $contractsQ,
|
||||
'count' => $contractsQ->count(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update import options (e.g., booleans like show_missing, reactivate) from the UI.
|
||||
*/
|
||||
public function updateOptions(Request $request, Import $import)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'show_missing' => 'nullable|boolean',
|
||||
'reactivate' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$payload = [];
|
||||
if (array_key_exists('show_missing', $data)) {
|
||||
$payload['show_missing'] = (bool) $data['show_missing'];
|
||||
}
|
||||
if (array_key_exists('reactivate', $data)) {
|
||||
$payload['reactivate'] = (bool) $data['reactivate'];
|
||||
}
|
||||
if (! empty($payload)) {
|
||||
$import->update($payload);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'import' => [
|
||||
'id' => $import->id,
|
||||
'uuid' => $import->uuid,
|
||||
'show_missing' => (bool) ($import->show_missing ?? false),
|
||||
'reactivate' => (bool) ($import->reactivate ?? false),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// Fetch recent import events (logs) for an import
|
||||
public function getEvents(Import $import)
|
||||
{
|
||||
@@ -533,6 +645,8 @@ public function show(Import $import)
|
||||
'client_id' => $import->client_id,
|
||||
'client_uuid' => optional($client)->uuid,
|
||||
'import_template_id' => $import->import_template_id,
|
||||
'show_missing' => (bool) ($import->show_missing ?? false),
|
||||
'reactivate' => (bool) ($import->reactivate ?? false),
|
||||
'total_rows' => $import->total_rows,
|
||||
'imported_rows' => $import->imported_rows,
|
||||
'invalid_rows' => $import->invalid_rows,
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Activity;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
public function unread(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
if (! $user) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$today = now()->toDateString();
|
||||
$perPage = max(1, min(100, (int) $request->integer('perPage', 15)));
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
|
||||
$query = Activity::query()
|
||||
->select(['id', 'due_date', 'amount', 'contract_id', 'client_case_id', 'created_at'])
|
||||
->whereNotNull('due_date')
|
||||
->whereDate('due_date', '<=', $today)
|
||||
->whereNotExists(function ($q) use ($user) {
|
||||
$q->from('activity_notification_reads as anr')
|
||||
->whereColumn('anr.activity_id', 'activities.id')
|
||||
->where('anr.user_id', $user->id)
|
||||
->whereColumn('anr.due_date', 'activities.due_date');
|
||||
})
|
||||
// allow simple search by contract reference or person name
|
||||
->when($search !== '', function ($q) use ($search) {
|
||||
$s = mb_strtolower($search);
|
||||
$q->leftJoin('contracts', 'contracts.id', '=', 'activities.contract_id')
|
||||
->leftJoin('client_cases', 'client_cases.id', '=', 'activities.client_case_id')
|
||||
->leftJoin('person', 'person.id', '=', 'client_cases.person_id')
|
||||
->where(function ($qq) use ($s) {
|
||||
$qq->whereRaw('LOWER(COALESCE(contracts.reference, \'\')) LIKE ?', ['%'.$s.'%'])
|
||||
->orWhereRaw('LOWER(COALESCE(person.full_name, \'\')) LIKE ?', ['%'.$s.'%']);
|
||||
});
|
||||
})
|
||||
->with([
|
||||
'contract' => function ($q) {
|
||||
$q->select(['contracts.id', 'contracts.uuid', 'contracts.reference', 'contracts.client_case_id'])
|
||||
->with([
|
||||
'clientCase' => function ($qq) {
|
||||
$qq->select(['client_cases.id', 'client_cases.uuid']);
|
||||
},
|
||||
'account' => function ($qq) {
|
||||
$qq->select(['accounts.id', 'accounts.contract_id', 'accounts.balance_amount', 'accounts.initial_amount']);
|
||||
},
|
||||
]);
|
||||
},
|
||||
'clientCase' => function ($q) {
|
||||
$q->select(['client_cases.id', 'client_cases.uuid', 'client_cases.person_id'])
|
||||
->with([
|
||||
'person' => function ($qq) {
|
||||
$qq->select(['person.id', 'person.full_name']);
|
||||
},
|
||||
]);
|
||||
},
|
||||
])
|
||||
// force ordering by due_date DESC only
|
||||
->orderByDesc('activities.due_date');
|
||||
|
||||
// Use a custom page parameter name to match the frontend DataTableServer
|
||||
$activities = $query->paginate($perPage, ['*'], 'unread-page')->withQueryString();
|
||||
|
||||
return Inertia::render('Notifications/Unread', [
|
||||
'activities' => $activities,
|
||||
'today' => $today,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user