@php $user = auth()->user(); $profileSummary = app(\App\Support\ProfileCompletion::class)->summary($user); $hasProfile = $profileSummary['missing_required'] === []; $employerOwnedOrLinked = function ($query) use ($user): void { $query ->where('user_id', $user->id) ->orWhereHas('nounouLinks', fn ($query) => $query ->where('user_id', $user->id) ->whereNull('unlinked_at')); }; $hasEmployers = \App\Models\Employer::query() ->whereNull('archived_at') ->where($employerOwnedOrLinked) ->exists(); $hasChildren = \App\Models\ChildRecord::query() ->whereNull('archived_at') ->whereHas('employer', fn ($query) => $query->whereNull('archived_at')->where($employerOwnedOrLinked)) ->exists(); $hasContracts = \App\Models\EmploymentContract::query() ->whereNull('ended_at') ->where('status', '!=', 'draft') ->whereHas('child.employer', fn ($query) => $query->whereNull('archived_at')->where($employerOwnedOrLinked)) ->exists(); $hasActiveContract = \App\Models\EmploymentContract::query() ->where('status', 'active') ->whereHas('child.employer', fn ($query) => $query->whereNull('archived_at')->where($employerOwnedOrLinked)) ->exists(); $onboardingSteps = [ ['number' => '1', 'icon' => 'ph-user-circle', 'icon_class' => 'dashboard-icon-purple', 'title' => 'Mon profil', 'description' => 'Agrément, coordonnées, tarifs et disponibilités.', 'duration' => '4 min', 'done' => $hasProfile, 'locked' => false, 'href' => route('profile.edit')], ['number' => '2', 'icon' => 'ph-users-three', 'icon_class' => 'dashboard-icon-green', 'title' => 'Mes familles', 'description' => 'Les parents employeurs, leurs coordonnées et leurs enfants.', 'duration' => '3 min', 'done' => $hasEmployers, 'locked' => ! $hasProfile, 'href' => route('employers.index')], ['number' => '3', 'icon' => 'ph-file-text', 'icon_class' => 'dashboard-icon-orange', 'title' => 'Mes contrats', 'description' => 'Générés depuis votre profil et vos familles.', 'duration' => '5 min', 'done' => $hasContracts, 'locked' => ! $hasProfile || ! $hasEmployers || ! $hasChildren, 'href' => route('contracts.create')], ]; $configuredSteps = collect($onboardingSteps)->where('done', true)->count(); $totalSteps = count($onboardingSteps); $progressPercent = $totalSteps > 0 ? ($configuredSteps / $totalSteps) * 100 : 0; $firstName = trim($user->first_name ?: (explode(' ', trim($user->name))[0] ?? '')); $firstStepIndex = collect($onboardingSteps)->search(fn ($step) => ! $step['done']); $benefits = [ ['icon' => 'ph-calendar-blank', 'title' => 'Planning automatique', 'description' => 'Vos horaires contractuels remplissent le planning, vous ne saisissez que les écarts.'], ['icon' => 'ph-calculator', 'title' => 'Salaires calculés', 'description' => "Heures normales, majorées, indemnités d'entretien et de repas : le net tombe juste."], ['icon' => 'ph-paper-plane-tilt', 'title' => 'Déclarations prêtes', 'description' => 'Chaque mois, les montants Pajemploi sont préparés et rappelés avant la date limite.'], ['icon' => 'ph-umbrella', 'title' => 'Congés suivis', 'description' => 'Jours acquis, pris et restants, contrat par contrat.'], ]; $todayLabel = ucfirst(now()->locale('fr')->isoFormat('dddd D MMMM')); $dayKeys = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; $dayShortLabels = ['monday' => 'Lun', 'tuesday' => 'Mar', 'wednesday' => 'Mer', 'thursday' => 'Jeu', 'friday' => 'Ven', 'saturday' => 'Sam', 'sunday' => 'Dim']; $planningMode = in_array((string) request('planning_days'), ['7', 'weekend'], true) ? (string) request('planning_days') : '5'; $planningDaysCount = $planningMode === 'weekend' ? 7 : (int) $planningMode; $requestedPlanningWeek = request('planning_week'); try { $parsedPlanningWeek = $requestedPlanningWeek ? \Carbon\Carbon::createFromFormat('!Y-m-d', $requestedPlanningWeek) : null; $planningWeekStart = $parsedPlanningWeek ? $parsedPlanningWeek->copy() : now()->startOfWeek(); } catch (\Throwable) { $planningWeekStart = now()->startOfWeek(); } $planningWeekEnd = $planningWeekStart->copy()->addDays($planningDaysCount - 1)->endOfDay(); $planningWeekLabel = $planningWeekStart->translatedFormat('j').' – '.$planningWeekEnd->translatedFormat('j F Y'); $requestedPlanningDay = request('planning_day'); try { $parsedPlanningDay = $requestedPlanningDay ? \Carbon\Carbon::createFromFormat('!Y-m-d', $requestedPlanningDay) : null; } catch (\Throwable) { $parsedPlanningDay = null; } $previousPlanningWeekUrl = route('dashboard', array_filter([ 'planning_week' => $planningWeekStart->copy()->subWeek()->toDateString(), 'planning_days' => $planningMode, 'revenue_month' => request('revenue_month'), ])); $nextPlanningWeekUrl = route('dashboard', array_filter([ 'planning_week' => $planningWeekStart->copy()->addWeek()->toDateString(), 'planning_days' => $planningMode, 'revenue_month' => request('revenue_month'), ])); $weekJumpBaseUrl = route('dashboard', array_filter([ 'planning_days' => $planningMode, 'revenue_month' => request('revenue_month'), ])); $weekJumpUrlPrefix = $weekJumpBaseUrl.(str_contains($weekJumpBaseUrl, '?') ? '&' : '?').'planning_week='; $fiveDaysPlanningUrl = route('dashboard', array_filter([ 'planning_week' => $planningWeekStart->toDateString(), 'planning_day' => request('planning_day'), 'planning_days' => 5, 'revenue_month' => request('revenue_month'), ])); $sevenDaysPlanningUrl = route('dashboard', array_filter([ 'planning_week' => $planningWeekStart->toDateString(), 'planning_day' => request('planning_day'), 'planning_days' => 7, 'revenue_month' => request('revenue_month'), ])); $weekendPlanningUrl = route('dashboard', array_filter([ 'planning_week' => $planningWeekStart->toDateString(), 'planning_day' => request('planning_day'), 'planning_days' => 'weekend', 'revenue_month' => request('revenue_month'), ])); $planningContractStatuses = ['draft', 'cancelled']; $planningChildren = \App\Models\ChildRecord::query() ->whereNull('archived_at') ->whereHas('employer', function ($query) use ($user): void { $query ->whereNull('archived_at') ->where(function ($query) use ($user): void { $query ->where('user_id', $user->id) ->orWhereHas('nounouLinks', fn ($query) => $query ->where('user_id', $user->id) ->whereNull('unlinked_at')); }); }) ->whereHas('contracts', fn ($query) => $query->whereNotIn('status', $planningContractStatuses)->whereNotNull('weekly_schedule')) ->with(['employer', 'contracts' => fn ($query) => $query->whereNotIn('status', $planningContractStatuses)->whereNotNull('weekly_schedule')->orderByDesc('started_on')]) ->orderBy('first_name') ->get(); $planningOverrides = \App\Models\PlanningOverride::query() ->whereIn('child_record_id', $planningChildren->pluck('id')) ->whereBetween('date', [$planningWeekStart->toDateString(), $planningWeekEnd->toDateString()]) ->get() ->keyBy(fn ($override): string => $override->date->toDateString().'|'.$override->child_record_id); $planningLeaves = \App\Models\LeavePeriod::query() ->where('user_id', $user->id) ->whereDate('started_on', '<=', $planningWeekEnd->toDateString()) ->whereDate('ended_on', '>=', $planningWeekStart->toDateString()) ->with('contracts') ->get(); $planningActivitiesByDate = \App\Models\PlanningActivity::query() ->where('user_id', $user->id) ->whereBetween('date', [$planningWeekStart->toDateString(), $planningWeekEnd->toDateString()]) ->orderBy('time') ->get() ->groupBy(fn ($activity): string => $activity->date->toDateString()); $planningHolidaysByDate = collect(\App\Support\FrenchPublicHolidays::between($planningWeekStart, $planningWeekEnd)) ->keyBy('date'); $planningSlotMinutes = function (?string $start, ?string $end): int { if (! $start || ! $end) { return 0; } return abs(\Carbon\Carbon::parse($end)->diffInMinutes(\Carbon\Carbon::parse($start))); }; $planningDayEntries = function (\Carbon\Carbon $date) use ($planningChildren, $planningOverrides, $dayKeys, $planningSlotMinutes) { return $planningChildren ->map(function ($child) use ($date, $planningOverrides, $dayKeys, $planningSlotMinutes) { $contract = $child->contracts->first(function ($contract) use ($date): bool { $effectiveEnd = collect([ $contract->ended_at?->toDateString(), $contract->ends_on?->toDateString(), ])->filter()->min(); return (! $contract->started_on || $contract->started_on->lte($date)) && ($effectiveEnd === null || $date->toDateString() <= $effectiveEnd); }); if (! $contract) { return null; } $override = $planningOverrides->get($date->toDateString().'|'.$child->id); $isAbsent = $override?->status === 'absent'; $schedule = $contract->effectiveWeeklySchedule(); $dayKey = $dayKeys[$date->dayOfWeekIso - 1]; $plannedSlot = $schedule[$dayKey] ?? null; if (! $override && ! ($plannedSlot['enabled'] ?? false)) { return null; } $start = $override?->actual_start_time ? substr($override->actual_start_time, 0, 5) : ($override?->start_time ? substr($override->start_time, 0, 5) : ($plannedSlot['start'] ?? null)); $end = $override?->actual_end_time ? substr($override->actual_end_time, 0, 5) : ($override?->end_time ? substr($override->end_time, 0, 5) : ($plannedSlot['end'] ?? null)); $minutes = $planningSlotMinutes($start, $end); if (! $isAbsent && $minutes <= 0) { return null; } return [ 'initials' => mb_strtoupper(mb_substr($child->first_name, 0, 1).mb_substr((string) $child->last_name, 0, 1)), 'color' => \App\Support\ChildColorPalette::key($child), 'child' => $child->first_name ?: 'Enfant', 'child_last_name' => $child->last_name, 'family' => 'Famille '.$child->employer->family_label, 'date' => $date->toDateString(), 'modal_id' => 'slot-'.$date->format('Ymd').'-'.$child->id, 'start' => $start, 'end' => $end, 'planned_start' => $plannedSlot['start'] ?? $start, 'planned_end' => $plannedSlot['end'] ?? $end, 'hours' => $minutes > 0 ? intdiv($minutes, 60).'h'.str_pad((string) ($minutes % 60), 2, '0', STR_PAD_LEFT) : null, 'minutes' => $isAbsent ? 0 : $minutes, 'is_absent' => $isAbsent, 'status_label' => $isAbsent ? ('Absence '.($override->reason === 'injust' ? 'injustifiée' : 'justifiée')) : ($contract->status === 'to_sign' ? 'En attente de signature' : 'Confirmé'), 'status_class' => $isAbsent ? 'dashboard-week-status-locked' : ($contract->status === 'to_sign' ? 'dashboard-week-status-pending' : ''), 'status_icon' => $isAbsent ? 'ph-eye-slash' : ($contract->status === 'to_sign' ? 'ph-hourglass' : 'ph-check-circle'), 'can_adjust' => true, 'update_url' => route('dashboard.planning.overrides.update', [$date->toDateString(), $child]), ]; }) ->filter() ->sortBy('start') ->values(); }; $planningWeekDays = collect(\Carbon\CarbonPeriod::create($planningWeekStart, $planningWeekEnd)) ->map(function ($date) use ($dayShortLabels, $dayKeys, $planningDayEntries, $planningMode, $planningActivitiesByDate, $planningHolidaysByDate, $planningLeaves) { $dayLeave = \App\Models\LeavePeriod::coveringDate($planningLeaves, $date); $entries = $dayLeave ? collect() : $planningDayEntries($date); $minutes = $entries->sum('minutes'); return [ 'date' => $date->copy(), 'label' => mb_strtoupper($dayShortLabels[$dayKeys[$date->dayOfWeekIso - 1]]).'.', 'day_number' => $date->format('d'), 'is_selected' => false, 'is_weekend' => $date->isWeekend(), 'entries' => $entries, 'activities' => $dayLeave ? collect() : $planningActivitiesByDate->get($date->toDateString(), collect()), 'holiday' => $planningHolidaysByDate->get($date->toDateString()), 'leave' => $dayLeave ? [ 'id' => $dayLeave->id, 'label' => \App\Models\LeavePeriod::TYPE_LABELS[$dayLeave->leave_type] ?? 'Absence', 'class' => \App\Models\LeavePeriod::TYPE_CSS_CLASSES[$dayLeave->leave_type] ?? 'planning-allday-pill--leave-other', 'icon' => \App\Models\LeavePeriod::TYPE_ICONS[$dayLeave->leave_type] ?? 'ph ph-island', 'url' => route('planning.index').'?leave='.$dayLeave->id, ] : null, 'minutes' => $minutes, 'hours' => intdiv($minutes, 60).'h'.($minutes % 60 ? str_pad((string) ($minutes % 60), 2, '0', STR_PAD_LEFT) : ''), 'url' => route('dashboard', array_filter([ 'planning_week' => $date->copy()->startOfWeek()->toDateString(), 'planning_day' => $date->toDateString(), 'planning_days' => $planningMode, 'revenue_month' => request('revenue_month'), ])), ]; }) ->when($planningMode === 'weekend', fn ($days) => $days->filter(fn ($day) => $day['date']->isWeekend())->values()); $planningWeekTotalMinutes = (int) $planningWeekDays->sum('minutes'); $planningWeekTotalLabel = intdiv($planningWeekTotalMinutes, 60).'h'.($planningWeekTotalMinutes % 60 ? str_pad((string) ($planningWeekTotalMinutes % 60), 2, '0', STR_PAD_LEFT) : '').' prévues'; $selectedPlanningDate = $parsedPlanningDay && $parsedPlanningDay->betweenIncluded($planningWeekStart, $planningWeekEnd) && ($planningMode !== 'weekend' || $parsedPlanningDay->isWeekend()) ? $parsedPlanningDay : ($planningWeekDays->first(fn ($day) => $day['entries']->isNotEmpty())['date'] ?? ($planningMode === 'weekend' ? $planningWeekStart->copy()->addDays(5) : (now()->betweenIncluded($planningWeekStart, $planningWeekEnd) ? now()->startOfDay() : $planningWeekStart->copy()))); $planningWeekDays = $planningWeekDays ->map(function ($day) use ($selectedPlanningDate) { $day['is_selected'] = $day['date']->isSameDay($selectedPlanningDate); return $day; }) ->values(); $selectedPlanningDay = $planningWeekDays->firstWhere('is_selected', true) ?? $planningWeekDays->first(); $requestedRevenueMonth = request('revenue_month'); try { $parsedRevenueMonth = $requestedRevenueMonth ? \Carbon\Carbon::createFromFormat('!Y-m', $requestedRevenueMonth) : null; $revenueMonth = $parsedRevenueMonth ? $parsedRevenueMonth->startOfMonth() : now()->startOfMonth(); } catch (\Throwable) { $revenueMonth = now()->startOfMonth(); } $revenueMonthEnd = $revenueMonth->copy()->endOfMonth(); $revenueMonthLabel = ucfirst($revenueMonth->copy()->locale('fr')->isoFormat('MMMM YYYY')); $isRevenueProjection = $revenueMonth->greaterThan(now()->startOfMonth()); $previousRevenueMonthUrl = route('dashboard', ['revenue_month' => $revenueMonth->copy()->subMonth()->format('Y-m')]); $nextRevenueMonthUrl = route('dashboard', ['revenue_month' => $revenueMonth->copy()->addMonth()->format('Y-m')]); $timeMinutes = function (?string $time): ?int { if (! $time) { return null; } [$hours, $minutes] = array_map('intval', array_slice(explode(':', $time), 0, 2)); return ($hours * 60) + $minutes; }; $slotMinutes = function (?string $start, ?string $end) use ($timeMinutes): int { $startMinutes = $timeMinutes($start); $endMinutes = $timeMinutes($end); return $startMinutes !== null && $endMinutes !== null && $endMinutes > $startMinutes ? $endMinutes - $startMinutes : 0; }; $formatHours = fn (int $minutes): string => intdiv($minutes, 60).'h'.str_pad((string) ($minutes % 60), 2, '0', STR_PAD_LEFT); $formatMoney = fn (float $amount): string => number_format($amount, 2, ',', ' ').' €'; $contractStatusesForRevenue = ['draft', 'cancelled']; $revenueChildren = \App\Models\ChildRecord::query() ->whereNull('archived_at') ->whereHas('employer', function ($query) use ($user): void { $query ->whereNull('archived_at') ->where(function ($query) use ($user): void { $query ->where('user_id', $user->id) ->orWhereHas('nounouLinks', fn ($query) => $query ->where('user_id', $user->id) ->whereNull('unlinked_at')); }); }) ->whereHas('contracts', function ($query) use ($contractStatusesForRevenue, $revenueMonth, $revenueMonthEnd): void { $query ->where(function ($query) use ($contractStatusesForRevenue, $revenueMonth): void { $query->whereNotIn('status', $contractStatusesForRevenue) ->orWhere(function ($query) use ($revenueMonth): void { $query->where('status', 'draft')->whereExists(function ($declarations) use ($revenueMonth): void { $declarations->selectRaw('1')->from('imported_monthly_declarations') ->whereColumn('imported_monthly_declarations.child_record_id', 'contracts.child_record_id') ->whereDate('month', $revenueMonth->toDateString()); }); }); }) ->whereNotNull('started_on') ->whereDate('started_on', '<=', $revenueMonthEnd) ->where(function ($query) use ($revenueMonth): void { $query->whereNull('ended_at')->orWhereDate('ended_at', '>=', $revenueMonth); }) ->where(function ($query) use ($revenueMonth): void { $query->whereNull('ends_on')->orWhereDate('ends_on', '>=', $revenueMonth); }); }) ->with(['employer', 'contracts' => function ($query) use ($contractStatusesForRevenue, $revenueMonth, $revenueMonthEnd): void { $query ->where(function ($query) use ($contractStatusesForRevenue, $revenueMonth): void { $query->whereNotIn('status', $contractStatusesForRevenue) ->orWhere(function ($query) use ($revenueMonth): void { $query->where('status', 'draft')->whereExists(function ($declarations) use ($revenueMonth): void { $declarations->selectRaw('1')->from('imported_monthly_declarations') ->whereColumn('imported_monthly_declarations.child_record_id', 'contracts.child_record_id') ->whereDate('month', $revenueMonth->toDateString()); }); }); }) ->whereNotNull('started_on') ->whereDate('started_on', '<=', $revenueMonthEnd) ->where(function ($query) use ($revenueMonth): void { $query->whereNull('ended_at')->orWhereDate('ended_at', '>=', $revenueMonth); }) ->where(function ($query) use ($revenueMonth): void { $query->whereNull('ends_on')->orWhereDate('ends_on', '>=', $revenueMonth); }) ->orderByDesc('started_on'); }]) ->orderBy('first_name') ->get(); $revenueOverrides = \App\Models\PlanningOverride::query() ->whereIn('child_record_id', $revenueChildren->pluck('id')) ->whereBetween('date', [$revenueMonth->toDateString(), $revenueMonthEnd->toDateString()]) ->get() ->keyBy(fn ($override): string => $override->date->toDateString().'|'.$override->child_record_id); $revenueLeaves = \App\Models\LeavePeriod::query() ->where('user_id', $user->id) ->whereDate('started_on', '<=', $revenueMonthEnd->toDateString()) ->whereDate('ended_on', '>=', $revenueMonth->toDateString()) ->with('contracts') ->get(); $revenueMonthKey = $revenueMonth->format('Y-m'); $revenueRows = $revenueChildren ->map(function ($child) use ($dayKeys, $revenueMonth, $revenueMonthEnd, $revenueMonthKey, $revenueMonthLabel, $revenueOverrides, $revenueLeaves, $slotMinutes, $formatHours, $formatMoney) { $minutes = 0; $days = 0; $maintenanceTotal = 0.0; $maintenanceDays = 0; $mealTotal = 0.0; $mealDays = 0; $snackTotal = 0.0; $snackDays = 0; $travelTotal = 0.0; $travelKmSum = 0.0; $dayRecords = []; $currentContract = null; foreach (\Carbon\CarbonPeriod::create($revenueMonth, $revenueMonthEnd) as $date) { $contract = $child->contracts->first(function ($contract) use ($date): bool { $effectiveEnd = collect([ $contract->ended_at?->toDateString(), $contract->ends_on?->toDateString(), ])->filter()->min(); return $contract->started_on && $contract->started_on->lte($date) && ($effectiveEnd === null || $date->toDateString() <= $effectiveEnd); }); if (! $contract) { continue; } $currentContract = $contract; $dayKey = $dayKeys[$date->dayOfWeekIso - 1]; $override = $revenueOverrides->get($date->toDateString().'|'.$child->id); $leave = \App\Models\LeavePeriod::coveringDate($revenueLeaves, $date); if ($override?->status === 'absent' || $leave !== null) { if ($leave !== null) { $missedSlot = $contract->effectiveWeeklySchedule()[$dayKey] ?? null; $missedMinutes = ($missedSlot['enabled'] ?? false) ? $slotMinutes($missedSlot['start'] ?? null, $missedSlot['end'] ?? null) : 0; $dayRecords[] = ['date' => \Carbon\CarbonImmutable::instance($date), 'minutes' => 0, 'amount' => 0.0, 'leaveType' => $leave->leave_type, 'missedMinutes' => $missedMinutes]; } continue; } $schedule = $contract->effectiveWeeklySchedule(); $plannedSlot = $schedule[$dayKey] ?? null; $plannedStart = $override?->start_time ? substr($override->start_time, 0, 5) : ($plannedSlot['start'] ?? null); $plannedEnd = $override?->end_time ? substr($override->end_time, 0, 5) : ($plannedSlot['end'] ?? null); $actualStart = $override?->actual_start_time ? substr($override->actual_start_time, 0, 5) : null; $actualEnd = $override?->actual_end_time ? substr($override->actual_end_time, 0, 5) : null; $dayMinutes = $slotMinutes($actualStart ?: $plannedStart, $actualEnd ?: $plannedEnd); if ($dayMinutes <= 0 || (! $override && ! ($plannedSlot['enabled'] ?? false))) { continue; } $dayAmount = ($dayMinutes / 60) * (float) $contract->hourly_rate_net; $days++; $minutes += $dayMinutes; if ((float) $contract->maintenance_indemnity > 0) { $maintenanceTotal += (float) $contract->maintenance_indemnity; $maintenanceDays++; } if ($contract->meal_provider === 'assistant') { $mealTotal += (float) $contract->meal_indemnity; $mealDays++; } if ($contract->snack_provider === 'assistant') { $snackTotal += (float) $contract->snack_indemnity; $snackDays++; } $dayTravelKm = (float) ($override?->travel_km ?? 0); if ($dayTravelKm > 0 && (float) $contract->travel_expense_per_km > 0) { $travelKmSum += $dayTravelKm; $travelTotal += $dayTravelKm * (float) $contract->travel_expense_per_km; } $dayRecords[] = ['date' => \Carbon\CarbonImmutable::instance($date), 'minutes' => $dayMinutes, 'amount' => $dayAmount, 'leaveType' => null]; } $indemnitiesRaw = [ 'maintenance' => $maintenanceTotal > 0 ? ['days' => $maintenanceDays, 'total' => $maintenanceTotal] : null, 'meal' => $mealTotal > 0 ? ['days' => $mealDays, 'total' => $mealTotal] : null, 'snack' => $snackTotal > 0 ? ['days' => $snackDays, 'total' => $snackTotal] : null, 'travel' => $travelTotal > 0 ? ['km' => $travelKmSum, 'total' => $travelTotal] : null, ]; $indemnities = $maintenanceTotal + $mealTotal + $snackTotal + $travelTotal; if ($days === 0 && $minutes === 0 && $indemnities === 0.0 && ! ($currentContract && \App\Support\MonthlyCareCostCalculator::hasImportedMonth($currentContract, $revenueMonth))) { return null; } $cpCompensation = 0.0; $cpDaysPaid = 0.0; $absenceDeduction = 0.0; $leaveSummaries = []; if ($currentContract) { $cpService = new \App\Domain\Leave\PaidLeavePaymentService; foreach (\App\Models\PaidLeavePeriod::query()->where('contract_id', $currentContract->id)->get() as $plp) { $cpCompensation += $cpService->compensationDueForMonth( $currentContract, $plp, \Carbon\CarbonImmutable::instance($revenueMonth), ); } $contractLeaveRows = \Illuminate\Support\Facades\DB::table('leave_period_contract') ->join('leave_periods', 'leave_periods.id', '=', 'leave_period_contract.leave_period_id') ->where('leave_period_contract.contract_id', $currentContract->id) ->whereDate('leave_periods.started_on', '<=', $revenueMonthEnd->toDateString()) ->whereDate('leave_periods.ended_on', '>=', $revenueMonth->toDateString()) ->select([ 'leave_periods.started_on', 'leave_periods.ended_on', 'leave_periods.leave_type', 'leave_period_contract.paid_leave_days_used', 'leave_period_contract.unpaid_annual_complementary_days', 'leave_period_contract.selected_valuation_method', 'leave_period_contract.paid_leave_compensation', 'leave_period_contract.salary_deduction_by_month', ]) ->get(); foreach ($contractLeaveRows as $row) { $deductionMap = $row->salary_deduction_by_month ? json_decode($row->salary_deduction_by_month, true) : []; $deductionThisMonth = (float) ($deductionMap[$revenueMonthKey] ?? 0); $absenceDeduction += $deductionThisMonth; $cpDaysPaid += (float) $row->paid_leave_days_used; $leaveSummaries[] = [ 'type' => \App\Models\LeavePeriod::TYPE_LABELS[$row->leave_type] ?? $row->leave_type, 'period' => \Carbon\Carbon::parse($row->started_on)->translatedFormat('d/m').' → '.\Carbon\Carbon::parse($row->ended_on)->translatedFormat('d/m'), 'paidDays' => (float) $row->paid_leave_days_used, 'unpaidDays' => (float) $row->unpaid_annual_complementary_days, 'compensation' => (float) $row->paid_leave_compensation > 0 ? $formatMoney((float) $row->paid_leave_compensation) : null, 'deductionThisMonth' => $deductionThisMonth > 0 ? $formatMoney($deductionThisMonth) : null, ]; } } $cpDaysRemaining = $cpDaysPaid; foreach ($dayRecords as $i => $record) { if (($record['leaveType'] ?? null) !== 'annual') { continue; } if ($cpDaysRemaining >= 1) { $cpDaysRemaining -= 1; } else { $dayRecords[$i]['leaveType'] = 'unpaid'; } } $monthlyDetail = \App\Support\MonthlyCareCostCalculator::build( $dayRecords, $revenueMonth, $revenueMonthEnd, $currentContract, $indemnitiesRaw, $cpCompensation, $absenceDeduction, $child->contracts, ); $cpCompensation = $monthlyDetail['cpCompensationRaw']; $absenceDeduction = $monthlyDetail['absenceDeductionRaw']; if ($monthlyDetail['importedDays'] !== null) { $cpDaysPaid = 0; $leaveSummaries = []; } $minutes = $monthlyDetail['totalMinutes']; $days = $monthlyDetail['importedDays'] ?? $days; $indemnitiesRaw = $monthlyDetail['indemnitiesRaw']; $indemnities = array_sum(array_map(fn ($row) => $row['total'] ?? 0, $indemnitiesRaw)) + $monthlyDetail['terminationIndemnityRaw'] + $monthlyDetail['precarityIndemnityRaw']; $salary = $monthlyDetail['cost']['totalHoursAmountRaw'] + $cpCompensation; $total = $monthlyDetail['cost']['netToPayRaw']; return [ 'initials' => mb_strtoupper(mb_substr($child->first_name, 0, 1).mb_substr((string) $child->last_name, 0, 1)), 'color' => \App\Support\ChildColorPalette::key($child), 'child' => $child->first_name ?: 'Enfant', 'family' => 'Famille '.$child->employer->family_label, 'hours' => $formatHours($minutes), 'days' => $days, 'salary' => $formatMoney($salary), 'indemnities' => $formatMoney($indemnities), 'total' => $formatMoney($total), 'minutes' => $minutes, 'salary_raw' => $salary, 'indemnities_raw' => $indemnities, 'total_raw' => $total, 'details' => array_merge($monthlyDetail, [ 'initials' => mb_strtoupper(mb_substr($child->first_name, 0, 1).mb_substr((string) $child->last_name, 0, 1)), 'color' => \App\Support\ChildColorPalette::key($child), 'child' => $child->first_name ?: 'Enfant', 'family' => 'Famille '.$child->employer->family_label, 'month' => mb_strtolower($revenueMonthLabel), 'pajemploiNumber' => $child->employer->primary_pajemploi_number, 'hourlyRate' => $currentContract ? $formatMoney((float) $currentContract->hourly_rate_net).'/h' : null, 'contractUrl' => $currentContract ? route('contracts.show', $currentContract) : null, 'cpDaysPaid' => $cpDaysPaid > 0 ? $cpDaysPaid : null, 'cpCompensation' => $cpCompensation > 0 ? $formatMoney($cpCompensation) : null, 'absenceDeduction' => $absenceDeduction > 0 ? $formatMoney($absenceDeduction) : null, 'leaves' => $leaveSummaries, ]), ]; }) ->filter() ->values(); $revenueTotalMinutes = (int) $revenueRows->sum('minutes'); $revenueTotalDays = (int) $revenueRows->sum('days'); $revenueTotalSalary = (float) $revenueRows->sum('salary_raw'); $revenueTotalIndemnities = (float) $revenueRows->sum('indemnities_raw'); $revenueTotal = (float) $revenueRows->sum('total_raw'); @endphp
@if ($configuredSteps < $totalSteps && $revenueRows->isEmpty()) @if (! $hasProfile)

