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\Models\Client;
7use App\Models\TblQuotations;
8use Illuminate\Http\Request;
9use Illuminate\Support\Facades\App;
10use Illuminate\Support\Facades\DB;
11use App\Http\Requests\ListClientsRequest;
12use App\Http\Requests\StoreClientRequest;
13use App\Http\Requests\UpdateClientRequest;
14use App\Http\Resources\ClientResource;
15use App\Http\Resources\ClientSummaryResource;
16use App\Http\Controllers\Concerns\AuthorizesClientAccess;
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    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            return response(['message' => 'KO', 'error' => $e->getMessage()]);
107        }
108    }
109
110    /**
111     * POST /clients
112     * Create a new client.
113     */
114    function create_client(StoreClientRequest $request)
115    {
116        if (!$this->canWrite()) {
117            return $this->forbidden();
118        }
119
120        try {
121            $data = $request->validated();
122            $client = Client::create($data);
123
124            if (!empty($data['commercial_ids'])) {
125                $client->commercials()->sync($data['commercial_ids']);
126            }
127
128            $client->load(['clientType', 'segment', 'strategyType', 'administrator', 'commercials']);
129
130            return new ClientSummaryResource($client);
131
132        } catch (\Exception $e) {
133            report(AppException::fromException($e, 'CREATE_CLIENT_EXCEPTION'));
134            return response(['message' => 'KO', 'error' => $e->getMessage()]);
135        }
136    }
137
138    /**
139     * GET /clients/{id}
140     * Get client details.
141     */
142    function get_client($id)
143    {
144        $id = (int) $id;
145
146        if ($this->isCommercial() && !$this->commercialOwnsClient($id)) {
147            return $this->forbidden();
148        }
149
150        try {
151            $client = Client::with(['clientType', 'segment', 'strategyType', 'administrator', 'commercials', 'tickets'])
152                ->findOrFail($id);
153
154            return new ClientResource($client);
155
156        } catch (\Exception $e) {
157            report(AppException::fromException($e, 'GET_CLIENT_EXCEPTION'));
158            return response(['message' => 'KO', 'error' => $e->getMessage()]);
159        }
160    }
161
162    /**
163     * PUT /clients/{id}
164     * Update a client.
165     */
166    function update_client(UpdateClientRequest $request, $id)
167    {
168        $id = (int) $id;
169
170        if (!$this->canWrite()) {
171            return $this->forbidden();
172        }
173
174        if ($this->isCommercial() && !$this->commercialOwnsClient($id)) {
175            return $this->forbidden();
176        }
177
178        try {
179            $data = $request->validated();
180
181            Client::where('id', $id)->update(
182                collect($data)->except('commercial_ids')->toArray()
183            );
184
185            $client = Client::findOrFail($id);
186
187            if (!empty($data['commercial_ids'])) {
188                $client->commercials()->sync($data['commercial_ids']);
189            }
190
191            $client->load(['clientType', 'segment', 'strategyType', 'administrator', 'commercials', 'tickets']);
192
193            return new ClientResource($client);
194
195        } catch (\Exception $e) {
196            report(AppException::fromException($e, 'UPDATE_CLIENT_EXCEPTION'));
197            return response(['message' => 'KO', 'error' => $e->getMessage()]);
198        }
199    }
200
201    /**
202     * DELETE /clients/{id}
203     * Delete a client only if it has no linked quotations.
204     */
205    function delete_client($id)
206    {
207        $id = (int) $id;
208
209        if (!$this->canDelete()) {
210            return $this->forbidden();
211        }
212
213        try {
214
215            $quotations = TblQuotations::where('client_id', $id)->count();
216
217            if ($quotations > 0) {
218                return response([
219                    'message' => 'KO',
220                    'error' => "Cannot delete client because it has {$quotations} linked quotation(s).",
221                ]);
222            }
223
224            Client::where('id', $id)->delete();
225
226            return response(['message' => 'OK']);
227
228        } catch (\Exception $e) {
229            report(AppException::fromException($e, 'DELETE_CLIENT_EXCEPTION'));
230            return response(['message' => 'KO', 'error' => $e->getMessage()]);
231        }
232    }
233
234    /**
235     * GET /clients/autocomplete?search=xxx
236     * Combined search: clients table + historical quotation customer names.
237     */
238    function autocomplete(Request $request)
239    {
240        try {
241            $search = $request->input('search', '');
242
243            if (strlen($search) < 2) {
244                return response(['message' => 'OK', 'data' => []]);
245            }
246
247            $fromTable = Client::where(function ($q) use ($search) {
248                    $q->where('company_name', 'LIKE', "%{$search}%")
249                      ->orWhere('fiscal_id', 'LIKE', "%{$search}%")
250                      ->orWhere('contact_phone', 'LIKE', "%{$search}%")
251                      ->orWhere('contact_email', 'LIKE', "%{$search}%");
252                })
253                ->select('id', 'company_name', 'fiscal_id')
254                ->limit(20)
255                ->get()
256                ->map(fn($c) => [
257                    'id'           => $c->id,
258                    'company_name' => $c->company_name,
259                    'fiscal_id'    => $c->fiscal_id,
260                    'source'       => 'clients',
261                ]);
262
263            $existingNames = $fromTable->pluck('company_name')->map(fn($n) => mb_strtolower($n))->toArray();
264
265            $fromQuotations = DB::table('tbl_quotations')
266                ->where('client', 'LIKE', "%{$search}%")
267                ->whereNull('client_id')
268                ->select('client')
269                ->distinct()
270                ->limit(20)
271                ->get()
272                ->filter(fn($q) => !in_array(mb_strtolower($q->client), $existingNames))
273                ->map(fn($q) => [
274                    'id'           => null,
275                    'company_name' => $q->client,
276                    'source'       => 'quotations',
277                ]);
278
279            return response([
280                'message' => 'OK',
281                'data'    => $fromTable->concat($fromQuotations->values()),
282            ]);
283
284        } catch (\Exception $e) {
285            report(AppException::fromException($e, 'AUTOCOMPLETE_CLIENTS_EXCEPTION'));
286            return response(['message' => 'KO', 'error' => $e->getMessage()]);
287        }
288    }
289}