DashboardService.php 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015
  1. <?php
  2. namespace App\Services;
  3. use App\Enums\GenderEnum;
  4. use App\Enums\ServicePackageStatusEnum;
  5. use App\Enums\UserTypeEnum;
  6. use App\Models\Address;
  7. use App\Models\Client;
  8. use App\Models\ClientFavoriteProvider;
  9. use App\Models\ClientPaymentMethod;
  10. use App\Models\Notification;
  11. use App\Models\Provider;
  12. use App\Models\Review;
  13. use App\Models\Schedule;
  14. use App\Models\ScheduleProposal;
  15. use App\Models\ServicePackage;
  16. use App\Rules\ScheduleBusinessRules;
  17. use Illuminate\Auth\Access\AuthorizationException;
  18. use Illuminate\Support\Collection;
  19. use Illuminate\Support\Facades\Auth;
  20. use Illuminate\Support\Facades\DB;
  21. use Illuminate\Support\Facades\Storage;
  22. class DashboardService
  23. {
  24. public function __construct(
  25. private readonly CustomScheduleService $customScheduleService,
  26. private readonly ZipCodeCoordinatesService $zipCodeCoordinatesService,
  27. ) {}
  28. public function dadosDashboardCliente(): array
  29. {
  30. $user = Auth::user();
  31. if ($user->type !== UserTypeEnum::CLIENT) {
  32. throw new AuthorizationException(__('messages.only_clients_allowed'));
  33. }
  34. $cliente = Client::with('profileMedia')->where('user_id', $user->id)->first();
  35. $headerBar = [
  36. 'rating' => $cliente->average_rating,
  37. 'total_services' => $cliente->total_services,
  38. 'total_ratings' => Review::where('reviews.origin', 'provider')
  39. ->leftJoin('schedules', 'schedules.id', '=', 'reviews.schedule_id')
  40. ->where('schedules.client_id', $cliente->id)
  41. ->count(),
  42. ];
  43. $address = Address::where('source', 'client')
  44. ->where('source_id', $cliente->id)
  45. ->with(['city', 'state'])
  46. ->select('id', 'source', 'source_id', 'address', 'number', 'district', 'nickname', 'address_type', 'city_id', 'state_id', 'is_primary')
  47. ->first();
  48. $summaryInfos = [
  49. 'name' => $user->name,
  50. 'address' => $address,
  51. 'profile_photo' => $cliente->profileMedia?->path
  52. ? Storage::temporaryUrl($cliente->profileMedia->path, now()->addMinutes(60))
  53. : null,
  54. 'pending_services' => Schedule::where('client_id', $cliente->id)
  55. ->whereIn('status', ['pending', 'paid', 'accepted'])
  56. ->whereDate('date', '>=', now()->toDateString())
  57. ->count(),
  58. ];
  59. $nextSchedules = Schedule::with([
  60. 'address:district,address,source_id,source,id,address_type',
  61. ])
  62. ->where('schedules.client_id', $cliente->id)
  63. ->whereIn('schedules.status', ['accepted', 'paid'])
  64. ->whereDate('schedules.date', '>=', now()->toDateString())
  65. ->where('schedules.date', '>=', now()->toDateString())
  66. ->leftJoin('providers', 'providers.id', '=', 'schedules.provider_id')
  67. ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
  68. ->leftJoin('custom_schedules', 'custom_schedules.schedule_id', '=', 'schedules.id')
  69. ->select(
  70. 'schedules.id',
  71. 'schedules.provider_id',
  72. 'provider_user.name as provider_name',
  73. 'providers.gender',
  74. 'schedules.date',
  75. 'schedules.start_time',
  76. 'schedules.end_time',
  77. 'schedules.total_amount',
  78. 'schedules.period_type',
  79. 'schedules.schedule_type',
  80. 'schedules.address_id',
  81. 'custom_schedules.address_type as custom_address_type',
  82. DB::raw("
  83. (
  84. SELECT spi.service_package_id
  85. FROM service_package_items spi
  86. WHERE spi.schedule_id = schedules.id
  87. LIMIT 1
  88. ) AS service_package_id
  89. "),
  90. DB::raw("
  91. (
  92. SELECT COUNT(*)
  93. FROM service_package_items spi_count
  94. WHERE spi_count.service_package_id = (
  95. SELECT spi.service_package_id
  96. FROM service_package_items spi
  97. WHERE spi.schedule_id = schedules.id
  98. LIMIT 1
  99. )
  100. ) AS service_package_items_count
  101. "),
  102. )
  103. ->orderBy('schedules.date', 'asc')
  104. ->limit(5)
  105. ->get();
  106. $nextSchedules->each(function ($item) {
  107. $item->gender_label = GenderEnum::labelFor($item->gender);
  108. });
  109. $latestPerProvider = Schedule::where('client_id', $cliente->id)
  110. ->where('status', 'finished')
  111. ->select('provider_id', DB::raw('MAX(id) as max_id'))
  112. ->groupBy('provider_id');
  113. $lastDoneSchedules = Schedule::joinSub($latestPerProvider, 'latest', function ($join) {
  114. $join->on('schedules.id', '=', 'latest.max_id');
  115. })
  116. ->leftJoin('providers', 'providers.id', '=', 'schedules.provider_id')
  117. ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
  118. ->leftJoinSub(
  119. Address::preferredForProvider(),
  120. 'provider_address',
  121. fn($join) =>
  122. $join->on('provider_address.source_id', '=', 'providers.id')
  123. ->where('provider_address.rn', 1)
  124. )
  125. ->select(
  126. 'schedules.id',
  127. 'schedules.provider_id',
  128. 'provider_user.name as provider_name',
  129. 'providers.gender',
  130. 'provider_address.district as provider_district',
  131. )
  132. ->orderBy('schedules.date', 'desc')
  133. ->limit(5)
  134. ->get();
  135. $lastDoneSchedules->each(function ($item) {
  136. $item->gender_label = GenderEnum::labelFor($item->gender);
  137. });
  138. $favoriteProviders = ClientFavoriteProvider::where('client_favorite_providers.client_id', $cliente->id)
  139. ->leftJoin('providers', 'providers.id', '=', 'client_favorite_providers.provider_id')
  140. ->whereNotNull('providers.recipient_default_bank_account')
  141. ->whereRaw("providers.recipient_default_bank_account::text <> '{}'")
  142. ->whereRaw("providers.recipient_default_bank_account::text <> '[]'")
  143. ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
  144. ->leftJoinSub(
  145. Address::preferredForProvider(),
  146. 'provider_address',
  147. fn($join) =>
  148. $join->on('provider_address.source_id', '=', 'providers.id')
  149. ->where('provider_address.rn', 1)
  150. )
  151. ->select(
  152. 'providers.id as provider_id',
  153. 'provider_user.name as provider_name',
  154. 'providers.gender',
  155. 'providers.average_rating',
  156. 'provider_address.district as provider_district',
  157. )
  158. ->orderBy('client_favorite_providers.created_at', 'desc')
  159. ->limit(5)
  160. ->get();
  161. $favoriteProviders->each(function ($item) {
  162. $item->gender_label = GenderEnum::labelFor($item->gender);
  163. });
  164. $blockedProviderIds = ScheduleBusinessRules::getBlockedProviderIdsForClient($cliente->id);
  165. $providersWithWorkingDays = ScheduleBusinessRules::getProviderIdsWithWorkingDays();
  166. $clientPrimaryAddress = Address::where('source', 'client')
  167. ->where('source_id', $cliente->id)
  168. ->orderByDesc('is_primary')
  169. ->orderByDesc('id')
  170. ->first();
  171. $clientDistanceAddress = $this->addressForDistance($cliente->id, $clientPrimaryAddress);
  172. $clientCoordinates = $this->zipCodeCoordinatesService->resolve(
  173. $clientDistanceAddress?->latitude !== null ? (float) $clientDistanceAddress->latitude : null,
  174. $clientDistanceAddress?->longitude !== null ? (float) $clientDistanceAddress->longitude : null,
  175. $clientDistanceAddress?->zip_code,
  176. );
  177. $providersCloseDistanceSelect = $this->distanceSelect(
  178. data_get($clientCoordinates, 'latitude'),
  179. data_get($clientCoordinates, 'longitude'),
  180. );
  181. $providerAddressLatestSubquery = DB::raw("
  182. (
  183. SELECT DISTINCT ON (source_id)
  184. *
  185. FROM addresses
  186. WHERE
  187. source = 'provider'
  188. AND deleted_at IS NULL
  189. ORDER BY
  190. source_id,
  191. (latitude IS NOT NULL AND longitude IS NOT NULL) DESC,
  192. is_primary DESC,
  193. id DESC
  194. ) AS provider_address
  195. ");
  196. $providersClose = Provider::leftJoin(
  197. 'users as provider_user',
  198. 'provider_user.id',
  199. '=',
  200. 'providers.user_id'
  201. )
  202. ->visibleToCustomers()
  203. ->leftJoin(
  204. $providerAddressLatestSubquery,
  205. 'provider_address.source_id',
  206. '=',
  207. 'providers.id'
  208. )
  209. ->whereNotNull('provider_address.id')
  210. ->when(
  211. $clientPrimaryAddress?->city_id,
  212. fn($query, int $cityId) => $query->where('provider_address.city_id', $cityId)
  213. )
  214. ->whereNotIn('providers.id', $blockedProviderIds)
  215. ->whereIn('providers.id', $providersWithWorkingDays)
  216. ->whereNull('providers.deleted_at')
  217. ->select(
  218. 'providers.id as provider_id',
  219. 'provider_user.name as provider_name',
  220. 'providers.gender',
  221. 'provider_address.id as address_id',
  222. 'provider_address.zip_code as provider_zip_code',
  223. 'provider_address.district',
  224. 'provider_address.latitude as provider_latitude',
  225. 'provider_address.longitude as provider_longitude',
  226. 'providers.average_rating',
  227. 'providers.total_services',
  228. 'providers.daily_price_8h',
  229. 'providers.daily_price_6h',
  230. 'providers.daily_price_4h',
  231. 'providers.daily_price_2h',
  232. DB::raw("
  233. (
  234. SELECT COUNT(*)
  235. FROM reviews
  236. LEFT JOIN schedules
  237. ON schedules.id = reviews.schedule_id
  238. WHERE reviews.origin = 'provider'
  239. AND schedules.provider_id = providers.id
  240. ) AS total_reviews
  241. "),
  242. $providersCloseDistanceSelect,
  243. )
  244. ->orderByRaw('distance_km ASC NULLS LAST')
  245. ->get();
  246. $this->zipCodeCoordinatesService->preload(
  247. $providersClose->whereNull('distance_km')->pluck('provider_zip_code')
  248. );
  249. $providersClose->each(function ($item) use ($clientDistanceAddress) {
  250. $item->gender_label = GenderEnum::labelFor($item->gender);
  251. if ($item->distance_km === null) {
  252. $item->distance_km = $this->zipCodeCoordinatesService->calculateDistance(
  253. $clientDistanceAddress?->latitude !== null ? (float) $clientDistanceAddress->latitude : null,
  254. $clientDistanceAddress?->longitude !== null ? (float) $clientDistanceAddress->longitude : null,
  255. $clientDistanceAddress?->zip_code,
  256. $item->provider_latitude !== null ? (float) $item->provider_latitude : null,
  257. $item->provider_longitude !== null ? (float) $item->provider_longitude : null,
  258. $item->provider_zip_code,
  259. );
  260. }
  261. $item->specialities = ProviderSpeciality::query()
  262. ->join('specialities', 'specialities.id', '=', 'provider_specialities.speciality_id')
  263. ->where('provider_specialities.provider_id', $item->provider_id)
  264. ->where('specialities.active', true)
  265. ->orderBy('specialities.description')
  266. ->get([
  267. 'specialities.id',
  268. 'specialities.description',
  269. ]);
  270. $item->age = Provider::query()
  271. ->where('id', $item->provider_id)
  272. ->value(DB::raw("DATE_PART('year', AGE(birth_date))"));
  273. unset($item->provider_zip_code);
  274. $item->daily_price_8h_base = $item->daily_price_8h;
  275. $item->daily_price_6h_base = $item->daily_price_6h;
  276. $item->daily_price_4h_base = $item->daily_price_4h;
  277. $item->daily_price_2h_base = $item->daily_price_2h;
  278. $item->daily_price_8h = $this->applyCreditCardFee($item->daily_price_8h);
  279. $item->daily_price_6h = $this->applyCreditCardFee($item->daily_price_6h);
  280. $item->daily_price_4h = $this->applyCreditCardFee($item->daily_price_4h);
  281. $item->daily_price_2h = $this->applyCreditCardFee($item->daily_price_2h);
  282. });
  283. $providersClose = $providersClose
  284. ->sortBy(fn($provider) => $provider->distance_km ?? PHP_FLOAT_MAX)
  285. ->values();
  286. $pendingSchedules = Schedule::with([
  287. 'address:district,address,number,source_id,source,id,address_type',
  288. ])
  289. ->where('schedules.client_id', $cliente->id)
  290. ->whereIn('schedules.status', ['pending', 'accepted'])
  291. ->where('schedules.schedule_type', 'default')
  292. ->leftJoin('providers', 'providers.id', '=', 'schedules.provider_id')
  293. ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
  294. ->select(
  295. 'schedules.id',
  296. 'schedules.provider_id',
  297. 'provider_user.name as provider_name',
  298. 'providers.gender',
  299. 'schedules.date',
  300. 'schedules.address_id',
  301. 'schedules.status',
  302. 'schedules.total_amount',
  303. 'schedules.start_time',
  304. 'schedules.end_time',
  305. DB::raw("
  306. CASE
  307. WHEN (NOW() - schedules.created_at) < INTERVAL '1 hour' THEN
  308. CONCAT(
  309. ROUND(EXTRACT(EPOCH FROM (NOW() - schedules.created_at)) / 60),
  310. 'min'
  311. )
  312. WHEN (NOW() - schedules.created_at) < INTERVAL '1 day' THEN
  313. CONCAT(
  314. ROUND(EXTRACT(EPOCH FROM (NOW() - schedules.created_at)) / 3600),
  315. 'h'
  316. )
  317. ELSE
  318. CONCAT(
  319. ROUND(EXTRACT(EPOCH FROM (NOW() - schedules.created_at)) / 86400),
  320. 'd'
  321. )
  322. END AS time_since_request
  323. "),
  324. DB::raw("
  325. (
  326. SELECT spi.service_package_id
  327. FROM service_package_items spi
  328. WHERE spi.schedule_id = schedules.id
  329. LIMIT 1
  330. ) AS service_package_id
  331. "),
  332. DB::raw("
  333. (
  334. SELECT COUNT(*)
  335. FROM service_package_items spi_count
  336. WHERE spi_count.service_package_id = (
  337. SELECT spi.service_package_id
  338. FROM service_package_items spi
  339. WHERE spi.schedule_id = schedules.id
  340. LIMIT 1
  341. )
  342. ) AS service_package_items_count
  343. "),
  344. )
  345. ->orderBy('schedules.date', 'asc')
  346. ->get();
  347. $pendingSchedules->each(function ($item) {
  348. $item->gender_label = GenderEnum::labelFor($item->gender);
  349. });
  350. $proposalsDistanceSelect = DistanceService::sqlExpression(
  351. data_get($clientCoordinates, 'latitude'),
  352. data_get($clientCoordinates, 'longitude'),
  353. );
  354. $schedulesProposals = ScheduleProposal::query()
  355. ->leftJoin(
  356. 'schedules',
  357. 'schedule_proposals.schedule_id',
  358. '=',
  359. 'schedules.id'
  360. )
  361. ->leftJoin(
  362. 'providers',
  363. 'schedule_proposals.provider_id',
  364. '=',
  365. 'providers.id'
  366. )
  367. ->whereNotNull('providers.recipient_default_bank_account')
  368. ->whereRaw("providers.recipient_default_bank_account::text <> '{}'")
  369. ->whereRaw("providers.recipient_default_bank_account::text <> '[]'")
  370. ->leftJoin('users', 'providers.user_id', '=', 'users.id')
  371. ->leftJoin(
  372. DB::raw("
  373. (
  374. SELECT DISTINCT ON (source_id)
  375. *
  376. FROM addresses
  377. WHERE source = 'provider'
  378. AND deleted_at IS NULL
  379. ORDER BY source_id, is_primary DESC
  380. ) AS provider_address
  381. "),
  382. 'provider_address.source_id',
  383. '=',
  384. 'providers.id'
  385. )
  386. ->where('schedules.client_id', $cliente->id)
  387. ->where('schedules.schedule_type', 'custom')
  388. ->where('schedules.status', 'pending')
  389. ->whereNull('schedules.deleted_at')
  390. ->whereDate('schedules.date', '>=', now()->toDateString())
  391. ->orderBy('schedule_proposals.created_at', 'desc')
  392. ->select([
  393. 'schedule_proposals.id',
  394. DB::raw("
  395. DATE_PART('year', AGE(providers.birth_date)) AS idade
  396. "),
  397. 'providers.id as provider_id',
  398. 'providers.gender',
  399. 'schedules.id as schedule_id',
  400. 'schedules.date',
  401. 'schedules.start_time',
  402. 'schedules.end_time',
  403. 'schedules.period_type',
  404. 'schedules.total_amount',
  405. 'providers.daily_price_8h',
  406. 'providers.average_rating',
  407. 'providers.total_services',
  408. 'users.name as provider_name',
  409. 'provider_address.latitude as provider_latitude',
  410. 'provider_address.longitude as provider_longitude',
  411. 'provider_address.zip_code as provider_zip_code',
  412. $proposalsDistanceSelect,
  413. ])
  414. ->get();
  415. $this->zipCodeCoordinatesService->preload(
  416. $schedulesProposals->whereNull('distance_km')->pluck('provider_zip_code')
  417. );
  418. $custom_schedules_with_no_proposals = Schedule::where('client_id', $cliente->id)
  419. ->where('schedule_type', 'custom')
  420. ->where('status', 'pending')
  421. ->whereDate('date', '>=', now()->toDateString())
  422. ->doesntHave('proposals')
  423. ->get();
  424. $schedulesProposals->each(function ($item) use ($clientDistanceAddress) {
  425. $item->gender_label = GenderEnum::labelFor($item->gender);
  426. if ($item->distance_km === null) {
  427. $item->distance_km = $this->zipCodeCoordinatesService->calculateDistance(
  428. $clientDistanceAddress?->latitude !== null ? (float) $clientDistanceAddress->latitude : null,
  429. $clientDistanceAddress?->longitude !== null ? (float) $clientDistanceAddress->longitude : null,
  430. $clientDistanceAddress?->zip_code,
  431. $item->provider_latitude !== null ? (float) $item->provider_latitude : null,
  432. $item->provider_longitude !== null ? (float) $item->provider_longitude : null,
  433. $item->provider_zip_code,
  434. );
  435. }
  436. unset(
  437. $item->provider_latitude,
  438. $item->provider_longitude,
  439. $item->provider_zip_code,
  440. );
  441. });
  442. $todaySchedules = Schedule::with([
  443. 'address:district,address,number,source_id,source,id,address_type',
  444. ])
  445. ->where('schedules.client_id', $cliente->id)
  446. ->whereIn('schedules.status', ['accepted', 'paid', 'started', 'cancelled', 'finished'])
  447. ->whereDate('schedules.date', now()->toDateString())
  448. ->leftJoin('providers', 'providers.id', '=', 'schedules.provider_id')
  449. ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
  450. ->select(
  451. 'schedules.id',
  452. 'schedules.provider_id',
  453. 'provider_user.name as provider_name',
  454. 'providers.gender',
  455. 'schedules.date',
  456. 'schedules.start_time',
  457. 'schedules.end_time',
  458. 'schedules.total_amount',
  459. 'schedules.period_type',
  460. 'schedules.schedule_type',
  461. 'schedules.address_id',
  462. 'schedules.status',
  463. 'schedules.code_verified',
  464. 'schedules.code',
  465. DB::raw("
  466. (
  467. SELECT spi.service_package_id
  468. FROM service_package_items spi
  469. WHERE spi.schedule_id = schedules.id
  470. LIMIT 1
  471. ) AS service_package_id
  472. "),
  473. DB::raw("
  474. (
  475. SELECT COUNT(*)
  476. FROM service_package_items spi_count
  477. WHERE spi_count.service_package_id = (
  478. SELECT spi.service_package_id
  479. FROM service_package_items spi
  480. WHERE spi.schedule_id = schedules.id
  481. LIMIT 1
  482. )
  483. ) AS service_package_items_count
  484. "),
  485. DB::raw("
  486. EXISTS (
  487. SELECT 1
  488. FROM reviews
  489. WHERE reviews.schedule_id = schedules.id
  490. AND reviews.origin = 'client'
  491. AND reviews.origin_id = {$cliente->id}
  492. AND reviews.deleted_at IS NULL
  493. ) AS client_reviewed
  494. "),
  495. )
  496. ->orderBy('schedules.start_time', 'asc')
  497. ->get()
  498. ->map(function ($item) {
  499. $item->gender_label = GenderEnum::labelFor($item->gender);
  500. return $item;
  501. });
  502. $providerCollections = collect([
  503. $nextSchedules,
  504. $lastDoneSchedules,
  505. $favoriteProviders,
  506. $providersClose,
  507. $pendingSchedules,
  508. $schedulesProposals,
  509. $todaySchedules,
  510. ]);
  511. $providerPhotoUrls = $this->providerPhotoUrls(
  512. $providerCollections->flatMap(
  513. fn(Collection $items) => $items->pluck('provider_id'),
  514. ),
  515. );
  516. $providerCollections->each(function (Collection $items) use ($providerPhotoUrls) {
  517. $items->each(function ($item) use ($providerPhotoUrls) {
  518. $item->provider_photo = $providerPhotoUrls->get($item->provider_id);
  519. });
  520. });
  521. $notifications = Notification::where('user_id', $user->id)
  522. ->orderBy('read', 'asc')
  523. ->orderBy('created_at', 'desc')
  524. ->limit(10)
  525. ->get()
  526. ->map(function ($notification) {
  527. return [
  528. 'id' => $notification->id,
  529. 'title' => $notification->title,
  530. 'description' => $notification->description,
  531. 'time' => $notification->created_at->diffForHumans(),
  532. 'read' => $notification->read,
  533. 'avatar' => '/icons/avatar.svg',
  534. ];
  535. });
  536. $hasPaymentMethods = ClientPaymentMethod::where('client_id', $cliente->id)->exists();
  537. $pendingServicePackages = ServicePackage::query()
  538. ->where('client_id', $cliente->id)
  539. ->where('status', ServicePackageStatusEnum::OPEN->value)
  540. ->whereDoesntHave('items.schedule', fn($q) => $q->where('status', '!=', 'accepted'))
  541. ->with(['items.schedule' => function ($query) {
  542. $query->with(['provider.user', 'address']);
  543. }])
  544. ->with('provider.user')
  545. ->get();
  546. return [
  547. 'headerBar' => $headerBar,
  548. 'summaryInfos' => $summaryInfos,
  549. 'pendingSchedules' => $pendingSchedules,
  550. 'nextSchedules' => $nextSchedules,
  551. 'lastDoneSchedules' => $lastDoneSchedules,
  552. 'favoriteProviders' => $favoriteProviders,
  553. 'providersClose' => $providersClose,
  554. 'todaySchedules' => $todaySchedules,
  555. 'schedulesProposals' => $schedulesProposals,
  556. 'customSchedulesNoProposals' => $custom_schedules_with_no_proposals,
  557. 'notifications' => $notifications,
  558. 'has_payment_methods' => $hasPaymentMethods,
  559. 'pendingServicePackages' => $pendingServicePackages,
  560. ];
  561. }
  562. public function dadosDashboardPrestador(): array
  563. {
  564. $user = Auth::user();
  565. if ($user->type !== UserTypeEnum::PROVIDER) {
  566. throw new AuthorizationException(__('messages.only_providers_allowed'));
  567. }
  568. $provider = Provider::with('profileMedia')->where('user_id', $user->id)->first();
  569. $headerBar = [
  570. 'rating' => $provider->average_rating,
  571. 'total_ratings' => Review::where('reviews.origin', 'client')->leftJoin('schedules', 'schedules.id', '=', 'reviews.schedule_id')->where('schedules.provider_id', $provider->id)->count(),
  572. 'total_services' => $provider->total_services,
  573. ];
  574. $address = Address::where('source', 'provider')->where('source_id', $provider->id)->with(['city', 'state'])->first();
  575. $summaryInfos = [
  576. 'name' => $user->name,
  577. 'address' => $address,
  578. 'pending_services' => Schedule::where('provider_id', $provider->id)->where('status', 'pending')->count(),
  579. 'profile_photo' => $provider->profileMedia?->path
  580. ? Storage::temporaryUrl($provider->profileMedia->path, now()->addMinutes(60))
  581. : null,
  582. ];
  583. $priceSuggestedAvg = Provider::where('user_id', '!=', $user->id)
  584. ->whereNotNull('daily_price_8h')
  585. ->pluck('daily_price_8h')
  586. ->avg();
  587. $priceActual = $provider->daily_price_8h;
  588. $priceSuggested = [
  589. 'average_price' => $priceSuggestedAvg,
  590. 'your_price' => $priceActual,
  591. ];
  592. $solicitations = Schedule::with([
  593. 'address:district,source_id,source,id,zip_code,latitude,longitude',
  594. 'customSchedule.specialities',
  595. ])
  596. ->where('schedules.provider_id', $provider->id)
  597. ->where('schedules.status', 'pending')
  598. ->leftJoin('clients', 'clients.id', '=', 'schedules.client_id')
  599. ->leftJoin('users as client_user', 'client_user.id', '=', 'clients.user_id')
  600. ->leftJoin('custom_schedules', 'custom_schedules.schedule_id', '=', 'schedules.id')
  601. ->select(
  602. 'schedules.id',
  603. 'schedules.client_id',
  604. 'client_user.name as client_name',
  605. 'clients.average_rating',
  606. 'schedules.date',
  607. DB::raw("
  608. TO_CHAR(schedules.date, 'DD/MM/YYYY') AS formatted_date
  609. "),
  610. 'schedules.start_time',
  611. 'schedules.end_time',
  612. 'schedules.total_amount',
  613. 'schedules.period_type',
  614. 'schedules.schedule_type',
  615. 'schedules.address_id',
  616. 'schedules.status',
  617. 'custom_schedules.offers_meal',
  618. DB::raw("
  619. CASE
  620. WHEN (NOW() - schedules.created_at) < INTERVAL '1 day' THEN
  621. CONCAT(
  622. ROUND(
  623. EXTRACT(
  624. EPOCH FROM (NOW() - schedules.created_at)
  625. ) / 3600
  626. ),
  627. ' hours ago'
  628. )
  629. ELSE
  630. CONCAT(
  631. ROUND(
  632. EXTRACT(
  633. EPOCH FROM (NOW() - schedules.created_at)
  634. ) / 86400
  635. ),
  636. ' days ago'
  637. )
  638. END AS time_since_request
  639. "),
  640. DB::raw("
  641. (
  642. SELECT service_package_items.service_package_id
  643. FROM service_package_items
  644. WHERE service_package_items.schedule_id = schedules.id
  645. LIMIT 1
  646. ) AS service_package_id
  647. "),
  648. )
  649. ->orderBy('schedules.date', 'asc')
  650. ->get()
  651. ->append('specialities')
  652. ->makeHidden('customSchedule');
  653. $solicitations->each(function ($solicitation) use ($address) {
  654. $solicitation->distance_km = $this->zipCodeCoordinatesService->calculateDistance(
  655. $address?->latitude !== null ? (float) $address->latitude : null,
  656. $address?->longitude !== null ? (float) $address->longitude : null,
  657. $address?->zip_code,
  658. $solicitation->address?->latitude !== null ? (float) $solicitation->address->latitude : null,
  659. $solicitation->address?->longitude !== null ? (float) $solicitation->address->longitude : null,
  660. $solicitation->address?->zip_code,
  661. );
  662. });
  663. $todayServices = Schedule::with([
  664. 'address:district,address,number,source_id,source,id',
  665. ])
  666. ->where('schedules.provider_id', $provider->id)
  667. ->whereIn('schedules.status', ['accepted', 'paid', 'started', 'finished'])
  668. ->whereDate('schedules.date', now()->toDateString())
  669. ->leftJoin('clients', 'clients.id', '=', 'schedules.client_id')
  670. ->leftJoin('users as client_user', 'client_user.id', '=', 'clients.user_id')
  671. ->leftJoin('custom_schedules', 'custom_schedules.schedule_id', '=', 'schedules.id')
  672. ->select(
  673. 'schedules.id',
  674. 'schedules.client_id',
  675. 'client_user.name as client_name',
  676. 'schedules.date',
  677. 'schedules.start_time',
  678. 'schedules.end_time',
  679. 'schedules.total_amount',
  680. 'schedules.period_type',
  681. 'schedules.address_id',
  682. 'schedules.schedule_type',
  683. 'schedules.status',
  684. 'schedules.code_verified',
  685. 'schedules.code',
  686. 'custom_schedules.offers_meal',
  687. DB::raw("
  688. EXISTS (
  689. SELECT 1
  690. FROM reviews
  691. WHERE reviews.schedule_id = schedules.id
  692. AND reviews.origin = 'provider'
  693. AND reviews.origin_id = {$provider->id}
  694. AND reviews.deleted_at IS NULL
  695. ) AS provider_reviewed
  696. "),
  697. DB::raw("
  698. (
  699. SELECT service_package_items.service_package_id
  700. FROM service_package_items
  701. WHERE service_package_items.schedule_id = schedules.id
  702. LIMIT 1
  703. ) AS service_package_id
  704. "),
  705. )
  706. ->orderBy('schedules.start_time', 'asc')
  707. ->get();
  708. $nextSchedules = Schedule::with('address:district,address,number,source_id,source,id,zip_code,latitude,longitude')
  709. ->where('schedules.provider_id', $provider->id)
  710. ->whereIn('schedules.status', ['accepted', 'paid'])
  711. ->whereDate('schedules.date', '>=', now()->toDateString())
  712. ->leftJoin('clients', 'clients.id', '=', 'schedules.client_id')
  713. ->leftJoin('users as client_user', 'client_user.id', '=', 'clients.user_id')
  714. ->leftJoin('custom_schedules', 'custom_schedules.schedule_id', '=', 'schedules.id')
  715. ->select(
  716. 'schedules.id',
  717. 'schedules.client_id',
  718. 'client_user.name as client_name',
  719. 'schedules.date',
  720. 'schedules.start_time',
  721. 'schedules.end_time',
  722. 'schedules.total_amount',
  723. 'schedules.period_type',
  724. 'schedules.address_id',
  725. 'schedules.schedule_type',
  726. 'schedules.status',
  727. 'custom_schedules.offers_meal',
  728. DB::raw("
  729. (
  730. SELECT service_package_items.service_package_id
  731. FROM service_package_items
  732. WHERE service_package_items.schedule_id = schedules.id
  733. LIMIT 1
  734. ) AS service_package_id
  735. "),
  736. )
  737. ->orderBy('schedules.date', 'asc')
  738. ->get();
  739. $nextSchedules->each(function ($schedule) use ($address) {
  740. $schedule->distance_km = $this->zipCodeCoordinatesService->calculateDistance(
  741. $address?->latitude !== null ? (float) $address->latitude : null,
  742. $address?->longitude !== null ? (float) $address->longitude : null,
  743. $address?->zip_code,
  744. $schedule->address?->latitude !== null ? (float) $schedule->address->latitude : null,
  745. $schedule->address?->longitude !== null ? (float) $schedule->address->longitude : null,
  746. $schedule->address?->zip_code,
  747. );
  748. });
  749. $clientCollections = collect([
  750. $solicitations,
  751. $todayServices,
  752. $nextSchedules,
  753. ]);
  754. $clientPhotoUrls = $this->clientPhotoUrls(
  755. $clientCollections->flatMap(
  756. fn(Collection $items) => $items->pluck('client_id'),
  757. ),
  758. );
  759. $clientCollections->each(function (Collection $items) use ($clientPhotoUrls) {
  760. $items->each(function ($item) use ($clientPhotoUrls) {
  761. $item->customer_photo = $clientPhotoUrls->get($item->client_id);
  762. });
  763. });
  764. $notifications = Notification::where('user_id', $user->id)
  765. ->orderBy('read', 'asc')
  766. ->orderBy('created_at', 'desc')
  767. ->limit(10)
  768. ->get()
  769. ->map(function ($notification) {
  770. return [
  771. 'id' => $notification->id,
  772. 'title' => $notification->title,
  773. 'description' => $notification->description,
  774. 'time' => $notification->created_at->diffForHumans(),
  775. 'read' => $notification->read,
  776. 'avatar' => '/icons/avatar.svg',
  777. ];
  778. });
  779. $opportunities = $this->customScheduleService->getAvailableOpportunities($provider->id);
  780. $opportunities->each(function ($o) {
  781. $o->customer_photo = $o->client?->profileMedia?->path
  782. ? Storage::temporaryUrl($o->client->profileMedia->path, now()->addMinutes(60))
  783. : null;
  784. });
  785. return [
  786. 'headerBar' => $headerBar,
  787. 'summaryInfos' => $summaryInfos,
  788. 'priceSuggested' => $priceSuggested,
  789. 'solicitations' => $solicitations,
  790. 'todayServices' => $todayServices,
  791. 'nextSchedules' => $nextSchedules,
  792. 'opportunities' => $opportunities,
  793. 'notifications' => $notifications,
  794. ];
  795. }
  796. public function getScheduleClienteDetails(int $scheduleId): array
  797. {
  798. $user = Auth::user();
  799. $cliente = Client::where('user_id', $user->id)->firstOrFail();
  800. $schedule = Schedule::with('customSchedule.specialities')
  801. ->where('schedules.id', $scheduleId)
  802. ->where('schedules.client_id', $cliente->id)
  803. ->leftJoin('providers', 'providers.id', '=', 'schedules.provider_id')
  804. ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
  805. ->leftJoin('custom_schedules', 'custom_schedules.schedule_id', '=', 'schedules.id')
  806. ->select(
  807. 'schedules.id',
  808. 'schedules.provider_id',
  809. 'schedules.schedule_type',
  810. 'provider_user.name as provider_name',
  811. 'providers.birth_date as provider_birth_date',
  812. 'providers.gender',
  813. 'custom_schedules.offers_meal',
  814. )
  815. ->firstOrFail();
  816. $providerPhoto = $this->providerPhotoUrls([$schedule->provider_id])
  817. ->get($schedule->provider_id);
  818. return [
  819. 'schedule_type' => $schedule->schedule_type,
  820. 'provider_name' => $schedule->provider_name,
  821. 'provider_birth_date' => $schedule->provider_birth_date,
  822. 'gender' => $schedule->gender,
  823. 'gender_label' => GenderEnum::labelFor($schedule->gender),
  824. 'offers_meal' => $schedule->offers_meal,
  825. 'specialities' => $schedule->specialities,
  826. 'provider_photo' => $providerPhoto,
  827. ];
  828. }
  829. // gera urls apenas para fotos verificadas ou visiveis ao proprio prestador.
  830. private function providerPhotoUrls(iterable $providerIds): Collection
  831. {
  832. return Provider::query()
  833. ->select('id', 'profile_media_id')
  834. ->with('profileMedia')
  835. ->whereIn('id', collect($providerIds)->filter()->unique())
  836. ->get()
  837. ->mapWithKeys(function (Provider $provider) {
  838. $path = $provider->profileMedia?->path;
  839. return [
  840. $provider->id => $path ? Storage::temporaryUrl($path, now()->addMinutes(60)) : null,
  841. ];
  842. });
  843. }
  844. // Gera URLs apenas para fotos verificadas ou visíveis ao próprio cliente.
  845. private function clientPhotoUrls(iterable $clientIds): Collection
  846. {
  847. return Client::query()
  848. ->select('id', 'profile_media_id')
  849. ->with('profileMedia')
  850. ->whereIn('id', collect($clientIds)->filter()->unique())
  851. ->get()
  852. ->mapWithKeys(function (Client $client) {
  853. $path = $client->profileMedia?->path;
  854. return [
  855. $client->id => $path ? Storage::temporaryUrl($path, now()->addMinutes(60)) : null,
  856. ];
  857. });
  858. }
  859. //
  860. private function applyCreditCardFee(?float $price): ?float
  861. {
  862. if ($price === null) {
  863. return null;
  864. }
  865. $rate = $this->platformCreditCardFeeRate();
  866. return round($price * (1 + $rate), 2);
  867. }
  868. private function platformCreditCardFeeRate(): float
  869. {
  870. $rate = config('services.pagarme.platform_credit_card_fee_rate', 0.16);
  871. if (is_string($rate)) {
  872. $rate = str_replace(',', '.', trim($rate));
  873. }
  874. $rate = (float) $rate;
  875. return $rate > 1 ? $rate / 100 : $rate;
  876. }
  877. //
  878. private function addressForDistance(int $clientId, ?Address $primaryAddress): ?Address
  879. {
  880. if ($primaryAddress?->latitude !== null && $primaryAddress?->longitude !== null) {
  881. return $primaryAddress;
  882. }
  883. return Address::where('source', 'client')
  884. ->where('source_id', $clientId)
  885. ->whereNotNull('latitude')
  886. ->whereNotNull('longitude')
  887. ->orderByDesc('is_primary')
  888. ->orderByDesc('id')
  889. ->first() ?? $primaryAddress;
  890. }
  891. private function distanceSelect(?float $clientLatitude, ?float $clientLongitude): \Illuminate\Contracts\Database\Query\Expression
  892. {
  893. return DistanceService::sqlExpression($clientLatitude, $clientLongitude);
  894. }
  895. }