Mon espace personnel

Bienvenue dans naneasy{{ $firstName !== '' ? ', '.$firstName : '' }}

Trois étapes, une quinzaine de minutes : ensuite vos contrats, votre planning et vos déclarations se remplissent presque tout seuls.

Commencer par mon profil ≈ 4 min pour la première étape
@else

Mon espace personnel

{{ $firstName !== '' ? $firstName.', ' : '' }} le premier mois est offert

Aucun moyen de paiement à renseigner : je profite d'un mois complet, gratuit et automatique, pour tester tous les avantages de naneasy avant de préparer mon premier contrat.

@endif

Ma configuration

{{ $configuredSteps }} / {{ $totalSteps }}
@foreach ($onboardingSteps as $index => $step) @php $stepCardClasses = ['dashboard-step-card', 'dashboard-step-card-success' => $step['done'], 'dashboard-step-card-current' => ! $step['done'] && $index === $firstStepIndex, 'dashboard-step-card-locked' => $step['locked']]; $stepCta = $step['done'] ? 'Terminé' : ($step['locked'] ? 'À déverrouiller' : (($index === $firstStepIndex) ? 'Commencer' : 'À suivre')); $stepIcon = $step['done'] ? 'ph-check-circle' : ($step['locked'] ? 'ph-lock-simple' : $step['icon']); @endphp @if ($step['locked']) @else @endif

