Adding delay and penalty calculations
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Morilog\Jalali\Jalalian;
|
||||||
|
|
||||||
|
class NotifyUpcomingBirthdays extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'birthdays:notify';
|
||||||
|
|
||||||
|
protected $description = 'Send Bale reminder to admins one day before employee birthdays';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$tomorrow = Jalalian::now()->addDays(1);
|
||||||
|
$month = $tomorrow->getMonth();
|
||||||
|
$day = $tomorrow->getDay();
|
||||||
|
$jalaliDate = $tomorrow->format('j F');
|
||||||
|
|
||||||
|
$users = User::query()
|
||||||
|
->whereNotNull('birthday')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->get()
|
||||||
|
->filter(function (User $user) use ($month, $day) {
|
||||||
|
$birth = Jalalian::fromCarbon(Carbon::parse($user->birthday));
|
||||||
|
|
||||||
|
return $birth->getMonth() === $month && $birth->getDay() === $day;
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($users->isEmpty()) {
|
||||||
|
$this->info('No birthdays tomorrow.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$name = $user->name ?: $user->user_name;
|
||||||
|
$message = "🌸 یک یادآوری لطیف از قلب تیم...\n\n"
|
||||||
|
. "فردا {$jalaliDate}، روز تولد «{$name}» است.\n\n"
|
||||||
|
. "فردایی که میتواند با یک پیام گرم، یک لبخند، یا یک «تولدت مبارک» ساده، برایش خاطرهای قشنگ بسازد.\n"
|
||||||
|
. "بیایید این روز را از یاد نبریم و بهش حس ارزشمند بودن بدهیم 💛";
|
||||||
|
|
||||||
|
$this->sendToAdmins($message);
|
||||||
|
$this->info("Birthday reminder sent for: {$name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendToAdmins(string $message): void
|
||||||
|
{
|
||||||
|
$admins = User::query()
|
||||||
|
->whereNotNull('is_admin')
|
||||||
|
->whereNotNull('bale')
|
||||||
|
->select('bale')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($admins as $admin) {
|
||||||
|
try {
|
||||||
|
Http::withoutVerifying()->post(
|
||||||
|
'https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage',
|
||||||
|
[
|
||||||
|
'chat_id' => $admin->bale,
|
||||||
|
'text' => $message,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\LatePenaltyService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Morilog\Jalali\Jalalian;
|
||||||
|
|
||||||
|
class SendDailyLateReport extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'late:daily-report {--date=}';
|
||||||
|
|
||||||
|
protected $description = 'Send daily late-penalty report to admins via Bale';
|
||||||
|
|
||||||
|
public function handle(LatePenaltyService $service): int
|
||||||
|
{
|
||||||
|
$date = $this->option('date')
|
||||||
|
? Carbon::parse($this->option('date'))->startOfDay()
|
||||||
|
: Carbon::yesterday()->startOfDay();
|
||||||
|
|
||||||
|
$start = $date->copy()->startOfDay();
|
||||||
|
$end = $date->copy()->endOfDay();
|
||||||
|
$jalali = Jalalian::fromCarbon($date)->format('Y/m/d');
|
||||||
|
|
||||||
|
$entries = $service->firstEntriesBetween($start, $end)
|
||||||
|
->filter(fn ($att) => (int) ($att->late_minutes ?? 0) > 0 || (int) ($att->daily_fine_amount ?? 0) > 0)
|
||||||
|
->values();
|
||||||
|
|
||||||
|
if ($entries->isEmpty()) {
|
||||||
|
$message = "📋 گزارش تأخیر روزانه\nتاریخ: {$jalali}\n\nامروز تأخیر مشمول جریمه ثبت نشده است.";
|
||||||
|
$service->sendToAdmins($message);
|
||||||
|
$this->info('Daily late report sent (empty).');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = ["📋 گزارش تأخیر روزانه", "تاریخ: {$jalali}", ""];
|
||||||
|
$totalFine = 0;
|
||||||
|
|
||||||
|
foreach ($entries as $att) {
|
||||||
|
$name = $service->userName($att->user_identity);
|
||||||
|
$enter = Carbon::parse($att->entered_at)->format('H:i');
|
||||||
|
$late = (int) $att->late_minutes;
|
||||||
|
$fineMin = (int) $att->daily_fine_minutes;
|
||||||
|
$fine = (int) $att->daily_fine_amount;
|
||||||
|
$totalFine += $fine;
|
||||||
|
|
||||||
|
$lines[] = "• {$name}";
|
||||||
|
$lines[] = " ورود: {$enter} | تأخیر: {$late} دقیقه";
|
||||||
|
$lines[] = $fineMin > 0
|
||||||
|
? " جریمه روزانه: {$fineMin} دقیقه × " . number_format(LatePenaltyService::FINE_PER_MINUTE) . " = " . $service->formatAmount($fine)
|
||||||
|
: " جریمه روزانه: ۰ (داخل ارفاق ۱۵ دقیقهای)";
|
||||||
|
$lines[] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines[] = "جمع جریمه روز: " . $service->formatAmount($totalFine);
|
||||||
|
$service->sendToAdmins(implode("\n", $lines));
|
||||||
|
$this->info('Daily late report sent.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\LatePenaltyService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Morilog\Jalali\Jalalian;
|
||||||
|
|
||||||
|
class SendMonthlyLateReport extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'late:monthly-report {--date=}';
|
||||||
|
|
||||||
|
protected $description = 'Send monthly late-penalty report to admins via Bale';
|
||||||
|
|
||||||
|
public function handle(LatePenaltyService $service): int
|
||||||
|
{
|
||||||
|
$forcedDate = $this->option('date');
|
||||||
|
|
||||||
|
// پیشفرض: ماه شمسیِ دیروز (برای اجرای روز اول ماه بعد)
|
||||||
|
$anchor = $forcedDate
|
||||||
|
? Carbon::parse($forcedDate)
|
||||||
|
: Carbon::yesterday();
|
||||||
|
|
||||||
|
// بدون --date فقط وقتی دیروز آخرین روز ماه شمسی بوده اجرا شود
|
||||||
|
if (!$forcedDate) {
|
||||||
|
$jYesterday = Jalalian::fromCarbon($anchor);
|
||||||
|
$daysInMonth = $jYesterday->getMonth() <= 6
|
||||||
|
? 31
|
||||||
|
: ($jYesterday->getMonth() <= 11 ? 30 : ($jYesterday->isLeapYear() ? 30 : 29));
|
||||||
|
|
||||||
|
if ($jYesterday->getDay() !== $daysInMonth) {
|
||||||
|
$this->info('Not end of Jalali month; skipped.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[$start, $end] = $service->jalaliMonthBounds($anchor);
|
||||||
|
$monthName = Jalalian::fromCarbon($start)->format('%B %Y');
|
||||||
|
|
||||||
|
$users = User::query()
|
||||||
|
->whereNotNull('user_name')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->get(['user_name', 'name']);
|
||||||
|
|
||||||
|
$lines = [
|
||||||
|
"📅 گزارش تأخیر ماهانه",
|
||||||
|
"ماه: {$monthName}",
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
|
||||||
|
$any = false;
|
||||||
|
$monthTotal = 0;
|
||||||
|
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$summary = $service->summarizeUserPeriod($user->user_name, $start, $end);
|
||||||
|
$weeklyAmount = (int) \App\Models\LatePeriodFine::query()
|
||||||
|
->where('user_identity', $user->user_name)
|
||||||
|
->where('period_type', 'weekly')
|
||||||
|
->whereBetween('period_start', [$start->toDateString(), $end->toDateString()])
|
||||||
|
->sum('fine_amount');
|
||||||
|
|
||||||
|
$total = $summary['daily_fine_amount'] + $weeklyAmount;
|
||||||
|
if ($summary['late_minutes'] <= 0 && $total <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$any = true;
|
||||||
|
$monthTotal += $total;
|
||||||
|
|
||||||
|
$service->storePeriodFine([
|
||||||
|
'user_identity' => $user->user_name,
|
||||||
|
'period_type' => 'monthly',
|
||||||
|
'period_start' => $start->toDateString(),
|
||||||
|
'period_end' => $end->toDateString(),
|
||||||
|
'late_minutes' => $summary['late_minutes'],
|
||||||
|
'allowance_minutes' => 0,
|
||||||
|
'fine_minutes' => 0,
|
||||||
|
'fine_amount' => $total,
|
||||||
|
'daily_fine_amount' => $summary['daily_fine_amount'],
|
||||||
|
'weekly_fine_amount' => $weeklyAmount,
|
||||||
|
'total_fine_amount' => $total,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$name = $user->name ?: $user->user_name;
|
||||||
|
$lines[] = "• {$name}";
|
||||||
|
$lines[] = " مجموع تأخیر ماه: {$summary['late_minutes']} دقیقه";
|
||||||
|
$lines[] = " جریمه روزانه: " . $service->formatAmount($summary['daily_fine_amount']);
|
||||||
|
$lines[] = " جریمه هفتگی: " . $service->formatAmount($weeklyAmount);
|
||||||
|
$lines[] = " جمع کل: " . $service->formatAmount($total);
|
||||||
|
$lines[] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$any) {
|
||||||
|
$lines[] = "در این ماه تأخیر مشمول جریمهای ثبت نشده است.";
|
||||||
|
} else {
|
||||||
|
$lines[] = "جمع کل جرایم ماه: " . $service->formatAmount($monthTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
$service->sendToAdmins(implode("\n", $lines));
|
||||||
|
$this->info('Monthly late report sent.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\LatePenaltyService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Morilog\Jalali\Jalalian;
|
||||||
|
|
||||||
|
class SendWeeklyLateReport extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'late:weekly-report {--date=}';
|
||||||
|
|
||||||
|
protected $description = 'Send weekly late-penalty report to admins via Bale';
|
||||||
|
|
||||||
|
public function handle(LatePenaltyService $service): int
|
||||||
|
{
|
||||||
|
$anchor = $this->option('date')
|
||||||
|
? Carbon::parse($this->option('date'))
|
||||||
|
: Carbon::yesterday();
|
||||||
|
|
||||||
|
[$start, $end] = $service->weekBounds($anchor);
|
||||||
|
$startLabel = Jalalian::fromCarbon($start)->format('Y/m/d');
|
||||||
|
$endLabel = Jalalian::fromCarbon($end)->format('Y/m/d');
|
||||||
|
|
||||||
|
$users = User::query()
|
||||||
|
->whereNotNull('user_name')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->get(['user_name', 'name']);
|
||||||
|
|
||||||
|
$lines = [
|
||||||
|
"📊 گزارش تأخیر هفتگی",
|
||||||
|
"بازه: {$startLabel} تا {$endLabel}",
|
||||||
|
"ارفاق هفتگی: " . LatePenaltyService::WEEKLY_ALLOWANCE_MINUTES . " دقیقه (از جمع تأخیرهای ۸:۳۰ تا ۸:۴۵)",
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
|
||||||
|
$any = false;
|
||||||
|
$sumWeeklyFine = 0;
|
||||||
|
$sumDailyFine = 0;
|
||||||
|
$sumReportTotal = 0;
|
||||||
|
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$summary = $service->summarizeUserPeriod($user->user_name, $start, $end);
|
||||||
|
if ($summary['grace_late_minutes'] <= 0 && $summary['daily_fine_amount'] <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$any = true;
|
||||||
|
// فقط تأخیر بازه ۸:۳۰–۸:۴۵ برای جریمه هفتگی
|
||||||
|
$weekly = $service->computeWeeklyFine($summary['grace_late_minutes']);
|
||||||
|
$reportTotal = $weekly['fine_amount'] + $summary['daily_fine_amount'];
|
||||||
|
|
||||||
|
$sumWeeklyFine += $weekly['fine_amount'];
|
||||||
|
$sumDailyFine += $summary['daily_fine_amount'];
|
||||||
|
$sumReportTotal += $reportTotal;
|
||||||
|
|
||||||
|
$service->storePeriodFine([
|
||||||
|
'user_identity' => $user->user_name,
|
||||||
|
'period_type' => 'weekly',
|
||||||
|
'period_start' => $start->toDateString(),
|
||||||
|
'period_end' => $end->toDateString(),
|
||||||
|
'late_minutes' => $summary['grace_late_minutes'],
|
||||||
|
'allowance_minutes' => $weekly['allowance_minutes'],
|
||||||
|
'fine_minutes' => $weekly['fine_minutes'],
|
||||||
|
'fine_amount' => $weekly['fine_amount'],
|
||||||
|
'daily_fine_amount' => $summary['daily_fine_amount'],
|
||||||
|
'weekly_fine_amount' => $weekly['fine_amount'],
|
||||||
|
'total_fine_amount' => $reportTotal,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$name = $user->name ?: $user->user_name;
|
||||||
|
$lines[] = "• {$name}";
|
||||||
|
$lines[] = " جمع تأخیر ۸:۳۰–۸:۴۵: {$summary['grace_late_minutes']} دقیقه";
|
||||||
|
$lines[] = " ارفاق هفتگی: {$weekly['allowance_minutes']} دقیقه";
|
||||||
|
$lines[] = $weekly['fine_minutes'] > 0
|
||||||
|
? " جریمه هفتگی: {$weekly['fine_minutes']} × " . number_format(LatePenaltyService::FINE_PER_MINUTE) . " = " . $service->formatAmount($weekly['fine_amount'])
|
||||||
|
: " جریمه هفتگی: ۰";
|
||||||
|
$lines[] = " جریمه روزانه این هفته: " . $service->formatAmount($summary['daily_fine_amount']);
|
||||||
|
$lines[] = " جمع گزارش: " . $service->formatAmount($reportTotal);
|
||||||
|
$lines[] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$any) {
|
||||||
|
$lines[] = "در این هفته تأخیری ثبت نشده است.";
|
||||||
|
} else {
|
||||||
|
$lines[] = "جمع جریمه هفتگی: " . $service->formatAmount($sumWeeklyFine);
|
||||||
|
$lines[] = "جمع جریمه روزانه: " . $service->formatAmount($sumDailyFine);
|
||||||
|
$lines[] = "جمع کل گزارش هفته: " . $service->formatAmount($sumReportTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
$service->sendToAdmins(implode("\n", $lines));
|
||||||
|
$this->info('Weekly late report sent.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ use Morilog\Jalali\Jalalian;
|
|||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use App\Models\Tasks;
|
use App\Models\Tasks;
|
||||||
use App\Models\Pivot_Tasks;
|
use App\Models\Pivot_Tasks;
|
||||||
|
use App\Services\LatePenaltyService;
|
||||||
|
|
||||||
|
|
||||||
class AdminController extends Controller
|
class AdminController extends Controller
|
||||||
@@ -283,6 +284,9 @@ class AdminController extends Controller
|
|||||||
$hours = intdiv($totalSeconds, 3600);
|
$hours = intdiv($totalSeconds, 3600);
|
||||||
$minutes = intdiv($totalSeconds % 3600, 60);
|
$minutes = intdiv($totalSeconds % 3600, 60);
|
||||||
$user->totaltime = sprintf('%02d:%02d', $hours, $minutes);
|
$user->totaltime = sprintf('%02d:%02d', $hours, $minutes);
|
||||||
|
[$jMonthStart, $jMonthEnd] = app(LatePenaltyService::class)->jalaliMonthBounds($now);
|
||||||
|
$user->monthly_late_fine = app(LatePenaltyService::class)
|
||||||
|
->monthlyTotalFine($user->user_name, $jMonthStart, $jMonthEnd);
|
||||||
$user->working = 0;
|
$user->working = 0;
|
||||||
|
|
||||||
$openForUser = $openAttendances->get($user->user_name, collect());
|
$openForUser = $openAttendances->get($user->user_name, collect());
|
||||||
@@ -435,6 +439,7 @@ class AdminController extends Controller
|
|||||||
$attendance->exited_at = $nendt ? $date . ' ' . $nendt : null;
|
$attendance->exited_at = $nendt ? $date . ' ' . $nendt : null;
|
||||||
$attendance->duration = $nendt !== null ? $attendance->duration($nstartt, $nendt) : null;
|
$attendance->duration = $nendt !== null ? $attendance->duration($nstartt, $nendt) : null;
|
||||||
$attendance->save();
|
$attendance->save();
|
||||||
|
app(LatePenaltyService::class)->syncAttendance($attendance);
|
||||||
return redirect()->back()->with('message', [
|
return redirect()->back()->with('message', [
|
||||||
'color' => '#6aff00ff',
|
'color' => '#6aff00ff',
|
||||||
"text" => 'تغییرات با موفقیت ذخیره شد.✔️'
|
"text" => 'تغییرات با موفقیت ذخیره شد.✔️'
|
||||||
@@ -461,6 +466,7 @@ class AdminController extends Controller
|
|||||||
$attendance->duration = $nendt ? $attendance->duration($nstartt, $nendt) : null;
|
$attendance->duration = $nendt ? $attendance->duration($nstartt, $nendt) : null;
|
||||||
|
|
||||||
$attendance->save();
|
$attendance->save();
|
||||||
|
app(LatePenaltyService::class)->syncAttendance($attendance);
|
||||||
return redirect()->back()->with('message', [
|
return redirect()->back()->with('message', [
|
||||||
'color' => '#6aff00ff',
|
'color' => '#6aff00ff',
|
||||||
"text" => 'تغییرات با موفقیت ذخیره شد.✔️'
|
"text" => 'تغییرات با موفقیت ذخیره شد.✔️'
|
||||||
@@ -568,6 +574,7 @@ class AdminController extends Controller
|
|||||||
$record->duration = $duration ?? $record->duration;
|
$record->duration = $duration ?? $record->duration;
|
||||||
$record->requested = '';
|
$record->requested = '';
|
||||||
$record->save();
|
$record->save();
|
||||||
|
app(LatePenaltyService::class)->syncAttendance($record);
|
||||||
|
|
||||||
Redis::connection()->del("att-tempreq:{$data->userid}:{$data->id}");
|
Redis::connection()->del("att-tempreq:{$data->userid}:{$data->id}");
|
||||||
$this->decrementUserRequestCount($data->userid);
|
$this->decrementUserRequestCount($data->userid);
|
||||||
@@ -602,6 +609,7 @@ class AdminController extends Controller
|
|||||||
? $attendance->duration($starttime, $endtime)
|
? $attendance->duration($starttime, $endtime)
|
||||||
: null;
|
: null;
|
||||||
$attendance->save();
|
$attendance->save();
|
||||||
|
app(LatePenaltyService::class)->syncAttendance($attendance);
|
||||||
$create_requests_key = "att-tempreq:creates:{$data->userid}";
|
$create_requests_key = "att-tempreq:creates:{$data->userid}";
|
||||||
Redis::connection()->lrem($create_requests_key, 1, json_encode($data));
|
Redis::connection()->lrem($create_requests_key, 1, json_encode($data));
|
||||||
$this->decrementUserRequestCount($data->userid);
|
$this->decrementUserRequestCount($data->userid);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use App\Models\User;
|
|||||||
use App\Models\Log;
|
use App\Models\Log;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use App\Models\ai;
|
use App\Models\ai;
|
||||||
|
use App\Services\LatePenaltyService;
|
||||||
|
|
||||||
|
|
||||||
class AttendanceController extends Controller
|
class AttendanceController extends Controller
|
||||||
@@ -294,26 +295,9 @@ class AttendanceController extends Controller
|
|||||||
->latest('entered_at')
|
->latest('entered_at')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($attendance) {
|
if ($attendance) {
|
||||||
// خروج
|
// خروج — همه تسکهای باز کاربر بسته شود
|
||||||
//end task
|
app(TaskController::class)->endOpenDurationsForUser($user);
|
||||||
if (!empty($user->active_task)) {
|
|
||||||
$taskDu = TaskDuration::where('user_id', $user->id)->whereNull('end')->whereNull('duration')->get();
|
|
||||||
if ($taskDu) {
|
|
||||||
foreach ($taskDu as $item) {
|
|
||||||
$fakeRequest = Request::create(
|
|
||||||
'/active_task', // مسیر فرضی (مهم نیست)
|
|
||||||
'POST',
|
|
||||||
['task_id' => $item->task_id] // شناسه تسک
|
|
||||||
);
|
|
||||||
if ($item->end == null) {
|
|
||||||
(new TaskController())->activeTask($fakeRequest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$user->active_task = null;
|
|
||||||
$user->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
//end break
|
//end break
|
||||||
if ($break = AttendanceBreak::where('attendance_id', $attendance->id)->whereNull('break_end')->latest('break_start')->first()) {
|
if ($break = AttendanceBreak::where('attendance_id', $attendance->id)->whereNull('break_end')->latest('break_start')->first()) {
|
||||||
@@ -322,6 +306,7 @@ class AttendanceController extends Controller
|
|||||||
$break->save();
|
$break->save();
|
||||||
}
|
}
|
||||||
$break = AttendanceBreak::where('attendance_id', $attendance->id)->sum('duration');
|
$break = AttendanceBreak::where('attendance_id', $attendance->id)->sum('duration');
|
||||||
|
$attendance->exited_at = Carbon::now()->format('Y-m-d H:i:s');
|
||||||
$start = Carbon::parse($attendance->entered_at)->format('H:i:s');
|
$start = Carbon::parse($attendance->entered_at)->format('H:i:s');
|
||||||
$end = Carbon::parse($attendance->exited_at)->format('H:i:s');
|
$end = Carbon::parse($attendance->exited_at)->format('H:i:s');
|
||||||
$duration = $attendance->duration($start, $end);
|
$duration = $attendance->duration($start, $end);
|
||||||
@@ -330,10 +315,9 @@ class AttendanceController extends Controller
|
|||||||
$breakSec = strtotime("1970-01-01 " . $break) - strtotime("1970-01-01 00:00:00");
|
$breakSec = strtotime("1970-01-01 " . $break) - strtotime("1970-01-01 00:00:00");
|
||||||
$durationSec = strtotime("1970-01-01 " . $duration) - strtotime("1970-01-01 00:00:00");
|
$durationSec = strtotime("1970-01-01 " . $duration) - strtotime("1970-01-01 00:00:00");
|
||||||
$durationSec -= $breakSec;
|
$durationSec -= $breakSec;
|
||||||
$duration = gmdate("H:i:s", $durationSec);
|
$duration = gmdate("H:i:s", max(0, $durationSec));
|
||||||
}
|
}
|
||||||
|
|
||||||
$attendance->exited_at = Carbon::now()->format('Y-m-d H:i:s');
|
|
||||||
$attendance->exit_location = ['lat' => $userLat, 'lng' => $userLng];
|
$attendance->exit_location = ['lat' => $userLat, 'lng' => $userLng];
|
||||||
$attendance->ip_exit = $userLng;
|
$attendance->ip_exit = $userLng;
|
||||||
$attendance->distance_exit = $request->input('distance');
|
$attendance->distance_exit = $request->input('distance');
|
||||||
@@ -359,6 +343,8 @@ class AttendanceController extends Controller
|
|||||||
$attendance->office_location = ['lat' => $officeLat, 'lng' => $officeLng];
|
$attendance->office_location = ['lat' => $officeLat, 'lng' => $officeLng];
|
||||||
$attendance->save();
|
$attendance->save();
|
||||||
|
|
||||||
|
app(LatePenaltyService::class)->syncAttendance($attendance);
|
||||||
|
|
||||||
$this->send($user->name, $attendance, 'attendance');
|
$this->send($user->name, $attendance, 'attendance');
|
||||||
|
|
||||||
return redirect()->back()->with('message', [
|
return redirect()->back()->with('message', [
|
||||||
@@ -648,6 +634,9 @@ class AttendanceController extends Controller
|
|||||||
$user->email = $request->email;
|
$user->email = $request->email;
|
||||||
$user->number = $request->number;
|
$user->number = $request->number;
|
||||||
$user->chatid = $request->chatid;
|
$user->chatid = $request->chatid;
|
||||||
|
$user->birthday = $request->filled('birthday')
|
||||||
|
? $this->fa2en_numbers($request->birthday)
|
||||||
|
: null;
|
||||||
|
|
||||||
// اگر کاربر ادمین باشه، میتونه نقش رو تغییر بده
|
// اگر کاربر ادمین باشه، میتونه نقش رو تغییر بده
|
||||||
if (!empty($_COOKIE['ad'])) {
|
if (!empty($_COOKIE['ad'])) {
|
||||||
|
|||||||
@@ -21,6 +21,93 @@ use Carbon\CarbonInterval;
|
|||||||
|
|
||||||
class TaskController extends Controller
|
class TaskController extends Controller
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* بستن همه نوبتهای باز تسک برای یک کاربر (مثلاً هنگام خروج)
|
||||||
|
*/
|
||||||
|
public function endOpenDurationsForUser(User $user, $endedAt = null): int
|
||||||
|
{
|
||||||
|
$endedAt = $endedAt ? Carbon::parse($endedAt) : Carbon::now();
|
||||||
|
$openItems = TaskDuration::query()
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->whereNotNull('start')
|
||||||
|
->whereNull('end')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$closed = 0;
|
||||||
|
foreach ($openItems as $item) {
|
||||||
|
$this->closeDurationRecord($item, $endedAt);
|
||||||
|
$closed++;
|
||||||
|
|
||||||
|
$taskTitle = Tasks::where('id', $item->task_id)->value('title') ?? ('#' . $item->task_id);
|
||||||
|
$this->send_level1_admins(
|
||||||
|
"پایان خودکار تسک (خروج): «{$taskTitle}»\nتوسط: {$user->name}\nمدت این نوبت: {$item->duration}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($closed > 0 || !empty($user->active_task)) {
|
||||||
|
$user->active_task = null;
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function closeDurationRecord(TaskDuration $taskDU, Carbon $endedAt): TaskDuration
|
||||||
|
{
|
||||||
|
$start = Carbon::parse($taskDU->start);
|
||||||
|
$seconds = max(0, (int) $start->diffInSeconds($endedAt, false));
|
||||||
|
$diff = sprintf(
|
||||||
|
'%02d:%02d:%02d',
|
||||||
|
intdiv($seconds, 3600),
|
||||||
|
intdiv($seconds % 3600, 60),
|
||||||
|
$seconds % 60
|
||||||
|
);
|
||||||
|
|
||||||
|
$duration = $taskDU->duration;
|
||||||
|
if ($duration !== null && $duration !== '') {
|
||||||
|
[$h1, $m1, $s1] = array_map('intval', explode(':', $duration));
|
||||||
|
$durationSeconds = $h1 * 3600 + $m1 * 60 + $s1;
|
||||||
|
$totalSeconds = $durationSeconds + $seconds;
|
||||||
|
$duration = sprintf(
|
||||||
|
'%02d:%02d:%02d',
|
||||||
|
intdiv($totalSeconds, 3600),
|
||||||
|
intdiv($totalSeconds % 3600, 60),
|
||||||
|
$totalSeconds % 60
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$duration = $diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
$taskDU->end = $endedAt;
|
||||||
|
$taskDU->duration = $duration;
|
||||||
|
$taskDU->save();
|
||||||
|
|
||||||
|
return $taskDU;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function secondsBetween(?string $start, ?string $end): int
|
||||||
|
{
|
||||||
|
if (!$start || !$end) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$s = Carbon::parse($start);
|
||||||
|
$e = Carbon::parse($end);
|
||||||
|
return max(0, (int) $s->diffInSeconds($e, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatSeconds(int $seconds): string
|
||||||
|
{
|
||||||
|
$seconds = max(0, $seconds);
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'%02d:%02d:%02d',
|
||||||
|
intdiv($seconds, 3600),
|
||||||
|
intdiv($seconds % 3600, 60),
|
||||||
|
$seconds % 60
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function taskChangeMessage(Tasks $task, array $before, array $after, ?User $actor): string
|
private function taskChangeMessage(Tasks $task, array $before, array $after, ?User $actor): string
|
||||||
{
|
{
|
||||||
$lines = [];
|
$lines = [];
|
||||||
@@ -167,6 +254,28 @@ class TaskController extends Controller
|
|||||||
return User::whereIn('id', $assignees)->pluck('name')->all();
|
return User::whereIn('id', $assignees)->pluck('name')->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function normalizeTaskDate(?string $value): ?string
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = trim($value);
|
||||||
|
if ($value === '' || str_starts_with($value, '0000-00-00')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $value, $m)) {
|
||||||
|
return $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Carbon::parse($value)->format('Y-m-d');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function buildTasksPayload($tasks, array $usersById = []): array
|
private function buildTasksPayload($tasks, array $usersById = []): array
|
||||||
{
|
{
|
||||||
$payload = [];
|
$payload = [];
|
||||||
@@ -175,8 +284,8 @@ class TaskController extends Controller
|
|||||||
'id' => $task->id,
|
'id' => $task->id,
|
||||||
'title' => $task->title,
|
'title' => $task->title,
|
||||||
'description' => $task->description,
|
'description' => $task->description,
|
||||||
'start_date' => $task->start_date,
|
'start_date' => $this->normalizeTaskDate($task->start_date ? (string) $task->start_date : null),
|
||||||
'target_date' => $task->target_date,
|
'target_date' => $this->normalizeTaskDate($task->target_date ? (string) $task->target_date : null),
|
||||||
'finished_at' => $task->finished_at ?? null,
|
'finished_at' => $task->finished_at ?? null,
|
||||||
'priority' => $task->priority,
|
'priority' => $task->priority,
|
||||||
'state' => $task->state,
|
'state' => $task->state,
|
||||||
@@ -221,8 +330,8 @@ class TaskController extends Controller
|
|||||||
'id' => $task->id,
|
'id' => $task->id,
|
||||||
'title' => $task->title,
|
'title' => $task->title,
|
||||||
'description' => $task->description,
|
'description' => $task->description,
|
||||||
'start_date' => $task->start_date,
|
'start_date' => $this->normalizeTaskDate($task->start_date ? (string) $task->start_date : null),
|
||||||
'target_date' => $task->target_date,
|
'target_date' => $this->normalizeTaskDate($task->target_date ? (string) $task->target_date : null),
|
||||||
'finished_at' => $this->resolveTaskFinishedAtByTaskId($task->id),
|
'finished_at' => $this->resolveTaskFinishedAtByTaskId($task->id),
|
||||||
'priority' => $task->priority,
|
'priority' => $task->priority,
|
||||||
'state' => $task->state,
|
'state' => $task->state,
|
||||||
@@ -516,6 +625,18 @@ class TaskController extends Controller
|
|||||||
$data['completed_at_display']
|
$data['completed_at_display']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (array_key_exists('start_date', $data)) {
|
||||||
|
$data['start_date'] = $this->normalizeTaskDate($data['start_date'] ?: null);
|
||||||
|
// خالی فرستادن نباید تاریخ قبلی را پاک کند مگر صریح null نشود
|
||||||
|
if ($data['start_date'] === null) {
|
||||||
|
unset($data['start_date']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (array_key_exists('target_date', $data)) {
|
||||||
|
$normalizedTarget = $this->normalizeTaskDate($data['target_date'] ?: null);
|
||||||
|
$data['target_date'] = $normalizedTarget;
|
||||||
|
}
|
||||||
|
|
||||||
$task->update($data);
|
$task->update($data);
|
||||||
$after = $task->toArray();
|
$after = $task->toArray();
|
||||||
|
|
||||||
@@ -567,9 +688,25 @@ class TaskController extends Controller
|
|||||||
}
|
}
|
||||||
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
|
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
|
||||||
|
|
||||||
$data = $request->all();
|
$startDate = $this->normalizeTaskDate($request->input('start_date'));
|
||||||
$data['created_by'] = $user->id;
|
if (!$startDate) {
|
||||||
$data['assignees'] = !empty($data['assignees']) ? json_encode($data['assignees']) : null;
|
return back()->withInput()->with('message', [
|
||||||
|
'color' => 'red',
|
||||||
|
'text' => 'تاریخ شروع الزامی است. لطفاً از تقویم انتخاب کنید.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'title' => $request->input('title'),
|
||||||
|
'description' => $request->input('description'),
|
||||||
|
'priority' => $request->input('priority', 'none'),
|
||||||
|
'state' => $request->input('state', 1),
|
||||||
|
'app' => $request->input('app') ?: null,
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'target_date' => $this->normalizeTaskDate($request->input('target_date')),
|
||||||
|
'created_by' => $user->id,
|
||||||
|
'assignees' => !empty($request->assignees) ? json_encode($request->assignees) : null,
|
||||||
|
];
|
||||||
|
|
||||||
$task = Tasks::create($data);
|
$task = Tasks::create($data);
|
||||||
|
|
||||||
@@ -580,16 +717,16 @@ class TaskController extends Controller
|
|||||||
$message_admin .= !empty($app) ? "\nاپ {$app->name_fa}" : null;
|
$message_admin .= !empty($app) ? "\nاپ {$app->name_fa}" : null;
|
||||||
$message_admin .= !empty($deadline) ? "\nددلاین {$deadline}" : null;
|
$message_admin .= !empty($deadline) ? "\nددلاین {$deadline}" : null;
|
||||||
$message_admin .= !empty($data['start_date']) ? "\nتاریخ شروع {$data['start_date']}" : null;
|
$message_admin .= !empty($data['start_date']) ? "\nتاریخ شروع {$data['start_date']}" : null;
|
||||||
$message_admin .= !empty($data['state_fa']) ? "\nوضعیت: {$data['state_fa']}" : null;
|
$message_admin .= !empty($request->input('state_fa')) ? "\nوضعیت: {$request->input('state_fa')}" : null;
|
||||||
$message_admin .= !empty($data['description']) ? "\nبا توضیحات:\n" . $data['description'] : null;
|
$message_admin .= !empty($data['description']) ? "\nبا توضیحات:\n" . $data['description'] : null;
|
||||||
$message_admin .= !empty($data['assignees']) ? "\nتوسط " . $user->name . " ساخته شد و به" : null;
|
$message_admin .= !empty($data['assignees']) ? "\nتوسط " . $user->name . " ساخته شد و به" : null;
|
||||||
|
|
||||||
if (!empty($data['assignees'])) {
|
if (!empty($request->assignees)) {
|
||||||
foreach ($request->assignees as $assignee) {
|
foreach ($request->assignees as $assignee) {
|
||||||
$user = User::where('id', $assignee)->first();
|
$assigneeUser = User::where('id', $assignee)->first();
|
||||||
$message_admin .= "\n$user->name";
|
$message_admin .= "\n" . ($assigneeUser->name ?? $assignee);
|
||||||
if ($user && $user->bale !== null) {
|
if ($assigneeUser && $assigneeUser->bale !== null) {
|
||||||
$this->send_bale($user->bale, $message);
|
$this->send_bale($assigneeUser->bale, $message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$message_admin .= "\nاختصاص یافت.";
|
$message_admin .= "\nاختصاص یافت.";
|
||||||
@@ -628,50 +765,18 @@ class TaskController extends Controller
|
|||||||
|
|
||||||
// اگر رکورد باز داریم => پایان
|
// اگر رکورد باز داریم => پایان
|
||||||
if (!empty($taskDU) && ($taskDU->start !== null && $taskDU->end == null)) {
|
if (!empty($taskDU) && ($taskDU->start !== null && $taskDU->end == null)) {
|
||||||
// dd('in if');
|
$this->closeDurationRecord($taskDU, Carbon::now());
|
||||||
|
$user->active_task = null;
|
||||||
$attendance = new Attendance();
|
$user->save();
|
||||||
$s = Carbon::parse($taskDU->start)->format('H:i:s');
|
$this->send_level1_admins("پایان تسک: «{$task->title}»\nتوسط: {$user->name}\nمدت این نوبت: {$taskDU->duration}");
|
||||||
$e = Carbon::parse(now())->format('H:i:s');
|
return response()->json([
|
||||||
$duration = $taskDU->duration;
|
'action' => 'ended',
|
||||||
if ($e > $s) {
|
'status' => true,
|
||||||
$diff = $attendance->duration($s, $e);
|
'duration' => $taskDU->duration,
|
||||||
if ($duration !== null) {
|
'start' => $taskDU->start,
|
||||||
[$h1, $m1, $s1] = explode(':', $duration);
|
'end' => $taskDU->end,
|
||||||
$durationSeconds = $h1 * 3600 + $m1 * 60 + $s1;
|
'finished_at' => $this->resolveTaskFinishedAtByTaskId($task->id),
|
||||||
|
]);
|
||||||
[$h2, $m2, $s2] = explode(':', $diff);
|
|
||||||
$diffSeconds = $h2 * 3600 + $m2 * 60 + $s2;
|
|
||||||
|
|
||||||
$totalSeconds = $durationSeconds + $diffSeconds;
|
|
||||||
|
|
||||||
$hours = floor($totalSeconds / 3600);
|
|
||||||
$minutes = floor(($totalSeconds % 3600) / 60);
|
|
||||||
$seconds = $totalSeconds % 60;
|
|
||||||
|
|
||||||
$duration = sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds);
|
|
||||||
} else {
|
|
||||||
$duration = $diff;
|
|
||||||
}
|
|
||||||
$taskDU->end = now();
|
|
||||||
$taskDU->duration = $duration;
|
|
||||||
$user->active_task = null;
|
|
||||||
$taskDU->save();
|
|
||||||
$user->save();
|
|
||||||
$this->send_level1_admins("پایان تسک: «{$task->title}»\nتوسط: {$user->name}\nمدت این نوبت: {$duration}");
|
|
||||||
return response()->json([
|
|
||||||
'action' => 'ended',
|
|
||||||
'status' => true,
|
|
||||||
'duration' => $duration,
|
|
||||||
'start' => $taskDU->start,
|
|
||||||
'end' => $taskDU->end,
|
|
||||||
'finished_at' => $this->resolveTaskFinishedAtByTaskId($task->id),
|
|
||||||
]);
|
|
||||||
} else
|
|
||||||
return response()->json([
|
|
||||||
'action' => 'end',
|
|
||||||
'status' => false
|
|
||||||
]);
|
|
||||||
} else {
|
} else {
|
||||||
// شروع => همیشه یک رکورد جدید بساز
|
// شروع => همیشه یک رکورد جدید بساز
|
||||||
$new_task_duration = TaskDuration::create([
|
$new_task_duration = TaskDuration::create([
|
||||||
@@ -695,6 +800,80 @@ class TaskController extends Controller
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updateTaskDuration(Request $request)
|
||||||
|
{
|
||||||
|
if (!isset($_COOKIE['user_identity'])) {
|
||||||
|
return response()->json(['status' => false, 'message' => 'unauthorized'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$admin = User::where('user_name', $_COOKIE['user_identity'])->first();
|
||||||
|
if (!$admin || (int) $admin->is_admin !== 1) {
|
||||||
|
return response()->json(['status' => false, 'message' => 'فقط ادمین میتواند زمان را ویرایش کند.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'duration_id' => 'required|integer',
|
||||||
|
'start' => 'required|date',
|
||||||
|
'end' => 'nullable|date',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$taskDU = TaskDuration::find($request->duration_id);
|
||||||
|
if (!$taskDU) {
|
||||||
|
return response()->json(['status' => false, 'message' => 'رکورد زمان یافت نشد.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = Carbon::parse($request->start);
|
||||||
|
$end = $request->filled('end') ? Carbon::parse($request->end) : null;
|
||||||
|
|
||||||
|
if ($end && $end->lt($start)) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => false,
|
||||||
|
'message' => 'زمان پایان نمیتواند قبل از شروع باشد.',
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$taskDU->start = $start;
|
||||||
|
$taskDU->end = $end;
|
||||||
|
$taskDU->duration = $end ? $this->formatSeconds($this->secondsBetween($start->toDateTimeString(), $end->toDateTimeString())) : null;
|
||||||
|
$taskDU->save();
|
||||||
|
|
||||||
|
// اگر این نوبت هنوز باز است و مال همان کاربر فعال است، active_task را همگام کن
|
||||||
|
$owner = User::find($taskDU->user_id);
|
||||||
|
if ($owner) {
|
||||||
|
$stillOpen = TaskDuration::query()
|
||||||
|
->where('user_id', $owner->id)
|
||||||
|
->whereNull('end')
|
||||||
|
->exists();
|
||||||
|
if (!$stillOpen) {
|
||||||
|
$owner->active_task = null;
|
||||||
|
$owner->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$task = Tasks::find($taskDU->task_id);
|
||||||
|
$this->send_level1_admins(
|
||||||
|
"ویرایش زمان تسک: «" . ($task->title ?? $taskDU->task_id) . "»\n"
|
||||||
|
. "توسط ادمین: {$admin->name}\n"
|
||||||
|
. "شروع: " . $start->format('Y-m-d H:i') . "\n"
|
||||||
|
. "پایان: " . ($end ? $end->format('Y-m-d H:i') : 'باز') . "\n"
|
||||||
|
. "مدت: " . ($taskDU->duration ?? '-')
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => true,
|
||||||
|
'message' => 'زمان با موفقیت ذخیره شد.',
|
||||||
|
'duration' => [
|
||||||
|
'id' => $taskDU->id,
|
||||||
|
'task_id' => $taskDU->task_id,
|
||||||
|
'user_id' => $taskDU->user_id,
|
||||||
|
'start' => $taskDU->start,
|
||||||
|
'end' => $taskDU->end,
|
||||||
|
'duration' => $taskDU->duration,
|
||||||
|
],
|
||||||
|
'finished_at' => $this->resolveTaskFinishedAtByTaskId($taskDU->task_id),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function addTask(Request $request)
|
public function addTask(Request $request)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ class Attendance extends Model
|
|||||||
'entered_at',
|
'entered_at',
|
||||||
'exited_at',
|
'exited_at',
|
||||||
'requested',
|
'requested',
|
||||||
|
'late_minutes',
|
||||||
|
'daily_fine_minutes',
|
||||||
|
'daily_fine_amount',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function setEntryLocationAttribute($value)
|
public function setEntryLocationAttribute($value)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class LatePeriodFine extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'user_identity',
|
||||||
|
'period_type',
|
||||||
|
'period_start',
|
||||||
|
'period_end',
|
||||||
|
'late_minutes',
|
||||||
|
'allowance_minutes',
|
||||||
|
'fine_minutes',
|
||||||
|
'fine_amount',
|
||||||
|
'daily_fine_amount',
|
||||||
|
'weekly_fine_amount',
|
||||||
|
'total_fine_amount',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'period_start' => 'date',
|
||||||
|
'period_end' => 'date',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-2
@@ -29,6 +29,7 @@ class User extends Authenticatable
|
|||||||
'chatid',
|
'chatid',
|
||||||
'name',
|
'name',
|
||||||
'number',
|
'number',
|
||||||
|
'birthday',
|
||||||
'password',
|
'password',
|
||||||
'user_name',
|
'user_name',
|
||||||
'is_admin',
|
'is_admin',
|
||||||
@@ -53,7 +54,9 @@ class User extends Authenticatable
|
|||||||
*/
|
*/
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [];
|
return [
|
||||||
|
'birthday' => 'date',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function task()
|
public function task()
|
||||||
@@ -78,7 +81,7 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
|
|
||||||
return $this->hasMany(Attendance::class, 'user_identity', 'user_name')
|
return $this->hasMany(Attendance::class, 'user_identity', 'user_name')
|
||||||
->select('id', 'user_identity', 'entered_at', 'exited_at', 'duration','requested', 'distance', 'distance_exit')
|
->select('id', 'user_identity', 'entered_at', 'exited_at', 'duration', 'requested', 'distance', 'distance_exit', 'late_minutes', 'daily_fine_minutes', 'daily_fine_amount')
|
||||||
->orderBy('entered_at', 'desc');
|
->orderBy('entered_at', 'desc');
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Attendance;
|
||||||
|
use App\Models\LatePeriodFine;
|
||||||
|
use App\Models\User;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Morilog\Jalali\Jalalian;
|
||||||
|
|
||||||
|
class LatePenaltyService
|
||||||
|
{
|
||||||
|
public const WORK_START = '08:30';
|
||||||
|
public const DAILY_GRACE_MINUTES = 15;
|
||||||
|
public const WEEKLY_ALLOWANCE_MINUTES = 15;
|
||||||
|
public const FINE_PER_MINUTE = 2000;
|
||||||
|
|
||||||
|
public function calculateFromEnteredAt(?string $enteredAt): array
|
||||||
|
{
|
||||||
|
if (!$enteredAt) {
|
||||||
|
return [
|
||||||
|
'late_minutes' => 0,
|
||||||
|
'grace_late_minutes' => 0,
|
||||||
|
'daily_fine_minutes' => 0,
|
||||||
|
'daily_fine_amount' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$entered = Carbon::parse($enteredAt);
|
||||||
|
$threshold = $entered->copy()->setTimeFromTimeString(self::WORK_START);
|
||||||
|
|
||||||
|
if ($entered->lte($threshold)) {
|
||||||
|
return [
|
||||||
|
'late_minutes' => 0,
|
||||||
|
'grace_late_minutes' => 0,
|
||||||
|
'daily_fine_minutes' => 0,
|
||||||
|
'daily_fine_amount' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$lateMinutes = (int) ceil($threshold->diffInSeconds($entered) / 60);
|
||||||
|
// تأخیر داخل بازه ارفاق روزانه (۸:۳۰ تا ۸:۴۵) — مبنای جریمه هفتگی
|
||||||
|
$graceLateMinutes = min($lateMinutes, self::DAILY_GRACE_MINUTES);
|
||||||
|
$dailyFineMinutes = max(0, $lateMinutes - self::DAILY_GRACE_MINUTES);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'late_minutes' => $lateMinutes,
|
||||||
|
'grace_late_minutes' => $graceLateMinutes,
|
||||||
|
'daily_fine_minutes' => $dailyFineMinutes,
|
||||||
|
'daily_fine_amount' => $dailyFineMinutes * self::FINE_PER_MINUTE,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** دقایق تأخیر داخل بازه ۸:۳۰–۸:۴۵ برای یک رکورد */
|
||||||
|
public function graceLateMinutes(Attendance $att): int
|
||||||
|
{
|
||||||
|
$late = (int) ($att->late_minutes ?? 0);
|
||||||
|
if ($late <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return min($late, self::DAILY_GRACE_MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncAttendance(Attendance $attendance): Attendance
|
||||||
|
{
|
||||||
|
if (!$attendance->entered_at) {
|
||||||
|
$attendance->late_minutes = 0;
|
||||||
|
$attendance->daily_fine_minutes = 0;
|
||||||
|
$attendance->daily_fine_amount = 0;
|
||||||
|
$attendance->save();
|
||||||
|
|
||||||
|
return $attendance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// فقط اولین ورود هر روز برای جریمه تأخیر محاسبه میشود
|
||||||
|
$day = Carbon::parse($attendance->entered_at)->toDateString();
|
||||||
|
$firstOfDay = Attendance::query()
|
||||||
|
->where('user_identity', $attendance->user_identity)
|
||||||
|
->whereDate('entered_at', $day)
|
||||||
|
->orderBy('entered_at')
|
||||||
|
->orderBy('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($firstOfDay && (int) $firstOfDay->id !== (int) $attendance->id) {
|
||||||
|
$attendance->late_minutes = 0;
|
||||||
|
$attendance->daily_fine_minutes = 0;
|
||||||
|
$attendance->daily_fine_amount = 0;
|
||||||
|
$attendance->save();
|
||||||
|
|
||||||
|
return $attendance;
|
||||||
|
}
|
||||||
|
|
||||||
|
$calc = $this->calculateFromEnteredAt($attendance->entered_at);
|
||||||
|
$attendance->late_minutes = $calc['late_minutes'];
|
||||||
|
$attendance->daily_fine_minutes = $calc['daily_fine_minutes'];
|
||||||
|
$attendance->daily_fine_amount = $calc['daily_fine_amount'];
|
||||||
|
$attendance->save();
|
||||||
|
|
||||||
|
// اگر این رکورد اولین ورود شد، بقیه همان روز را صفر کن
|
||||||
|
Attendance::query()
|
||||||
|
->where('user_identity', $attendance->user_identity)
|
||||||
|
->whereDate('entered_at', $day)
|
||||||
|
->where('id', '!=', $attendance->id)
|
||||||
|
->update([
|
||||||
|
'late_minutes' => 0,
|
||||||
|
'daily_fine_minutes' => 0,
|
||||||
|
'daily_fine_amount' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $attendance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function weekBounds(?Carbon $date = null): array
|
||||||
|
{
|
||||||
|
$date = ($date ?? Carbon::now())->copy()->startOfDay();
|
||||||
|
// هفته ایرانی: شنبه تا جمعه
|
||||||
|
$dayOfWeek = (int) $date->dayOfWeek; // 0=Sun ... 6=Sat
|
||||||
|
$daysFromSaturday = ($dayOfWeek + 1) % 7;
|
||||||
|
$start = $date->copy()->subDays($daysFromSaturday)->startOfDay();
|
||||||
|
$end = $start->copy()->addDays(6)->endOfDay();
|
||||||
|
|
||||||
|
return [$start, $end];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jalaliMonthBounds(?Carbon $date = null): array
|
||||||
|
{
|
||||||
|
$j = Jalalian::fromCarbon($date ?? Carbon::now());
|
||||||
|
$start = (new Jalalian($j->getYear(), $j->getMonth(), 1))->toCarbon()->startOfDay();
|
||||||
|
$endDay = $j->getMonth() <= 6 ? 31 : ($j->getMonth() <= 11 ? 30 : ($j->isLeapYear() ? 30 : 29));
|
||||||
|
$end = (new Jalalian($j->getYear(), $j->getMonth(), $endDay))->toCarbon()->endOfDay();
|
||||||
|
|
||||||
|
return [$start, $end];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function firstEntriesBetween(Carbon $start, Carbon $end, ?string $userIdentity = null): Collection
|
||||||
|
{
|
||||||
|
$query = Attendance::query()
|
||||||
|
->whereNotNull('entered_at')
|
||||||
|
->whereBetween('entered_at', [$start, $end])
|
||||||
|
->orderBy('entered_at')
|
||||||
|
->orderBy('id');
|
||||||
|
|
||||||
|
if ($userIdentity) {
|
||||||
|
$query->where('user_identity', $userIdentity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->get()
|
||||||
|
->groupBy(fn (Attendance $att) => $att->user_identity . '|' . Carbon::parse($att->entered_at)->toDateString())
|
||||||
|
->map(fn (Collection $items) => $items->first())
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function summarizeUserPeriod(string $userIdentity, Carbon $start, Carbon $end): array
|
||||||
|
{
|
||||||
|
$entries = $this->firstEntriesBetween($start, $end, $userIdentity);
|
||||||
|
$lateMinutes = (int) $entries->sum(fn (Attendance $att) => (int) ($att->late_minutes ?? 0));
|
||||||
|
$graceLateMinutes = (int) $entries->sum(fn (Attendance $att) => $this->graceLateMinutes($att));
|
||||||
|
$dailyFineAmount = (int) $entries->sum(fn (Attendance $att) => (int) ($att->daily_fine_amount ?? 0));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'late_minutes' => $lateMinutes,
|
||||||
|
'grace_late_minutes' => $graceLateMinutes,
|
||||||
|
'daily_fine_amount' => $dailyFineAmount,
|
||||||
|
'entries' => $entries,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* جریمه هفتگی = (جمع تأخیرهای ۸:۳۰–۸:۴۵ − ۱۵ دقیقه ارفاق) × ۲۰۰۰
|
||||||
|
*/
|
||||||
|
public function computeWeeklyFine(int $graceLateMinutes): array
|
||||||
|
{
|
||||||
|
$fineMinutes = max(0, $graceLateMinutes - self::WEEKLY_ALLOWANCE_MINUTES);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'allowance_minutes' => self::WEEKLY_ALLOWANCE_MINUTES,
|
||||||
|
'fine_minutes' => $fineMinutes,
|
||||||
|
'fine_amount' => $fineMinutes * self::FINE_PER_MINUTE,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function formatAmount(int $amount): string
|
||||||
|
{
|
||||||
|
return number_format($amount) . ' تومان';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendToAdmins(string $message): void
|
||||||
|
{
|
||||||
|
$admins = User::query()
|
||||||
|
->whereNotNull('is_admin')
|
||||||
|
->whereNotNull('bale')
|
||||||
|
->select('bale')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($admins as $admin) {
|
||||||
|
try {
|
||||||
|
Http::withoutVerifying()->post(
|
||||||
|
'https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage',
|
||||||
|
[
|
||||||
|
'chat_id' => $admin->bale,
|
||||||
|
'text' => $message,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function userName(string $userIdentity): string
|
||||||
|
{
|
||||||
|
return User::where('user_name', $userIdentity)->value('name') ?: $userIdentity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storePeriodFine(array $data): LatePeriodFine
|
||||||
|
{
|
||||||
|
return LatePeriodFine::updateOrCreate(
|
||||||
|
[
|
||||||
|
'user_identity' => $data['user_identity'],
|
||||||
|
'period_type' => $data['period_type'],
|
||||||
|
'period_start' => $data['period_start'],
|
||||||
|
'period_end' => $data['period_end'],
|
||||||
|
],
|
||||||
|
$data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function monthlyTotalFine(string $userIdentity, Carbon $monthStart, Carbon $monthEnd): int
|
||||||
|
{
|
||||||
|
$daily = (int) Attendance::query()
|
||||||
|
->where('user_identity', $userIdentity)
|
||||||
|
->whereBetween('entered_at', [$monthStart, $monthEnd])
|
||||||
|
->sum('daily_fine_amount');
|
||||||
|
|
||||||
|
$weekly = (int) LatePeriodFine::query()
|
||||||
|
->where('user_identity', $userIdentity)
|
||||||
|
->where('period_type', 'weekly')
|
||||||
|
->whereBetween('period_start', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
||||||
|
->sum('fine_amount');
|
||||||
|
|
||||||
|
return $daily + $weekly;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->date('birthday')->nullable()->after('number');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('birthday');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('attendances', function (Blueprint $table) {
|
||||||
|
$table->unsignedInteger('late_minutes')->default(0);
|
||||||
|
$table->unsignedInteger('daily_fine_minutes')->default(0);
|
||||||
|
$table->unsignedInteger('daily_fine_amount')->default(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('late_period_fines', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('user_identity');
|
||||||
|
$table->string('period_type'); // weekly | monthly
|
||||||
|
$table->date('period_start');
|
||||||
|
$table->date('period_end');
|
||||||
|
$table->unsignedInteger('late_minutes')->default(0);
|
||||||
|
$table->unsignedInteger('allowance_minutes')->default(0);
|
||||||
|
$table->unsignedInteger('fine_minutes')->default(0);
|
||||||
|
$table->unsignedInteger('fine_amount')->default(0);
|
||||||
|
$table->unsignedInteger('daily_fine_amount')->default(0);
|
||||||
|
$table->unsignedInteger('weekly_fine_amount')->default(0);
|
||||||
|
$table->unsignedInteger('total_fine_amount')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['user_identity', 'period_type', 'period_start', 'period_end'], 'late_period_unique');
|
||||||
|
$table->index(['period_type', 'period_start']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('late_period_fines');
|
||||||
|
|
||||||
|
Schema::table('attendances', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['late_minutes', 'daily_fine_minutes', 'daily_fine_amount']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -140,6 +140,7 @@ $usersChunked = $users->chunk(3);
|
|||||||
<p>وظیفه: {{ $u->active_task }}</p>
|
<p>وظیفه: {{ $u->active_task }}</p>
|
||||||
@endif
|
@endif
|
||||||
<p>کارکرد ماه جاری: {{ $u->totaltime }}</p>
|
<p>کارکرد ماه جاری: {{ $u->totaltime }}</p>
|
||||||
|
<p>جریمه تأخیر ماه: {{ number_format($u->monthly_late_fine ?? 0) }} تومان</p>
|
||||||
<p>تعداد درخواست ها: {{ $u->req_counts }}</p>
|
<p>تعداد درخواست ها: {{ $u->req_counts }}</p>
|
||||||
</a>
|
</a>
|
||||||
@if($userType == 'admin')
|
@if($userType == 'admin')
|
||||||
|
|||||||
@@ -1390,8 +1390,67 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .clockpicker-popover {
|
[data-theme="dark"] .clockpicker-popover {
|
||||||
background-color: #334155 !important;
|
background-color: #1e293b !important;
|
||||||
color: #f1f5f9 !important;
|
color: #f1f5f9 !important;
|
||||||
|
border: 1px solid #475569 !important;
|
||||||
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .clockpicker-popover .popover-title {
|
||||||
|
background-color: #0f172a !important;
|
||||||
|
color: #e2e8f0 !important;
|
||||||
|
border-bottom: 1px solid #334155 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .clockpicker-popover .popover-content {
|
||||||
|
background-color: #1e293b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .clockpicker-popover .clockpicker-plate {
|
||||||
|
background-color: #0f172a !important;
|
||||||
|
border-color: #475569 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .clockpicker-popover .clockpicker-tick {
|
||||||
|
color: #e2e8f0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .clockpicker-popover .clockpicker-tick.active,
|
||||||
|
[data-theme="dark"] .clockpicker-popover .clockpicker-tick:hover {
|
||||||
|
background-color: rgba(34, 211, 238, 0.28) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .clockpicker-popover .clockpicker-button,
|
||||||
|
[data-theme="dark"] .clockpicker-popover button.clockpicker-button,
|
||||||
|
[data-theme="dark"] .clockpicker-popover .btn.clockpicker-button,
|
||||||
|
[data-theme="dark"] .clockpicker-popover .btn-default {
|
||||||
|
background-color: #0f172a !important;
|
||||||
|
background-image: none !important;
|
||||||
|
color: #f8fafc !important;
|
||||||
|
border-color: #475569 !important;
|
||||||
|
border-top: 1px solid #475569 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .clockpicker-popover .clockpicker-button:hover,
|
||||||
|
[data-theme="dark"] .clockpicker-popover button.clockpicker-button:hover,
|
||||||
|
[data-theme="dark"] .clockpicker-popover .btn.clockpicker-button:hover,
|
||||||
|
[data-theme="dark"] .clockpicker-popover .btn-default:hover {
|
||||||
|
background-color: #334155 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .clockpicker-popover .clockpicker-canvas-bearing,
|
||||||
|
[data-theme="dark"] .clockpicker-popover .clockpicker-canvas-fg {
|
||||||
|
fill: #22d3ee !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .clockpicker-popover .clockpicker-canvas-bg {
|
||||||
|
fill: rgba(34, 211, 238, 0.35) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .clockpicker-popover .clockpicker-canvas line {
|
||||||
|
stroke: #22d3ee !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .datepicker-plot-area,
|
[data-theme="dark"] .datepicker-plot-area,
|
||||||
|
|||||||
@@ -180,11 +180,33 @@ if (!empty($user->tasks)) {
|
|||||||
<th>ورود</th>
|
<th>ورود</th>
|
||||||
<th>خروج</th>
|
<th>خروج</th>
|
||||||
<th>مدت</th>
|
<th>مدت</th>
|
||||||
|
<th>تأخیر</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@php
|
||||||
|
$monthLateMinutes = 0;
|
||||||
|
$monthDailyFine = 0;
|
||||||
|
$monthKey = $records->first()?->entered_at
|
||||||
|
? \Carbon\Carbon::parse($records->first()->entered_at)
|
||||||
|
: null;
|
||||||
|
if ($monthKey) {
|
||||||
|
[$mStart, $mEnd] = app(\App\Services\LatePenaltyService::class)->jalaliMonthBounds($monthKey);
|
||||||
|
$monthWeeklyFine = (int) \App\Models\LatePeriodFine::query()
|
||||||
|
->where('user_identity', $user->user_name)
|
||||||
|
->where('period_type', 'weekly')
|
||||||
|
->whereBetween('period_start', [$mStart->toDateString(), $mEnd->toDateString()])
|
||||||
|
->sum('fine_amount');
|
||||||
|
} else {
|
||||||
|
$monthWeeklyFine = 0;
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
@foreach($records as $item)
|
@foreach($records as $item)
|
||||||
|
@php
|
||||||
|
$monthLateMinutes += (int) ($item->late_minutes ?? 0);
|
||||||
|
$monthDailyFine += (int) ($item->daily_fine_amount ?? 0);
|
||||||
|
@endphp
|
||||||
<tr class="hover:bg-cyan-500 cursor-pointer transition duration-150"
|
<tr class="hover:bg-cyan-500 cursor-pointer transition duration-150"
|
||||||
style="border-bottom: 1px solid #ddd; background-color: {{$item->requested ? '#bfdbfe' : '#ffffff' }}"
|
style="border-bottom: 1px solid #ddd; background-color: {{$item->requested ? '#bfdbfe' : '#ffffff' }}"
|
||||||
onclick="openModal('{{ $item->id }}', '{{ $item->id }}' , '{{ $item->entered_at }}', '{{ $item->exited_at }} ')">
|
onclick="openModal('{{ $item->id }}', '{{ $item->id }}' , '{{ $item->entered_at }}', '{{ $item->exited_at }} ')">
|
||||||
@@ -218,14 +240,35 @@ if (!empty($user->tasks)) {
|
|||||||
<td style="border-right: 1px solid #ddd; padding: 4px;">
|
<td style="border-right: 1px solid #ddd; padding: 4px;">
|
||||||
{{ $item->entered_at && $item->exited_at ? \Carbon\Carbon::parse($item->duration)->format('H:i') : '-' }}
|
{{ $item->entered_at && $item->exited_at ? \Carbon\Carbon::parse($item->duration)->format('H:i') : '-' }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{{-- تأخیر --}}
|
||||||
|
<td style="border-right: 1px solid #ddd; padding: 4px; {{ ($item->daily_fine_minutes ?? 0) > 0 ? 'color:#dc2626;font-weight:600;' : (($item->late_minutes ?? 0) > 0 ? 'color:#d97706;' : '') }}">
|
||||||
|
@if(($item->late_minutes ?? 0) > 0)
|
||||||
|
{{ $item->late_minutes }}د
|
||||||
|
@if(($item->daily_fine_amount ?? 0) > 0)
|
||||||
|
<span class="text-[10px] block">{{ number_format($item->daily_fine_amount) }}ت</span>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
-
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
{{-- ردیف جمع کل (اختیاری - چون متغیر آن موجود نبود کامنت کردم که خطا ندهد) --}}
|
@php
|
||||||
|
$monthTotalFine = $monthDailyFine + $monthWeeklyFine;
|
||||||
|
@endphp
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="table-summary">
|
<td colspan="6" class="table-summary text-right leading-6">
|
||||||
تعداد رکورد: {{ count($records) }}
|
تعداد رکورد: {{ count($records) }}
|
||||||
|
<br>
|
||||||
|
تأخیر ماه: {{ $monthLateMinutes }} دقیقه
|
||||||
|
<br>
|
||||||
|
جریمه روزانه: {{ number_format($monthDailyFine) }} تومان
|
||||||
|
<br>
|
||||||
|
جریمه هفتگی: {{ number_format($monthWeeklyFine) }} تومان
|
||||||
|
<br>
|
||||||
|
<strong>جمع جریمه ماه: {{ number_format($monthTotalFine) }} تومان</strong>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,21 @@
|
|||||||
pattern="^09\d{9}$"
|
pattern="^09\d{9}$"
|
||||||
class="border border-gray-300 rounded-md text-center" required>
|
class="border border-gray-300 rounded-md text-center" required>
|
||||||
|
|
||||||
|
<label class="pb-3">تاریخ تولد:</label>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<input type="text" id="birthday_display"
|
||||||
|
value="{{ $user->birthday ? \Morilog\Jalali\Jalalian::fromCarbon($user->birthday)->format('Y/m/d') : '' }}"
|
||||||
|
placeholder="اختیاری — انتخاب تاریخ"
|
||||||
|
class="border border-gray-300 rounded-md text-center"
|
||||||
|
autocomplete="off" readonly>
|
||||||
|
<input type="hidden" name="birthday" id="birthday"
|
||||||
|
value="{{ $user->birthday ? $user->birthday->format('Y-m-d') : '' }}">
|
||||||
|
<button type="button" id="clear_birthday"
|
||||||
|
class="text-xs text-gray-500 hover:text-red-500 cursor-pointer self-end">
|
||||||
|
پاک کردن
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="pb-3">چت آیدی تلگرام:</label>
|
<label class="pb-3">چت آیدی تلگرام:</label>
|
||||||
<input type="text" pattern="\d+" name="chatid" value="{{ $user->chatid ?? '' }}"
|
<input type="text" pattern="\d+" name="chatid" value="{{ $user->chatid ?? '' }}"
|
||||||
placeholder="چت آیدی تلگرام"
|
placeholder="چت آیدی تلگرام"
|
||||||
@@ -71,6 +86,27 @@
|
|||||||
<button type="button" id="registerFingerprint" class="...">ثبت اثر انگشت</button>
|
<button type="button" id="registerFingerprint" class="...">ثبت اثر انگشت</button>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('#birthday_display').persianDatepicker({
|
||||||
|
format: 'YYYY/MM/DD',
|
||||||
|
autoClose: true,
|
||||||
|
initialValue: false,
|
||||||
|
toolbox: {
|
||||||
|
calendarSwitch: {enabled: false},
|
||||||
|
todayButton: {enabled: true, text: {fa: 'امروز'}}
|
||||||
|
},
|
||||||
|
onSelect: function (unix) {
|
||||||
|
const gregorian = new persianDate(unix).toCalendar('gregorian').toLocale('en');
|
||||||
|
$('#birthday').val(gregorian.format('YYYY-MM-DD'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#clear_birthday').on('click', function () {
|
||||||
|
$('#birthday_display').val('');
|
||||||
|
$('#birthday').val('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function base64urlToBuffer(base64url) {
|
function base64urlToBuffer(base64url) {
|
||||||
const padding = '='.repeat((4 - base64url.length % 4) % 4);
|
const padding = '='.repeat((4 - base64url.length % 4) % 4);
|
||||||
const base64 = (base64url + padding).replace(/-/g, '+').replace(/_/g, '/');
|
const base64 = (base64url + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
|||||||
@@ -201,6 +201,21 @@ if (isset($task)) {
|
|||||||
updateSelectedCount();
|
updateSelectedCount();
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
function syncGregorianFromDisplay(displaySelector, hiddenSelector) {
|
||||||
|
const displayVal = $(displaySelector).val();
|
||||||
|
if (!displayVal || $(hiddenSelector).val()) return;
|
||||||
|
try {
|
||||||
|
// اگر مقدار نمایشی شمسی YYYY/MM/DD باشد
|
||||||
|
const parts = String(displayVal).replace(/-/g, '/').split('/');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
const pDate = new persianDate([Number(parts[0]), Number(parts[1]), Number(parts[2])]);
|
||||||
|
const gregorian = pDate.toCalendar('gregorian').toLocale('en').format('YYYY-MM-DD');
|
||||||
|
$(hiddenSelector).val(gregorian);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$('#start_date').persianDatepicker({
|
$('#start_date').persianDatepicker({
|
||||||
format: 'YYYY/MM/DD',
|
format: 'YYYY/MM/DD',
|
||||||
autoClose: true,
|
autoClose: true,
|
||||||
@@ -229,6 +244,26 @@ if (isset($task)) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// اگر مقدار اولیه از دیتابیس آمده، فیلد مخفی میلادی را هم پر کن
|
||||||
|
@if(!empty($task?->start_date))
|
||||||
|
$('#start_date_gregorian').val(@json(\Carbon\Carbon::parse($task->start_date)->format('Y-m-d')));
|
||||||
|
$('#start_date').val(@json(\Morilog\Jalali\Jalalian::fromCarbon(\Carbon\Carbon::parse($task->start_date))->format('Y/m/d')));
|
||||||
|
@endif
|
||||||
|
@if(!empty($task?->target_date))
|
||||||
|
$('#target_date_gregorian').val(@json(\Carbon\Carbon::parse($task->target_date)->format('Y-m-d')));
|
||||||
|
$('#target_date').val(@json(\Morilog\Jalali\Jalalian::fromCarbon(\Carbon\Carbon::parse($task->target_date))->format('Y/m/d')));
|
||||||
|
@endif
|
||||||
|
|
||||||
|
$('form').on('submit', function (e) {
|
||||||
|
syncGregorianFromDisplay('#start_date', '#start_date_gregorian');
|
||||||
|
syncGregorianFromDisplay('#target_date', '#target_date_gregorian');
|
||||||
|
if (!$('#start_date_gregorian').val()) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('لطفاً تاریخ شروع را از تقویم انتخاب کنید.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$('#clear_target_date').on('click', function () {
|
$('#clear_target_date').on('click', function () {
|
||||||
$('#target_date').val('');
|
$('#target_date').val('');
|
||||||
$('#target_date_gregorian').val('');
|
$('#target_date_gregorian').val('');
|
||||||
|
|||||||
@@ -1077,6 +1077,14 @@ $state = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toDatetimeLocalValue(iso) {
|
||||||
|
if (!iso) return '';
|
||||||
|
const d = new Date(iso);
|
||||||
|
if (Number.isNaN(d.getTime())) return '';
|
||||||
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderTaskDurations(task) {
|
function renderTaskDurations(task) {
|
||||||
const list = document.getElementById('modalDurationsList');
|
const list = document.getElementById('modalDurationsList');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
@@ -1102,7 +1110,9 @@ $state = [
|
|||||||
const start = d.start ? new Date(d.start).toLocaleString('fa-IR') : '-';
|
const start = d.start ? new Date(d.start).toLocaleString('fa-IR') : '-';
|
||||||
const end = d.end ? new Date(d.end).toLocaleString('fa-IR') : 'در حال انجام';
|
const end = d.end ? new Date(d.end).toLocaleString('fa-IR') : 'در حال انجام';
|
||||||
const dur = d.duration || (d.end ? '-' : '...');
|
const dur = d.duration || (d.end ? '-' : '...');
|
||||||
return `
|
|
||||||
|
if (!isAdminLevel1) {
|
||||||
|
return `
|
||||||
<div class="ui-panel border border-gray-200 rounded-lg p-2">
|
<div class="ui-panel border border-gray-200 rounded-lg p-2">
|
||||||
<div class="text-xs text-gray-500 flex justify-between">
|
<div class="text-xs text-gray-500 flex justify-between">
|
||||||
<span>${escapeHtml(uname)}</span>
|
<span>${escapeHtml(uname)}</span>
|
||||||
@@ -1112,9 +1122,92 @@ $state = [
|
|||||||
شروع: ${escapeHtml(start)}<br/>
|
شروع: ${escapeHtml(start)}<br/>
|
||||||
پایان: ${escapeHtml(end)}
|
پایان: ${escapeHtml(end)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>`;
|
||||||
`;
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="ui-panel border border-gray-200 rounded-lg p-2" data-duration-id="${d.id}">
|
||||||
|
<div class="text-xs text-gray-500 flex justify-between mb-2">
|
||||||
|
<span>${escapeHtml(uname)}</span>
|
||||||
|
<span class="font-mono duration-value">${escapeHtml(dur)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-2 text-xs">
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span>شروع</span>
|
||||||
|
<input type="datetime-local"
|
||||||
|
class="duration-start ui-input text-sm"
|
||||||
|
value="${toDatetimeLocalValue(d.start)}">
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col gap-1">
|
||||||
|
<span>پایان (خالی = در حال انجام)</span>
|
||||||
|
<input type="datetime-local"
|
||||||
|
class="duration-end ui-input text-sm"
|
||||||
|
value="${toDatetimeLocalValue(d.end)}">
|
||||||
|
</label>
|
||||||
|
<button type="button"
|
||||||
|
class="save-duration-btn bg-cyan-600 hover:bg-cyan-700 text-white rounded-lg py-1.5 px-3">
|
||||||
|
ذخیره زمان
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
if (isAdminLevel1) {
|
||||||
|
list.querySelectorAll('.save-duration-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async function () {
|
||||||
|
const card = this.closest('[data-duration-id]');
|
||||||
|
if (!card) return;
|
||||||
|
const durationId = card.getAttribute('data-duration-id');
|
||||||
|
const startVal = card.querySelector('.duration-start')?.value || '';
|
||||||
|
const endVal = card.querySelector('.duration-end')?.value || '';
|
||||||
|
|
||||||
|
if (!startVal) {
|
||||||
|
alert('زمان شروع الزامی است.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disabled = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch("{{ route('task.duration.edit') }}", {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
duration_id: Number(durationId),
|
||||||
|
start: startVal,
|
||||||
|
end: endVal || null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok || !data.status) {
|
||||||
|
alert(data.message || 'خطا در ذخیره زمان');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTaskDetails && Array.isArray(currentTaskDetails.duration)) {
|
||||||
|
const idx = currentTaskDetails.duration.findIndex(x => String(x.id) === String(durationId));
|
||||||
|
if (idx >= 0) {
|
||||||
|
currentTaskDetails.duration[idx] = data.duration;
|
||||||
|
}
|
||||||
|
if (data.finished_at !== undefined) {
|
||||||
|
currentTaskDetails.finished_at = data.finished_at;
|
||||||
|
}
|
||||||
|
renderTaskDurations(currentTaskDetails);
|
||||||
|
updateModalTotalDuration(currentTaskDetails);
|
||||||
|
}
|
||||||
|
alert(data.message || 'ذخیره شد');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert('خطا در ارتباط با سرور');
|
||||||
|
} finally {
|
||||||
|
this.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchDetailsTab(tab) {
|
function switchDetailsTab(tab) {
|
||||||
@@ -1262,6 +1355,7 @@ $state = [
|
|||||||
? formatDateTime(task.created_at)
|
? formatDateTime(task.created_at)
|
||||||
: '-';
|
: '-';
|
||||||
document.getElementById('modalTaskAssignees').textContent = formatAssigneeNames(task);
|
document.getElementById('modalTaskAssignees').textContent = formatAssigneeNames(task);
|
||||||
|
updateTaskDateViews(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
// باز کردن مودال جزئیات تسک
|
// باز کردن مودال جزئیات تسک
|
||||||
@@ -1280,8 +1374,7 @@ $state = [
|
|||||||
// پر کردن فیلدهای مودال
|
// پر کردن فیلدهای مودال
|
||||||
document.getElementById('modalTaskTitle').textContent = task.title || 'بدون عنوان';
|
document.getElementById('modalTaskTitle').textContent = task.title || 'بدون عنوان';
|
||||||
document.getElementById('modalTaskDescription').innerHTML = task.description || 'بدون توضیحات';
|
document.getElementById('modalTaskDescription').innerHTML = task.description || 'بدون توضیحات';
|
||||||
document.getElementById('modalTaskStartDateView').textContent = task.start_date ? formatJalaliDate(task.start_date) : '-';
|
updateTaskDateViews(task);
|
||||||
document.getElementById('modalTaskTargetDateView').textContent = task.target_date ? formatJalaliDate(task.target_date) : '-';
|
|
||||||
updateTaskFinishedAtView(task);
|
updateTaskFinishedAtView(task);
|
||||||
updateModalInfoFields(task);
|
updateModalInfoFields(task);
|
||||||
|
|
||||||
@@ -1497,8 +1590,8 @@ $state = [
|
|||||||
sdEdit.value = currentTaskDetails.start_date ? formatJalaliDate(currentTaskDetails.start_date) : '';
|
sdEdit.value = currentTaskDetails.start_date ? formatJalaliDate(currentTaskDetails.start_date) : '';
|
||||||
tdEdit.value = currentTaskDetails.target_date ? formatJalaliDate(currentTaskDetails.target_date) : '';
|
tdEdit.value = currentTaskDetails.target_date ? formatJalaliDate(currentTaskDetails.target_date) : '';
|
||||||
|
|
||||||
if (sdGreg) sdGreg.value = currentTaskDetails.start_date || '';
|
if (sdGreg) sdGreg.value = normalizeDateValue(currentTaskDetails.start_date);
|
||||||
if (tdGreg) tdGreg.value = currentTaskDetails.target_date || '';
|
if (tdGreg) tdGreg.value = normalizeDateValue(currentTaskDetails.target_date);
|
||||||
|
|
||||||
initModalDatepickers();
|
initModalDatepickers();
|
||||||
}
|
}
|
||||||
@@ -1638,15 +1731,52 @@ $state = [
|
|||||||
modal.style.display = 'none';
|
modal.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// فرمت تاریخ شمسی (ساده)
|
|
||||||
function formatJalaliDate(dateStr) {
|
function formatJalaliDate(dateStr) {
|
||||||
if (!dateStr) return '-';
|
if (!dateStr) return '-';
|
||||||
// فرض میکنیم از کتابخانه Jalalian در blade استفاده میکنید، اما در js نیز میتوان پیادهسازی کرد
|
const raw = String(dateStr).trim();
|
||||||
// برای سادگی از Intl یا یک تبدیل ساده استفاده میکنیم
|
if (!raw || raw.startsWith('0000-00-00')) return '-';
|
||||||
const d = new Date(dateStr);
|
|
||||||
if (isNaN(d)) return dateStr;
|
if (typeof persianDate !== 'undefined') {
|
||||||
// این قسمت میتواند با کتابخانه moment-jalaali جایگزین شود
|
try {
|
||||||
return d.toLocaleDateString('fa-IR'); // نیاز به تنظیم locale مرورگر دارد
|
const m = raw.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||||
|
if (m) {
|
||||||
|
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
||||||
|
return new persianDate(d)
|
||||||
|
.toCalendar('persian')
|
||||||
|
.toLocale('en')
|
||||||
|
.format('YYYY/MM/DD');
|
||||||
|
}
|
||||||
|
const d = new Date(raw);
|
||||||
|
if (!Number.isNaN(d.getTime())) {
|
||||||
|
return new persianDate(d)
|
||||||
|
.toCalendar('persian')
|
||||||
|
.toLocale('en')
|
||||||
|
.format('YYYY/MM/DD');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const d = new Date(raw.includes('T') || raw.includes(' ') ? raw : (raw + 'T00:00:00'));
|
||||||
|
if (Number.isNaN(d.getTime())) return raw;
|
||||||
|
return d.toLocaleDateString('fa-IR');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDateValue(dateStr) {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
const m = String(dateStr).match(/^(\d{4}-\d{2}-\d{2})/);
|
||||||
|
return m ? m[1] : String(dateStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTaskDateViews(task) {
|
||||||
|
const sdView = document.getElementById('modalTaskStartDateView');
|
||||||
|
const tdView = document.getElementById('modalTaskTargetDateView');
|
||||||
|
if (sdView) {
|
||||||
|
sdView.textContent = task.start_date ? formatJalaliDate(task.start_date) : '-';
|
||||||
|
}
|
||||||
|
if (tdView) {
|
||||||
|
tdView.textContent = task.target_date ? formatJalaliDate(task.target_date) : '-';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateTime(dateStr) {
|
function formatDateTime(dateStr) {
|
||||||
|
|||||||
@@ -2,7 +2,19 @@
|
|||||||
|
|
||||||
use Illuminate\Foundation\Inspiring;
|
use Illuminate\Foundation\Inspiring;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
|
||||||
Artisan::command('inspire', function () {
|
Artisan::command('inspire', function () {
|
||||||
$this->comment(Inspiring::quote());
|
$this->comment(Inspiring::quote());
|
||||||
})->purpose('Display an inspiring quote');
|
})->purpose('Display an inspiring quote');
|
||||||
|
|
||||||
|
Schedule::command('birthdays:notify')->dailyAt('09:00');
|
||||||
|
|
||||||
|
// گزارش تأخیر روز قبل — هر روز ساعت ۲۲:۳۰
|
||||||
|
Schedule::command('late:daily-report')->dailyAt('22:30');
|
||||||
|
|
||||||
|
// گزارش هفته (شنبه تا جمعه) — جمعهشب ساعت ۲۳:۰۰
|
||||||
|
Schedule::command('late:weekly-report')->weeklyOn(5, '23:00');
|
||||||
|
|
||||||
|
// گزارش ماه شمسی قبل — هر روز چک میشود؛ فقط روز اول ماه بعد اجرا شود
|
||||||
|
Schedule::command('late:monthly-report')->dailyAt('23:15');
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ Route::get('/tasks', [TaskController::class, 'Tasks']);
|
|||||||
|
|
||||||
Route::post('/active_task', [TaskController::class, 'activeTask'])->name('task.active');
|
Route::post('/active_task', [TaskController::class, 'activeTask'])->name('task.active');
|
||||||
|
|
||||||
|
Route::post('/task/duration/edit', [TaskController::class, 'updateTaskDuration'])->name('task.duration.edit');
|
||||||
|
|
||||||
Route::get('/create_task', [TaskController::class, 'createTask'])->name('task.create');
|
Route::get('/create_task', [TaskController::class, 'createTask'])->name('task.create');
|
||||||
|
|
||||||
Route::get('/task/{id}/details', [TaskController::class, 'taskDetailsJson'])->name('task.details');
|
Route::get('/task/{id}/details', [TaskController::class, 'taskDetailsJson'])->name('task.details');
|
||||||
|
|||||||
Reference in New Issue
Block a user