CaiWuXiTong/modules/EcommerceFinance/View/DingWorklogStatisticsPageData.php

231 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Modules\EcommerceFinance\View;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Modules\EcommerceFinance\Repositories\DingWorklogMirrorRepository;
use Modules\EcommerceFinance\Services\DingWorklogPenaltyCalculator;
use Throwable;
class DingWorklogStatisticsPageData
{
public function __construct(
private readonly DingWorklogMirrorRepository $repository,
private readonly DingWorklogPenaltyCalculator $penaltyCalculator,
) {}
public function page(Request $request): array
{
$timezone = (string) config(
'dingtalk.worklog.timezone',
config('app.platform_timezone', config('app.timezone', 'Asia/Shanghai')),
);
$now = now($timezone);
$latestStatisticsDate = $now->hour < 12
? $now->copy()->subDays(2)->startOfDay()
: $now->copy()->subDay()->startOfDay();
$from = $this->date($request->query('from'), $timezone)
?? $latestStatisticsDate->copy()->subDays(13);
$to = $this->date($request->query('to'), $timezone) ?? $latestStatisticsDate->copy();
if ($from->greaterThan($to)) {
[$from, $to] = [$to, $from];
}
if ($from->diffInDays($to) > 90) {
$from = $to->copy()->subDays(90);
}
$state = $this->repository->available() ? $this->repository->state() : null;
$errorCode = trim((string) ($state['last_error_code'] ?? ''));
$syncState = match (true) {
! $this->repository->available() => 'unavailable',
$errorCode !== '' => 'failed',
$state === null => 'pending',
($state['status'] ?? '') === 'running' => 'syncing',
($state['status'] ?? '') === 'succeeded' => 'succeeded',
default => 'pending',
};
$permissionRequired = DingWorklogMirrorRepository::isPermissionError($errorCode);
$daily = [];
$selectedDate = $this->date($request->query('date'), $timezone)?->toDateString();
$showDetails = $request->boolean('details');
$showLateDetails = $request->boolean('late');
$submissions = [];
$lateSubmissions = [];
$penalty = [
'start_date' => null,
'end_date' => null,
'total_points' => '0',
'total_points_formatted' => '0',
'late_submission_counts' => [],
'late_submissions' => [],
'members' => [],
];
$penaltyError = null;
if ($this->repository->available()) {
try {
$daily = $this->repository->dailySummary($from->toDateString(), $to->toDateString(), false);
$selectedDate ??= $daily[0]['statistics_date'] ?? $latestStatisticsDate->toDateString();
if ($showDetails) {
$submissions = array_map(static fn (object $row): array => [
'report_id' => (string) $row->report_id,
'template_name' => (string) ($row->template_name ?? ''),
'creator_userid' => (string) ($row->creator_userid ?? ''),
'creator_name' => (string) ($row->creator_name ?? ''),
'submitted_at' => (string) $row->submitted_at,
'statistics_date' => (string) $row->statistics_date,
], $this->repository->submissionsForDate($selectedDate));
}
} catch (Throwable $exception) {
report($exception);
$syncState = 'failed';
$errorCode = 'dingtalk_worklog_read_failed';
}
try {
$penalty = $this->penaltyCalculator->summary(
$from->toDateString(),
$to->toDateString(),
$now,
);
} catch (Throwable $exception) {
report($exception);
$penaltyError = match ($exception->getMessage()) {
'dingtalk_attendance_mirror_not_ready' => '钉钉考勤镜像尚未完成首次同步,扣分暂不计算。',
'dingtalk_attendance_schedule_not_ready' => '钉钉排班镜像尚未完成首次同步,无法判断公司工作日,扣分暂不计算。',
default => '日志漏写扣分暂时无法计算。',
};
}
if ($penaltyError === null && array_key_exists('late_submission_counts', $penalty)) {
$lateSubmissionCounts = $penalty['late_submission_counts'];
$daily = array_map(static fn (array $day): array => [
...$day,
'late_submission_count' => $lateSubmissionCounts[$day['statistics_date']] ?? 0,
], $daily);
}
if ($showLateDetails) {
$lateRows = $penaltyError === null ? ($penalty['late_submissions'] ?? []) : [];
$lateRows = array_values(array_filter(
$lateRows,
static fn (object $row): bool => (string) ($row->late_for_date ?? '') === $selectedDate,
));
$deadline = Carbon::parse($selectedDate, $timezone)->addDay()->setTime(12, 0);
$lateSubmissions = array_map(function (object $row) use ($deadline, $selectedDate, $timezone): array {
$submittedAt = DingWorklogMirrorRepository::submittedAt((string) $row->submitted_at);
$lateSeconds = max(0, (int) $deadline->diffInSeconds($submittedAt, false));
$duration = $this->formatDuration($lateSeconds);
return [
'report_id' => (string) $row->report_id,
'template_name' => (string) ($row->template_name ?? ''),
'creator_userid' => (string) ($row->creator_userid ?? ''),
'creator_name' => (string) ($row->creator_name ?? ''),
'submitted_at' => $submittedAt->timezone($timezone)->toDateTimeString(),
'statistics_date' => (string) $row->statistics_date,
'late_for_date' => (string) $row->late_for_date,
'deadline_at' => $deadline->toDateTimeString(),
'late_seconds' => $lateSeconds,
'late_duration' => $duration,
'reason' => sprintf(
'实际提交时间晚于截止时间 %s;该日志归入 %s 统计日,不能补记为 %s 的按时日志。扣分仍按排班和请假规则判断。',
$duration,
(string) $row->statistics_date,
$selectedDate,
),
];
}, $lateRows);
}
}
$currentDate = $latestStatisticsDate->toDateString();
$current = collect($daily)->firstWhere('statistics_date', $currentDate) ?? [
'statistics_date' => $currentDate,
'submission_count' => 0,
'submitter_count' => 0,
'late_submission_count' => 0,
'first_submitted_at' => null,
'last_submitted_at' => null,
];
return [
'view' => 'business.pages.other.log-statistics',
'worklogFilters' => [
'from' => $from->toDateString(),
'to' => $to->toDateString(),
'date' => $selectedDate,
],
'worklogState' => [
'state' => $syncState,
'error_code' => $errorCode ?: null,
'permission_required' => $permissionRequired,
'permission_apply_url' => $permissionRequired ? $this->repository->permissionApplyUrl() : null,
'status' => $state['status'] ?? null,
'last_attempted_at' => $state['last_attempted_at'] ?? null,
'last_succeeded_at' => $state['last_succeeded_at'] ?? null,
'mirror_row_count' => (int) ($state['mirror_row_count'] ?? 0),
],
'worklogCurrent' => $current,
'worklogDaily' => $daily,
'worklogSubmissions' => $submissions,
'worklogShowDetails' => $showDetails,
'worklogLateSubmissions' => $lateSubmissions,
'worklogShowLateDetails' => $showLateDetails,
'worklogPenalty' => $penalty,
'worklogPenaltyError' => $penaltyError,
'worklogDays' => (int) $from->diffInDays($to) + 1,
'worklogTotalSubmissions' => array_sum(array_column($daily, 'submission_count')),
'worklogTotalLateSubmissions' => array_sum(array_column($daily, 'late_submission_count')),
'worklogTemplateName' => (string) config('dingtalk.worklog.display_name', '财务日报 / 日志'),
'worklogCutoffAt' => $latestStatisticsDate->copy()->addDay()->setTime(12, 0)->toDateTimeString(),
];
}
private function date(mixed $value, string $timezone): ?Carbon
{
$value = trim((string) $value);
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) !== 1) {
return null;
}
try {
return Carbon::createFromFormat('!Y-m-d', $value, $timezone)->startOfDay();
} catch (Throwable) {
return null;
}
}
private function formatDuration(int $seconds): string
{
if ($seconds < 60) {
return $seconds . ' 秒';
}
$minutes = intdiv($seconds, 60);
if ($minutes < 60) {
$remainingSeconds = $seconds % 60;
return $remainingSeconds > 0
? $minutes . ' 分 ' . $remainingSeconds . ' 秒'
: $minutes . ' 分';
}
$hours = intdiv($minutes, 60);
$remainingMinutes = $minutes % 60;
if ($hours < 24) {
return $remainingMinutes > 0
? $hours . ' 小时 ' . $remainingMinutes . ' 分'
: $hours . ' 小时';
}
$days = intdiv($hours, 24);
$remainingHours = $hours % 24;
return $remainingHours > 0
? $days . ' 天 ' . $remainingHours . ' 小时'
: $days . ' 天';
}
}