{{ $step['title'] }}

{{ $step['number'] }}/{{ $totalSteps }}

{{ $step['description'] }}

! $step['done'] && $index === $firstStepIndex])> {{ $stepCta }} · {{ $step['duration'] }}
@if ($step['locked'])
@else @endif @endforeach

Ce que naneasy fera pour vous ensuite

@foreach ($benefits as $benefit)
{{ $benefit['title'] }}

{{ $benefit['description'] }}

@endforeach
@else @if ($hasActiveContract || $revenueRows->isNotEmpty())

Salut {{ $firstName !== '' ? $firstName : $user->name }}

{{ $todayLabel }}

@else

Mon espace personnel

{{ $firstName !== '' ? $firstName.', ' : '' }} le premier mois est offert

Aucun moyen de paiement à renseigner : je profite d'un mois complet, gratuit et automatique, pour tester tous les avantages de naneasy avant de préparer mon premier contrat.

@endif
@if ($hasActiveContract || $revenueRows->isNotEmpty())
$planningMode === '5', 'dashboard-week-grid-weekend' => $planningMode === 'weekend'])> @foreach ($planningWeekDays as $day) $day['is_selected'], 'dashboard-week-day-muted' => $day['entries']->isEmpty()])>
{{ $day['label'] }} {{ $day['day_number'] }}
@if ($day['leave'])
Absence · {{ $day['leave']['label'] }}
@else @forelse ($day['entries']->take(3) as $entry)
$entry['is_absent']])> {{ $entry['child'] }} {{ $entry['is_absent'] ? 'Absent(e)' : $entry['start'].' – '.$entry['end'] }}
@empty {{ $day['is_weekend'] ? 'Week-end' : 'Aucun accueil' }} @endforelse @endif
@if ($day['holiday'] || $day['activities']->isNotEmpty())
@if ($day['holiday'])
{{ $day['holiday']['label'] }} Jour férié
@endif @foreach ($day['activities']->take(2) as $activity)
{{ $activity->label }} {{ $activity->place ?: ($activity->time ? substr($activity->time, 0, 5) : 'Activité prévue') }}
@endforeach
@endif
@endforeach
@if ($selectedPlanningDay['leave'])
Absence · {{ $selectedPlanningDay['leave']['label'] }}

