Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
62.59% covered (warning)
62.59%
87 / 139
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ClientsController
62.59% covered (warning)
62.59%
87 / 139
14.29% covered (danger)
14.29%
1 / 7
180.89
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 list_clients
85.11% covered (warning)
85.11%
40 / 47
0.00% covered (danger)
0.00%
0 / 1
30.59
 create_client
18.18% covered (danger)
18.18%
2 / 11
0.00% covered (danger)
0.00%
0 / 1
12.76
 get_client
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
4.18
 update_client
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 delete_client
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 autocomplete
92.31% covered (success)
92.31%
36 / 39
0.00% covered (danger)
0.00%
0 / 1
3.00
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Exceptions\AppException;
6use App\Http\Controllers\Concerns\AuthorizesClientAccess;
7use App\Http\Requests\ListClientsRequest;
8use App\Http\Requests\StoreClientRequest;
9use App\Http\Requests\UpdateClientRequest;
10use App\Http\Resources\ClientResource;
11use App\Http\Resources\ClientSummaryResource;
12use App\Models\Client;
13use App\Models\TblQuotations;
14use Illuminate\Http\Request;
15use Illuminate\Support\Facades\App;
16use Illuminate\Support\Facades\DB;
17
18class ClientsController extends Controller
19{
20    use AuthorizesClientAccess;
21
22    private $userId;
23
24    public function __construct()
25    {
26        $this->userId = request()->header('backend-user-id');
27        App::setLocale(request()->header('Locale-Id'));
28    }
29
30    /**
31     * List clients
32     *
33     * Returns a list of clients with optional filters.
34     * Commercial users only see their own clients.
35     * Customer-service users see a maximum of 10 results with a `total` count in the response metadata.
36     */
37    public function list_clients(ListClientsRequest $request)
38    {
39        try {
40            $query = Client::with(['clientType', 'segment', 'strategyType', 'administrator', 'commercials']);
41
42            if ($request->filled('search')) {
43                $search = $request->input('search');
44                $query->where(function ($q) use ($search) {
45                    $q->where('company_name', 'LIKE', "%{$search}%")
46                        ->orWhere('fiscal_id', 'LIKE', "%{$search}%")
47                        ->orWhere('contact_phone', 'LIKE', "%{$search}%")
48                        ->orWhere('contact_email', 'LIKE', "%{$search}%");
49                });
50            }
51            if ($request->filled('client_type_id')) {
52                $query->where('client_type_id', $request->input('client_type_id'));
53            }
54            if ($request->filled('segment_id')) {
55                $query->where('segment_id', $request->input('segment_id'));
56            }
57            if ($request->filled('strategy_type_id')) {
58                $query->where('strategy_type_id', $request->input('strategy_type_id'));
59            }
60            if ($request->filled('scope')) {
61                $query->where('scope', $request->input('scope'));
62            }
63            if ($request->filled('annual_maintenance') && $request->input('annual_maintenance') !== 'all') {
64                $query->where('annual_maintenance', $request->input('annual_maintenance') === 'yes' ? 1 : 0);
65            }
66            if ($request->filled('quarterly_maintenance') && $request->input('quarterly_maintenance') !== 'all') {
67                $query->where('quarterly_maintenance', $request->input('quarterly_maintenance') === 'yes' ? 1 : 0);
68            }
69            if ($request->filled('fire_suppression') && $request->input('fire_suppression') !== 'all') {
70                $query->where('fire_suppression', $request->input('fire_suppression') === 'yes' ? 1 : 0);
71            }
72            if ($request->filled('fire_detection') && $request->input('fire_detection') !== 'all') {
73                $query->where('fire_detection', $request->input('fire_detection') === 'yes' ? 1 : 0);
74            }
75            if ($request->filled('water') && $request->input('water') !== 'all') {
76                $query->where('water', $request->input('water') === 'yes' ? 1 : 0);
77            }
78            if ($request->filled('relationship_status') && $request->input('relationship_status') !== 'all') {
79                $query->where('relationship_status', $request->input('relationship_status'));
80            }
81            if ($request->filled('administrator_id')) {
82                $query->where('administrator_id', $request->input('administrator_id'));
83            }
84            if ($request->filled('commercial_id')) {
85                $query->whereHas('commercials', function ($q) use ($request) {
86                    $q->where('tbl_users.id', $request->input('commercial_id'));
87                });
88            }
89
90            if ($request->header('backend-role') === 'commercial') {
91                $query->whereHas('commercials', function ($q) {
92                    $q->where('tbl_users.id', $this->userId);
93                });
94            }
95
96            $perPage = $request->header('backend-role') === 'customer_service'
97                ? 10
98                : (int) $request->input('per_page', 25);
99
100            $data = $query->orderBy('company_name')->paginate($perPage);
101
102            return ClientSummaryResource::collection($data);
103
104        } catch (\Exception $e) {
105            report(AppException::fromException($e, 'LIST_CLIENTS_EXCEPTION'));
106
107            return response(['message' => 'KO', 'error' => $e->getMessage()]);
108        }
109    }
110
111    /**
112     * POST /clients
113     * Create a new client.
114     */
115    public function create_client(StoreClientRequest $request)
116    {
117        if (! $this->canWrite()) {
118            return $this->forbidden();
119        }
120
121        try {
122            $data = $request->validated();
123            $client = Client::create($data);
124
125            if (! empty($data['commercial_ids'])) {
126                $client->commercials()->sync($data['commercial_ids']);
127            }
128
129            $client->load(['clientType', 'segment', 'strategyType', 'administrator', 'commercials']);
130
131            return new ClientSummaryResource($client);
132
133        } catch (\Exception $e) {
134            report(AppException::fromException($e, 'CREATE_CLIENT_EXCEPTION'));
135
136            return response(['message' => 'KO', 'error' => $e->getMessage()]);
137        }
138    }
139
140    /**
141     * GET /clients/{id}
142     * Get client details.
143     */
144    public function get_client($id)
145    {
146        $id = (int) $id;
147
148        if ($this->isCommercial() && ! $this->commercialOwnsClient($id)) {
149            return $this->forbidden();
150        }
151
152        try {
153            $client = Client::with(['clientType', 'segment', 'strategyType', 'administrator', 'commercials', 'tickets'])
154                ->findOrFail($id);
155
156            return new ClientResource($client);
157
158        } catch (\Exception $e) {
159            report(AppException::fromException($e, 'GET_CLIENT_EXCEPTION'));
160
161            return response(['message' => 'KO', 'error' => $e->getMessage()]);
162        }
163    }
164
165    /**
166     * PUT /clients/{id}
167     * Update a client.
168     */
169    public function update_client(UpdateClientRequest $request, $id)
170    {
171        $id = (int) $id;
172
173        if (! $this->canWrite()) {
174            return $this->forbidden();
175        }
176
177        if ($this->isCommercial() && ! $this->commercialOwnsClient($id)) {
178            return $this->forbidden();
179        }
180
181        try {
182            $data = $request->validated();
183
184            Client::where('id', $id)->update(
185                collect($data)->except('commercial_ids')->toArray()
186            );
187
188            $client = Client::findOrFail($id);
189
190            if (! empty($data['commercial_ids'])) {
191                $client->commercials()->sync($data['commercial_ids']);
192            }
193
194            $client->load(['clientType', 'segment', 'strategyType', 'administrator', 'commercials', 'tickets']);
195
196            return new ClientResource($client);
197
198        } catch (\Exception $e) {
199            report(AppException::fromException($e, 'UPDATE_CLIENT_EXCEPTION'));
200
201            return response(['message' => 'KO', 'error' => $e->getMessage()]);
202        }
203    }
204
205    /**
206     * DELETE /clients/{id}
207     * Delete a client only if it has no linked quotations.
208     */
209    public function delete_client($id)
210    {
211        $id = (int) $id;
212
213        if (! $this->canDelete()) {
214            return $this->forbidden();
215        }
216
217        try {
218
219            $quotations = TblQuotations::where('client_id', $id)->count();
220
221            if ($quotations > 0) {
222                return response([
223                    'message' => 'KO',
224                    'error' => "Cannot delete client because it has {$quotations} linked quotation(s).",
225                ]);
226            }
227
228            Client::where('id', $id)->delete();
229
230            return response(['message' => 'OK']);
231
232        } catch (\Exception $e) {
233            report(AppException::fromException($e, 'DELETE_CLIENT_EXCEPTION'));
234
235            return response(['message' => 'KO', 'error' => $e->getMessage()]);
236        }
237    }
238
239    /**
240     * GET /clients/autocomplete?search=xxx
241     * Combined search: clients table + historical quotation customer names.
242     */
243    public function autocomplete(Request $request)
244    {
245        try {
246            $search = $request->input('search', '');
247
248            if (strlen($search) < 2) {
249                return response(['message' => 'OK', 'data' => []]);
250            }
251
252            $fromTable = Client::where(function ($q) use ($search) {
253                $q->where('company_name', 'LIKE', "%{$search}%")
254                    ->orWhere('fiscal_id', 'LIKE', "%{$search}%")
255                    ->orWhere('contact_phone', 'LIKE', "%{$search}%")
256                    ->orWhere('contact_email', 'LIKE', "%{$search}%");
257            })
258                ->select('id', 'company_name', 'fiscal_id')
259                ->limit(20)
260                ->get()
261                ->map(fn ($c) => [
262                'id' => $c->id,
263                'company_name' => $c->company_name,
264                'fiscal_id' => $c->fiscal_id,
265                'source' => 'clients',
266            ]);
267
268            $existingNames = $fromTable->pluck('company_name')->map(fn ($n) => mb_strtolower($n))->toArray();
269
270            $fromQuotations = DB::table('tbl_quotations')
271                ->where('client', 'LIKE', "%{$search}%")
272                ->whereNull('client_id')
273                ->select('client')
274                ->distinct()
275                ->limit(20)
276                ->get()
277                ->filter(fn ($q) => ! in_array(mb_strtolower($q->client), $existingNames))
278                ->map(fn ($q) => [
279                    'id' => null,
280                    'company_name' => $q->client,
281                    'source' => 'quotations',
282                ]);
283
284            return response([
285                'message' => 'OK',
286                'data' => $fromTable->concat($fromQuotations->values()),
287            ]);
288
289        } catch (\Exception $e) {
290            report(AppException::fromException($e, 'AUTOCOMPLETE_CLIENTS_EXCEPTION'));
291
292            return response(['message' => 'KO', 'error' => $e->getMessage()]);
293        }
294    }
295}