215 lines
9.4 KiB
PHP
215 lines
9.4 KiB
PHP
<?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',
|
||
'members' => [],
|
||
];
|
||
$penaltyError = null;
|
||
if ($this->repository->available()) {
|
||
try {
|
||
$daily = $this->repository->dailySummary($from->toDateString(), $to->toDateString());
|
||
$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));
|
||
}
|
||
if ($showLateDetails) {
|
||
$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' => $selectedDate,
|
||
'deadline_at' => $deadline->toDateTimeString(),
|
||
'late_seconds' => $lateSeconds,
|
||
'late_duration' => $duration,
|
||
'reason' => sprintf(
|
||
'实际提交时间晚于截止时间 %s;该日志归入 %s 统计日,不能补记为 %s 的按时日志。扣分仍按排班和请假规则判断。',
|
||
$duration,
|
||
(string) $row->statistics_date,
|
||
$selectedDate,
|
||
),
|
||
];
|
||
}, $this->repository->lateSubmissionsForDate($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 => '日志漏写扣分暂时无法计算。',
|
||
};
|
||
}
|
||
}
|
||
|
||
$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 . ' 天';
|
||
}
|
||
}
|