Je suis en congé le {{ ucfirst($selectedPlanningDay['date']->locale('fr')->isoFormat('dddd D MMMM')) }}. Aucune garde n'est prévue.

Voir ou modifier l'absence
@else

{{ ucfirst($selectedPlanningDay['date']->locale('fr')->isoFormat('dddd D MMMM')) }}

{{ $selectedPlanningDay['entries']->count() }} {{ $selectedPlanningDay['entries']->count() > 1 ? 'gardes prévues' : 'garde prévue' }}
@forelse ($selectedPlanningDay['entries'] as $entry)
$entry['is_absent']]) role="button" tabindex="0" x-on:click="openSlot = @js($entry['modal_id'])" x-on:keydown.enter.prevent="openSlot = @js($entry['modal_id'])" x-on:keydown.space.prevent="openSlot = @js($entry['modal_id'])">
{{ $entry['child'] }} {{ $entry['family'] }}
Horaires {{ $entry['is_absent'] ? '—' : $entry['start'].' – '.$entry['end'] }}
Durée {{ $entry['is_absent'] ? '—' : $entry['hours'] }}
{{ $entry['status_label'] }}
@include('planning.partials.slot-modal', [ 'entry' => $entry, 'dateLabel' => ucfirst($selectedPlanningDay['date']->locale('fr')->isoFormat('dddd D MMMM')), 'closeAction' => 'openSlot = null', ])
@empty

Aucune garde prévue ce jour-là.

@endforelse
@endif

Mes revenus

{{ $revenueMonthLabel }} · {{ $isRevenueProjection ? 'estimation' : 'réalisé et prévisionnel' }}
{{ $revenueMonthLabel }}
Heures {{ $formatHours($revenueTotalMinutes) }}
Jours d'accueil {{ $revenueTotalDays }}
Salaire net {{ $formatMoney($revenueTotalSalary) }}
Total à percevoir {{ $formatMoney($revenueTotal) }}
@if ($revenueRows->isNotEmpty())
Enfant Heures Jours Salaire net Indemnités Total
@endif @forelse ($revenueRows as $row)
{{ $row['child'] }} {{ $row['family'] }}
{{ $row['hours'] }} {{ $row['days'] }} j {{ $row['salary'] }} {{ $row['indemnities'] }} {{ $row['total'] }}
@empty

Aucun revenu calculable pour ce mois.

@endforelse
@else

Votre calendrier de garde arrive bientôt

Le calendrier de garde et la projection de salaire s'afficheront ici dès qu'un contrat aura été validé.

@endif
@endif