Compare commits

...
27 Commits
Author SHA1 Message Date
Mahdi ce2ce49ffb Editing Employee Attendance and Check-ins 2026-10-07 17:51:03 +03:30
Mahdi 694f41968a fix office location detection radius and GPS accuracy 2026-09-23 13:07:21 +03:30
Mahdi 9e6407d648 Editing tabs and the months modal 2026-09-23 12:44:03 +03:30
Mahdi 0fd5867701 Changes to holidays and the overlap between this year and the previous year 2026-09-23 12:20:39 +03:30
Mahdi 5e306564b2 Edit equest for leave 2026-07-26 11:02:22 +03:30
Mahdi 280d2748f9 equest for leave 2026-07-26 10:49:48 +03:30
Mahdi c54038d986 Edit start date 2026-07-25 11:51:27 +03:30
Mahdi 3c0a797485 Edit task creation start time and server error 2026-07-25 11:43:04 +03:30
Mahdi 2c811c0d8c Edit task creation start time 2026-07-25 11:34:23 +03:30
MahdiandCursor 49c9a45e82 Remove unused Jalalian import from TaskController.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 18:05:02 +03:30
MahdiandCursor 54e995dadd Fix Jalali crash when creating task with invalid target_date.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 18:04:19 +03:30
MahdiandCursor 6caee30d9b Fix Jalali year-0 crash on /tasks from invalid target_date.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 17:56:39 +03:30
Mahdi dc276dfbb5 edit delay error 2026-07-22 17:48:22 +03:30
Mahdi e9f397d759 edit delay and penalty calculations 2026-07-22 17:38:35 +03:30
Mahdi e1f86e6ffe Adding delay and penalty calculations 2026-07-22 14:10:05 +03:30
Mahdi 28bf82cde9 edit form add task 2026-06-03 14:22:58 +03:30
Mahdi 41d1036300 edit filter time 2026-06-02 22:17:47 +03:30
Mahdi 300575930f Improve tasks UI, dark theme, performance fixes, and admin pages polish 2026-06-02 18:45:01 +03:30
Montzar.ah.01 54aa8f40dd commit some uncommited file 2026-06-02 07:45:28 +00:00
Montzar.ah.01 93a0d515ed NASER fix issue 2026-05-05 11:06:12 +00:00
Montzar.ah.01 c3dede5c6f NASER send in bale except telegram 2026-05-03 14:50:16 +00:00
Montzar.ah.01 afd07f611a NASER refresh page after close task detail modal and redirect to tasks list after create task 2026-05-03 13:29:25 +00:00
Montzar.ah.01 f4ce38e560 NASER change task orders and make daily report and end task when emploee exit 2026-04-28 19:53:46 +00:00
Montzar.ah.01 d1d092359e NASER fix send bale message 2026-04-27 16:48:01 +00:00
Montzar.ah.01 a285bcceac NASER fix some issue 2026-04-25 19:45:15 +00:00
Montzar.ah.01 cf50c3371f NASER add type for apps list 2026-04-25 12:54:33 +00:00
Montzar.ah.01 c159163601 NASER fix bale issue 2026-04-25 11:46:02 +00:00
30348 changed files with 6642 additions and 1248 deletions
Regular → Executable
View File
@@ -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,89 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Morilog\Jalali\Jalalian;
class RemindHolidayCalendarUpdate extends Command
{
protected $signature = 'holidays:remind';
protected $description = 'یادآوری ثبت تعطیلات رسمی سال‌های بعد از اتمام پوشش تقویم پروژه';
public function handle(): int
{
$coveredUntil = SeedIranianHolidays::coveredUntilYear();
if ($coveredUntil < 1300) {
$this->warn('فایل تعطیلات خالی یا نامعتبر است.');
return self::FAILURE;
}
$now = Jalalian::now();
$currentYear = $now->getYear();
// از ۲۰ اسفند سال پوشش‌داده‌شده، یا هر زمان بعد از آن سال
$shouldRemind = $currentYear > $coveredUntil
|| ($currentYear === $coveredUntil && $now->getMonth() === 12 && $now->getDay() >= 20);
if (!$shouldRemind) {
$this->info("هنوز نیازی به یادآوری نیست (پوشش تا {$coveredUntil}، الان {$currentYear}).");
return self::SUCCESS;
}
$yearsToRequest = [];
for ($y = $coveredUntil + 1; $y <= max($currentYear + 1, $coveredUntil + 1); $y++) {
$yearsToRequest[] = $y;
}
$yearsToRequest = array_values(array_unique($yearsToRequest));
$yearsLabel = implode(' و ', $yearsToRequest);
$cacheKey = 'holidays_remind_' . implode('_', $yearsToRequest);
if (Cache::has($cacheKey)) {
$this->info('یادآوری اخیراً ارسال شده؛ این ماه دوباره ارسال نمی‌شود.');
return self::SUCCESS;
}
$message = "📅 یادآوری تعطیلات رسمی\n\n"
. "پوشش تعطیلات در سیستم تا سال {$coveredUntil} است.\n"
. "لطفاً تعطیلات رسمی سال {$yearsLabel} را طبق تقویم رسمی ایران به فایل\n"
. "database/data/iranian_official_holidays.php\n"
. "اضافه کنید و سپس اجرا کنید:\n"
. "php artisan holidays:seed";
$this->notifyAdmins($message);
Cache::put($cacheKey, true, now()->addDays(30));
$this->info("یادآوری برای سال(های) {$yearsLabel} به ادمین‌ها ارسال شد.");
return self::SUCCESS;
}
private function notifyAdmins(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,71 @@
<?php
namespace App\Console\Commands;
use App\Models\Holiday;
use Illuminate\Console\Command;
use Morilog\Jalali\Jalalian;
class SeedIranianHolidays extends Command
{
protected $signature = 'holidays:seed {--year= : فقط یک سال شمسی مشخص}';
protected $description = 'ثبت/به‌روزرسانی تعطیلات رسمی ایران از فایل تقویم پروژه';
public function handle(): int
{
$path = database_path('data/iranian_official_holidays.php');
if (!is_file($path)) {
$this->error('فایل تعطیلات یافت نشد: ' . $path);
return self::FAILURE;
}
/** @var array<int, list<array{month:int, day:int, title:string}>> $years */
$years = require $path;
$onlyYear = $this->option('year') !== null ? (int) $this->option('year') : null;
$count = 0;
foreach ($years as $jalaliYear => $days) {
if ($onlyYear !== null && (int) $jalaliYear !== $onlyYear) {
continue;
}
foreach ($days as $day) {
try {
$gregorian = (new Jalalian((int) $jalaliYear, (int) $day['month'], (int) $day['day']))
->toCarbon()
->format('Y-m-d');
} catch (\Throwable $e) {
$this->warn("رد شد: {$jalaliYear}/{$day['month']}/{$day['day']} — {$e->getMessage()}");
continue;
}
Holiday::updateOrCreate(
['date' => $gregorian],
['title' => $day['title']]
);
$count++;
}
$this->info("سال {$jalaliYear}: " . count($days) . ' روز');
}
$this->info("جمع: {$count} تعطیل رسمی ثبت/به‌روز شد.");
return self::SUCCESS;
}
/** آخرین سال شمسی پوشش‌داده‌شده در فایل داده */
public static function coveredUntilYear(): int
{
$path = database_path('data/iranian_official_holidays.php');
if (!is_file($path)) {
return 0;
}
$years = array_keys(require $path);
return $years ? (int) max($years) : 0;
}
}
@@ -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;
}
}
+426 -83
View File
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Models\LeaveRequest;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
@@ -18,10 +19,350 @@ use Morilog\Jalali\Jalalian;
use GuzzleHttp\Client;
use App\Models\Tasks;
use App\Models\Pivot_Tasks;
use App\Services\LatePenaltyService;
class AdminController extends Controller
{
private function redisIsAvailable(): bool
{
try {
Redis::connection()->ping();
return true;
} catch (\Throwable) {
return false;
}
}
private function appendDecodedRequest(array &$target, ?string $raw): void
{
if (!$raw) {
return;
}
$request = json_decode($raw);
if ($request && ($request->function ?? null) !== 'rejected') {
$target[] = $request;
}
}
private function appendDecodedRequestWithAttendance(array &$target, ?string $raw, ?Attendance $attendance = null): void
{
if (!$raw) {
return;
}
$request = json_decode($raw);
if ($request && ($request->function ?? null) !== 'rejected') {
$target[] = $this->enrichRequestForView($request, $attendance);
}
}
private function buildFallbackRequestFromAttendance(Attendance $attendance): object
{
$request = (object) [
'function' => 'edit',
'userid' => $attendance->user_identity,
'id' => $attendance->id,
'oldenter' => $attendance->entered_at ? Carbon::parse($attendance->entered_at)->format('H:i') : '',
'newenter' => '',
'oldexit' => $attendance->exited_at ? Carbon::parse($attendance->exited_at)->format('H:i') : '',
'newexit' => '',
'date' => $attendance->entered_at
? Jalalian::fromCarbon(Carbon::parse($attendance->entered_at))->format('Y/m/d')
: null,
];
$request->display_date = $this->formatRequestDisplayDate($request, $attendance);
return $request;
}
private function formatRequestDisplayDate(object $request, ?Attendance $attendance = null): string
{
$candidates = array_filter([
$attendance?->entered_at,
(!empty($request->oldenter) && str_contains((string) $request->oldenter, ' '))
? $request->oldenter
: null,
$request->date ?? null,
]);
foreach ($candidates as $value) {
try {
if (is_string($value) && preg_match('/^\d{4}\/\d{2}\/\d{2}$/', $value)) {
$year = (int) substr($value, 0, 4);
if ($year >= 1700) {
return Jalalian::fromCarbon(Carbon::createFromFormat('Y/m/d', $value))->format('Y/m/d');
}
if ($year >= 1300) {
return Jalalian::fromFormat('Y/m/d', $value)->format('Y/m/d');
}
}
$carbon = Carbon::parse($value);
if ($carbon->year >= 1000) {
return Jalalian::fromCarbon($carbon)->format('Y/m/d');
}
} catch (\Throwable) {
continue;
}
}
return '-';
}
private function enrichRequestForView(object $request, ?Attendance $attendance = null): object
{
if (!isset($request->display_date)) {
$request->display_date = $this->formatRequestDisplayDate($request, $attendance);
}
return $request;
}
private function resolveRequestTime(?string $preferred, ?string $fallback, $recordDefault = null): ?string
{
$value = ($preferred !== null && $preferred !== '') ? $preferred : $fallback;
if ($value === null || $value === '') {
if ($recordDefault) {
return Carbon::parse($recordDefault)->format('H:i:s');
}
return null;
}
return Carbon::parse($value)->format('H:i:s');
}
private function buildAttendanceTimestamp(string $date, ?string $time): ?string
{
if (!$time) {
return null;
}
return $date . ' ' . $time;
}
private function decrementUserRequestCount(?string $userName): void
{
if (!$userName) {
return;
}
User::where('user_name', $userName)
->where('req_counts', '>', 0)
->decrement('req_counts');
}
private function loadPendingRequests($users): array
{
$userNames = $users->pluck('user_name')->filter()->values();
$redisAvailable = $this->redisIsAvailable();
$pendingByUser = Attendance::query()
->whereIn('user_identity', $userNames)
->whereNotNull('requested')
->where('requested', '!=', '')
->get(['id', 'user_identity', 'entered_at', 'exited_at'])
->groupBy('user_identity');
$records = [];
foreach ($users as $user) {
$rec = [
'user_name' => $user->user_name,
'name' => $user->name,
'attendances' => [],
];
$pendingForUser = $pendingByUser->get($user->user_name, collect());
$hasRequestHint = ($user->req_counts ?? 0) > 0 || $pendingForUser->isNotEmpty();
if (!$hasRequestHint) {
$records[] = $rec;
continue;
}
$loadedIds = [];
if ($redisAvailable) {
foreach (Redis::lrange("att-tempreq:creates:{$user->user_name}", 0, -1) as $reqJson) {
$this->appendDecodedRequestWithAttendance($rec['attendances'], $reqJson);
}
$this->appendDecodedRequestWithAttendance($rec['attendances'], Redis::get("att-tempreq:{$user->user_name}:new"));
if ($pendingForUser->isNotEmpty()) {
$redisKeys = $pendingForUser
->map(fn ($attendance) => "att-tempreq:{$user->user_name}:{$attendance->id}")
->values()
->all();
$rawValues = Redis::mget($redisKeys);
foreach ($pendingForUser->values() as $index => $attendance) {
$raw = $rawValues[$index] ?? null;
if (!$raw) {
// Redis موجود است ولی کلید نیست → درخواست قبلاً تایید/رد شده؛ پرچم کهنه را پاک کن
$attendance->requested = null;
$attendance->save();
continue;
}
$decoded = json_decode($raw);
if (($decoded->function ?? null) === 'rejected') {
Redis::del("att-tempreq:{$user->user_name}:{$attendance->id}");
$attendance->requested = null;
$attendance->save();
continue;
}
$beforeCount = count($rec['attendances']);
$this->appendDecodedRequestWithAttendance($rec['attendances'], $raw, $attendance);
if (count($rec['attendances']) > $beforeCount) {
$loadedIds[(string) $attendance->id] = true;
}
}
}
}
// فقط وقتی Redis در دسترس نیست از fallback استفاده کن
if (!$redisAvailable) {
foreach ($pendingForUser as $attendance) {
if (isset($loadedIds[(string) $attendance->id])) {
continue;
}
$rec['attendances'][] = $this->buildFallbackRequestFromAttendance($attendance);
}
}
$pendingById = $pendingForUser->keyBy('id');
foreach ($rec['attendances'] as $index => $request) {
$attendance = $pendingById->get($request->id ?? null);
$rec['attendances'][$index] = $this->enrichRequestForView($request, $attendance);
}
$records[] = $rec;
}
return $records;
}
private function removeCreateRequest(string $userid, object $data): void
{
$key = "att-tempreq:creates:{$userid}";
try {
$items = Redis::lrange($key, 0, -1);
} catch (\Throwable) {
return;
}
foreach ($items as $item) {
$decoded = json_decode($item);
if (!$decoded) {
continue;
}
$sameUser = ($decoded->userid ?? null) === ($data->userid ?? null);
$sameEnter = (string) ($decoded->oldenter ?? '') === (string) ($data->oldenter ?? '');
$sameExit = (string) ($decoded->oldexit ?? '') === (string) ($data->oldexit ?? '');
if ($sameUser && $sameEnter && $sameExit) {
Redis::lrem($key, 1, $item);
}
}
Redis::del("att-tempreq:{$userid}:new");
}
private function clearAttendanceRequestFlag($attendanceId): void
{
if (!$attendanceId || $attendanceId === 'new') {
return;
}
$att = Attendance::find($attendanceId);
if ($att) {
$att->requested = null;
$att->save();
}
}
private function enrichUsersForDashboard($users): void
{
$userNames = $users->pluck('user_name');
$now = Carbon::now();
$monthStart = $now->copy()->startOfMonth();
$monthEnd = $now->copy()->endOfMonth();
$today = $now->format('Y-m-d');
$monthAttendances = Attendance::query()
->whereIn('user_identity', $userNames)
->whereBetween('entered_at', [$monthStart, $monthEnd])
->whereNotNull('duration')
->select(['user_identity', 'duration'])
->get()
->groupBy('user_identity');
$openAttendances = Attendance::query()
->whereIn('user_identity', $userNames)
->whereNull('exited_at')
->select(['id', 'user_identity', 'entered_at'])
->get()
->groupBy('user_identity');
$activeBreakAttendanceIds = AttendanceBreak::query()
->whereIn('attendance_id', $openAttendances->flatten()->pluck('id'))
->whereNull('break_end')
->pluck('attendance_id')
->flip();
foreach ($users as $user) {
$totalSeconds = 0;
foreach ($monthAttendances->get($user->user_name, []) as $attendance) {
if (!$attendance->duration) {
continue;
}
$parts = explode(':', $attendance->duration);
if (count($parts) !== 3) {
continue;
}
[$h, $m, $s] = array_map('intval', $parts);
$totalSeconds += ($h * 3600) + ($m * 60) + $s;
}
$hours = intdiv($totalSeconds, 3600);
$minutes = intdiv($totalSeconds % 3600, 60);
$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;
$openForUser = $openAttendances->get($user->user_name, collect());
if ($openForUser->isEmpty()) {
continue;
}
$latestOpen = $openForUser->sortByDesc('entered_at')->first();
if ($latestOpen && Carbon::parse($latestOpen->entered_at)->format('Y-m-d') === $today) {
$user->working = 1;
}
foreach ($openForUser as $openAttendance) {
if ($activeBreakAttendanceIds->has($openAttendance->id)) {
$user->working = 2;
break;
}
}
}
}
public function dashboard()
{
@@ -29,66 +370,56 @@ class AdminController extends Controller
$uname = $_COOKIE['token-0'];
setcookie("user_identity", $_COOKIE['token-0'], time() + (60 * 60 * 24 * 365), '/');
Cookie::queue(Cookie::forget('token-0'));
} else
$uname = $_COOKIE['user_identity'];
} else {
$uname = $_COOKIE['user_identity'] ?? null;
}
if (!$uname) {
return redirect('/')->with('message', [
'color' => '#ff8800ff',
'text' => 'ابتدا باید وارد شوید!'
]);
}
$u = User::select('name', 'is_admin')
->where('user_name', $uname)
->first();
if (!$u) {
return redirect('/')->with('message', [
'color' => '#ff8800ff',
'text' => 'کاربر یافت نشد!'
]);
}
if ($u->is_admin == 1) {
$provider = 'admin';
} elseif ($u->is_admin == 2) {
$provider = 'supervisor';
}
if ($u->is_admin == '1' || $u->is_admin == '2') {
setcookie('ad', '1', time() + (60 * 60 * 24 * 365 * 5), "/");
$users = User::wherenotnull('user_name')
->with('attendances')
$users = User::whereNotNull('user_name')
->select(['id', 'name', 'user_name', 'req_counts', 'active_task', 'work_start_time'])
->get();
$records = [];
foreach ($users as $user) {
$rec = [
"user_name" => $user->user_name,
"name" => $user->name,
"attendances" => []
];
foreach ($user->attendances as $attendance) {
$key = "att-tempreq:{$user->user_name}:{$attendance->id}";
$get = json_decode(Redis::get("{$key}"));
$date = $attendance->entered_at ? Carbon::parse($attendance->entered_at)->format('Y-m-d') : null;
if ($get)
if ($get->function != 'rejected')
$rec['attendances'][] = $get;
}
// $key = "att-tempreq:{$user->user_name}:new";
$create_requests_key = "att-tempreq:creates:{$user->user_name}";
$create_requests = Redis::connection()->lrange($create_requests_key, 0, -1); // خواندن تمام آیتم های لیست
$this->enrichUsersForDashboard($users);
$users = $users->sortByDesc('working');
$records = $this->loadPendingRequests($users);
$leaveRequests = LeaveRequest::query()
->where('status', 'pending')
->with('user:id,name,user_name')
->orderByDesc('created_at')
->get();
foreach ($create_requests as $req_json) {
$get = json_decode($req_json);
if ($get && $get->function != 'rejected') {
$rec['attendances'][] = $get;
}
}
// $get = json_decode(Redis::get("{$key}"));
// if ($get) {
//// dd($get);
//
// if ($get->function != 'rejected')
//
// $rec['attendances'][] = $get;
// }
$records[] = $rec;
}
// dd($users);
// dd(json_decode($json,true));
return view("admin..admin")->with(['message', [
'color' => '#ff8800ff',
"text" => 'شما به پنل ادمین وارد شدید✔️'
],
"users" => $users,
'reqs' => $records,
'leaveRequests' => $leaveRequests,
"userType" => $provider
]);
}
@@ -169,6 +500,7 @@ class AdminController extends Controller
$attendance->exited_at = $nendt ? $date . ' ' . $nendt : null;
$attendance->duration = $nendt !== null ? $attendance->duration($nstartt, $nendt) : null;
$attendance->save();
app(LatePenaltyService::class)->syncAttendance($attendance);
return redirect()->back()->with('message', [
'color' => '#6aff00ff',
"text" => 'تغییرات با موفقیت ذخیره شد.✔️'
@@ -195,6 +527,7 @@ class AdminController extends Controller
$attendance->duration = $nendt ? $attendance->duration($nstartt, $nendt) : null;
$attendance->save();
app(LatePenaltyService::class)->syncAttendance($attendance);
return redirect()->back()->with('message', [
'color' => '#6aff00ff',
"text" => 'تغییرات با موفقیت ذخیره شد.✔️'
@@ -248,6 +581,7 @@ class AdminController extends Controller
// Redis::connection()->lrem($create_requests_key, 1, json_encode($data));
Redis::connection()->del("att-tempreq:{$data->userid}:{$data->id}");
$this->decrementUserRequestCount($data->userid);
return redirect()->back()->with('message', [
'color' => '#6aff00ff',
'text' => 'رکورد با موفقیت حذف شد✔️'
@@ -265,43 +599,46 @@ class AdminController extends Controller
]);
}
} elseif ($data->function == 'edit') {
// dd($data);
$record = Attendance::find($data->id);
if (!$record) {
return redirect()->back()->with('message', [
'color' => '#ff8800ff',
'text' => 'رکورد پیدا نشد✖️'
]);
}
$attendance = new Attendance();
$date = Carbon::parse($record->entered_at)->format('Y-m-d');
$enterTime = $this->resolveRequestTime($data->newenter ?? null, $data->oldenter ?? null, $record->entered_at);
$exitTime = $this->resolveRequestTime($data->newexit ?? null, $data->oldexit ?? null, $record->exited_at);
$record->ip_address = 'admin-edited';
$record->user_identity = $data->userid;
$record->entered_at = $data->newenter ? $date . ' ' . $data->newenter : null;
if ($data->newexit != null) {
//جریمه
// if($data->newexit > '16:00:00'){
// $data->newexit = date('H:i:s', strtotime($data->newexit . ' -1 hour'));
// }
$record->exited_at = $date . ' ' . $data->newexit;
$duration = $attendance->duration($data->newenter, $data->newexit);
$record->entered_at = $this->buildAttendanceTimestamp($date, $enterTime) ?? $record->entered_at;
if ($exitTime) {
$record->exited_at = $this->buildAttendanceTimestamp($date, $exitTime);
$duration = $attendance->duration($enterTime, $exitTime);
} else {
$record->exited_at = null;
$record->duration = null;
$duration = null;
}
$break = AttendanceBreak::where('attendance_id', $data->id)->sum('duration');
if ($break) {
if ($break && $duration) {
$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 -= $breakSec;
$duration = gmdate("H:i:s", $durationSec);
}
$record->duration = $duration ?? $record->duration;
//dd($record->duration, $duration, $break);
$record->requested = '';
$record->requested = null;
$record->save();
// $create_requests_key = "att-tempreq:creates:{$data->userid}";
// $create_requests_key = "att-tempreq:{$data->userid}:{$data->id}";
// $t = Redis::connection()->lrem($create_requests_key, -1, json_encode($data));
app(LatePenaltyService::class)->syncAttendance($record);
Redis::connection()->del("att-tempreq:{$data->userid}:{$data->id}");
$this->decrementUserRequestCount($data->userid);
tel_notif($record->user_identity, "تایید");
@@ -310,21 +647,33 @@ class AdminController extends Controller
"text" => 'رکورد با موفقیت ذخیره شد.✔️'
]);
} elseif ($data->function == 'new') {
// dd($data);
$starttime = $data->oldenter ? Carbon::parse($data->oldenter)->format('H:i:s') : null;
$endtime = $data->oldexit ? Carbon::parse($data->oldexit)->format('H:i:s') : null;
$enteredAt = !empty($data->oldenter) ? Carbon::parse($data->oldenter) : null;
$exitedAt = !empty($data->oldexit) ? Carbon::parse($data->oldexit) : null;
if (!$enteredAt) {
return redirect()->back()->with('message', [
'color' => '#ff8800ff',
'text' => 'زمان ورود در درخواست مشخص نیست✖️'
]);
}
$starttime = $enteredAt->format('H:i:s');
$endtime = $exitedAt?->format('H:i:s');
if (is_null($endtime) || $endtime > $starttime) {
$attendance = new Attendance();
$attendance->ip_address = 'admin-new';
$attendance->user_identity = $data->userid;
$attendance->entered_at = $data->oldenter ?: null;
$attendance->exited_at = $data->oldexit ?: null;
$attendance->duration = $data->oldexit ? $attendance->duration($data->oldenter, $data->oldexit) : null;
$attendance->entered_at = $enteredAt->format('Y-m-d H:i:s');
$attendance->exited_at = $exitedAt?->format('Y-m-d H:i:s');
$attendance->duration = $endtime
? $attendance->duration($starttime, $endtime)
: null;
$attendance->save();
$create_requests_key = "att-tempreq:creates:{$data->userid}";
Redis::connection()->lrem($create_requests_key, 1, json_encode($data));
app(LatePenaltyService::class)->syncAttendance($attendance);
$this->removeCreateRequest($data->userid, $data);
$this->decrementUserRequestCount($data->userid);
// Redis::connection()->del("att-tempreq:{$data->userid}:{$data->id}");
tel_notif($data->userid, "تایید");
return redirect()->back()->with('message', [
'color' => '#6aff00ff',
@@ -337,19 +686,13 @@ class AdminController extends Controller
if ($request->update == 'reject') {
if ($data->function == 'delete' || $data->function == 'edit') {
Redis::connection()->del("att-tempreq:{$data->userid}:{$data->id}");
$this->clearAttendanceRequestFlag($data->id ?? null);
} elseif ($data->function == 'new') {
$create_requests_key = "att-tempreq:creates:{$data->userid}";
Redis::connection()->lrem($create_requests_key, 1, json_encode($data));
$this->removeCreateRequest($data->userid, $data);
}
// dd($data->id);
if ($data->id !== "new") {
$att = Attendance::find($data->id);
// dd($att);
$att->requested = null;
$att->save();
}
tel_notif($data->userid, "رد");
$this->decrementUserRequestCount($data->userid);
return redirect()->back()->with('message', [
'color' => '#6aff00ff',
"text" => 'درخواست با موفقیت رد شد.✔️'
@@ -379,11 +722,11 @@ function tel_notif($username, $status)
$message = "درخواست شما {$status} شد.";
$user = User::where('user_name', $username)->whereNotNull('chatid')->select('chatid', 'user_name')->first();
if ($user) {
Http::get("https://api.wwwapp.ir/telegran_send.php", [
'botToken' => "7467099619:AAF2V6yYcbAb8oSIRSCyH5i2GKfdWVO3a7k",
'chatId' => $user->chatid,
'message' => $message,
]);
try {
Http::withoutVerifying()->post('https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage', [
"chat_id" => $user->bale,
'text' => $message,
]);
} catch (\Exception $e) {}
}
}
View File
+178 -58
View File
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use Carbon\CarbonInterval;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use function PHPUnit\Framework\throwException;
use Illuminate\Support\Facades\Cookie;
@@ -10,6 +11,7 @@ use Illuminate\Support\Facades\Http;
use App\Models\AttendanceBreak;
use Morilog\Jalali\Jalalian;
use Illuminate\Http\Request;
use App\Models\TaskDuration;
use App\Models\Pivot_Tasks;
use App\Models\Attendance;
use App\Models\Tasks;
@@ -17,6 +19,7 @@ use App\Models\User;
use App\Models\Log;
use Carbon\Carbon;
use App\Models\ai;
use App\Services\LatePenaltyService;
class AttendanceController extends Controller
@@ -24,14 +27,14 @@ class AttendanceController extends Controller
public function index()
{
if (isset($_COOKIE['user_identity'])) {
$username = $_COOKIE['user_identity'];
$attendanceSince = Carbon::now()->subMonths(12)->startOfMonth()->format('Y-m-d 00:00:00');
$Attendance = isset($_COOKIE['user_identity'])
? ($_COOKIE['user_identity'] == 1370
? Attendance::all()
: Attendance::where('user_identity', $_COOKIE['user_identity'])->get())
: collect();
$Attendance = $username == 1370
? Attendance::where('entered_at', '>=', $attendanceSince)->get()
: Attendance::where('user_identity', $username)
->where('entered_at', '>=', $attendanceSince)
->get();
$jalalian_object = Jalalian::now();
@@ -46,25 +49,31 @@ class AttendanceController extends Controller
->orderByDesc('id')
->first()
?? ai::where('key', 'angizeh')->orderByDesc('id')->first();
$ids = array_merge($ids, [$s->id]);
Cookie::queue('seen_angizeh_ids', json_encode($ids), 14400);
if ($s) {
$ids = array_merge($ids, [$s->id]);
Cookie::queue('seen_angizeh_ids', json_encode($ids), 14400);
}
//total time
$now = Jalalian::fromCarbon(Carbon::now());
$tsec = 0;
//dd(->user_profile);
if (isset($_COOKIE['token01'])) {
$a = json_decode($_COOKIE['token01']);
$user = User::where('user_name', $a->user_profile)->with('attendances')->first();
$user = User::where('user_name', $a->user_profile)
->with(['attendances' => fn($q) => $q->where('entered_at', '>=', $attendanceSince)])
->first();
} else {
$user = isset($_COOKIE['user_identity']) ? User::where('user_name', $_COOKIE['user_identity'])->with('attendances')->first() : null;
$user = User::where('user_name', $username)
->with(['attendances' => fn($q) => $q->where('entered_at', '>=', $attendanceSince)])
->first();
}
//dd($user);
if (!empty($user->attendances)) {
if ($user && !empty($user->attendances)) {
foreach ($user->attendances as $att) {
$date = $att->entered_at != null ? Jalalian::fromCarbon(Carbon::parse($att->entered_at)) : null;
$date = $att->entered_at != null
? \App\Support\JalaliHelper::fromDateTime($att->entered_at)
: null;
$att->date = $date != null ? $date->format('Y/m/d') : null;
if ($att->duration != null && $date != null && $now->getMonth() == $date->getMonth()) {
[$h, $m, $sec] = explode(':', $att->duration);
@@ -81,33 +90,41 @@ class AttendanceController extends Controller
$formaction = (isset($_COOKIE['ad']) && $_COOKIE['ad'] == 1)
? 'admin/'
: 'attendance/';
$now = Carbon::now()->format('Y-m-d 00:00:00');
$existed = $Attendance->whereNull('exited_at')->where('entered_at', '>=', now()->format('Y-m-d 00:00:00'))->where('entered_at', '<=', now()->format('Y-m-d 23:59:59'))->first();
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
$until_now = null;
if ($user->break == 1) {
$att = Attendance::whereNull('exited_at')->latest('entered_at')->first();
$break = AttendanceBreak::where('attendance_id', $att->id)
->whereNull('break_end')
->latest('break_start')
$duration = null;
if ($user && $user->break == 1) {
$att = Attendance::where('user_identity', $user->user_name)
->whereNull('exited_at')
->latest('entered_at')
->first();
if ($break)
$break = $att
? AttendanceBreak::where('attendance_id', $att->id)
->whereNull('break_end')
->latest('break_start')
->first()
: null;
if ($break) {
$until_now = (int)Carbon::parse($break->break_start)->diffInSeconds(now(), false);
// $until_now = Carbon::now()->diffInSeconds(Carbon::parse($break->break_start));
$duration = $att->duration($break->break_start, now()->format('Y-m-d H:i:s'));
}
}
$user->tasks = Tasks::where('created_by', $user->id)
->orWhereJsonContains('assignees', (string) $user->id)
->orderBy('target_date', 'desc')
->get();
//dd($user->tasks);
// dd($until_now);
if ($user) {
$user->tasks = Tasks::where('created_by', $user->id)
->orWhereJsonContains('assignees', (string)$user->id)
->orderBy('target_date', 'desc')
->limit(50)
->get();
}
return view('attendance.index', compact('user'))->with([
's' => $s,
'existed' => $existed,
'formaction' => $formaction,
'breakDu' => $until_now ?? null,
'duration' => $duration,
]);
} else {
foreach (request()->cookies as $name => $value) {
@@ -169,8 +186,6 @@ class AttendanceController extends Controller
// }
public function ai()
{
$response = Http::withHeaders([
@@ -223,7 +238,7 @@ class AttendanceController extends Controller
if ($user->is_admin == 1) {
setcookie('ad', '1', time() + (60 * 60 * 24 * 365 * 5), "/");
}elseif ($user->is_admin == 2){
} elseif ($user->is_admin == 2) {
setcookie('ad', '2', time() + (60 * 60 * 24 * 365 * 5), "/");
}
@@ -282,21 +297,9 @@ class AttendanceController extends Controller
->latest('entered_at')
->first();
if ($attendance) {
// خروج
//end task
if (!empty($user->active_task)) {
$task = Tasks::where('name', $user->active_task)->first();
if ($p_task = Pivot_Tasks::where('user_id', $user->plain_id)->where('task_id', $task->task_id)->whereNull('ended_at')->latest('started_at')->first()) {
$p_task->duration = $attendance->duration(Carbon::parse($p_task->started_at)->format('H:i:s'), Carbon::now()->format('H:i:s'));
$p_task->ended_at = Carbon::now();
$p_task->save();
}
$user->active_task = null;
$user->save();
}
if ($attendance) {
// خروج — همه تسک‌های باز کاربر بسته شود
app(TaskController::class)->endOpenDurationsForUser($user);
//end break
if ($break = AttendanceBreak::where('attendance_id', $attendance->id)->whereNull('break_end')->latest('break_start')->first()) {
@@ -305,6 +308,7 @@ class AttendanceController extends Controller
$break->save();
}
$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');
$end = Carbon::parse($attendance->exited_at)->format('H:i:s');
$duration = $attendance->duration($start, $end);
@@ -313,10 +317,9 @@ class AttendanceController extends Controller
$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 -= $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->ip_exit = $userLng;
$attendance->distance_exit = $request->input('distance');
@@ -324,7 +327,7 @@ class AttendanceController extends Controller
$attendance->save();
$this->send($user->name, $attendance, 'attendance');
$this->dailyReport($user->id, $user->name);
return redirect()->back()->with('message', [
'color' => '#3b82f6',
'text' => 'خروج با موفقیت ثبت شد.'
@@ -342,6 +345,8 @@ class AttendanceController extends Controller
$attendance->office_location = ['lat' => $officeLat, 'lng' => $officeLng];
$attendance->save();
app(LatePenaltyService::class)->syncAttendance($attendance);
$this->send($user->name, $attendance, 'attendance');
return redirect()->back()->with('message', [
@@ -388,7 +393,7 @@ class AttendanceController extends Controller
'duration' => $durationSeconds
]);
// dd($this->send($user->name, $break, 'break'));
$this->send($user->name, $break, 'break');
return response()->json([
"break_status" => 'ended',
'duration' => $durationSeconds
@@ -400,7 +405,7 @@ class AttendanceController extends Controller
'break_start' => $now,
]);
// dd($this->send($user->name, $break, 'break'));
$this->send($user->name, $break, 'break');
$user->update([
'break' => 1
@@ -604,18 +609,38 @@ class AttendanceController extends Controller
}
private function viewerIsAdmin(): bool
{
$viewer = User::where('user_name', $_COOKIE['user_identity'] ?? '')->first();
return $viewer && in_array((string) $viewer->is_admin, ['1', '2'], true);
}
public function profile(Request $request)
{
$username = $request->query('username') ?: $_COOKIE['user_identity'];
$user = User::where('user_name', $username)->first();
return view('attendance.profile', ['user' => $user]);
return view('attendance.profile', [
'user' => $user,
'viewerIsAdmin' => $this->viewerIsAdmin(),
]);
}
public function updateProfile(Request $request)
{
// dd($request->all());
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
$viewerIsAdmin = $this->viewerIsAdmin();
$targetUsername = ($viewerIsAdmin && $request->filled('target_username'))
? $request->input('target_username')
: $_COOKIE['user_identity'];
$user = User::where('user_name', $targetUsername)->first();
if (!$user) {
return redirect()->back()->with('message', [
'color' => '#ff8800ff',
'text' => 'کاربر یافت نشد!'
]);
}
if ($user->password != $request->password) {
$request->validate([
@@ -631,24 +656,119 @@ class AttendanceController extends Controller
$user->email = $request->email;
$user->number = $request->number;
$user->chatid = $request->chatid;
$user->birthday = $request->filled('birthday')
? $this->fa2en_numbers($request->birthday)
: null;
// اگر کاربر ادمین باشه، می‌تونه نقش رو تغییر بده
if (!empty($_COOKIE['ad'])) {
$user->is_admin = $request->role == 1 ? 1 : null;
}
$workStartChanged = false;
if ($viewerIsAdmin && $request->has('work_start_time')) {
$raw = trim((string) $this->fa2en_numbers($request->input('work_start_time')));
if ($raw === '') {
$newStart = null;
} elseif (preg_match('/^([01]?\d|2[0-3]):([0-5]\d)$/', $raw, $m)) {
$newStart = sprintf('%02d:%02d:00', (int) $m[1], (int) $m[2]);
} else {
return redirect()->back()->with('message', [
'color' => '#ff4400ff',
'text' => 'ساعت شروع کار نامعتبر است (مثال: 15:00).'
]);
}
$oldStart = $user->work_start_time ? substr((string) $user->work_start_time, 0, 8) : null;
if ($oldStart !== $newStart) {
$user->work_start_time = $newStart;
$workStartChanged = true;
}
}
$user->save();
if ($workStartChanged) {
$this->resyncCurrentMonthLate($user->user_name);
}
return redirect()->back()->with('message', [
'color' => '#00c300ff',
'text' => 'اطلاعات با موفقیت ویرایش شد.'
]);
}
private function resyncCurrentMonthLate(string $userIdentity): void
{
$service = app(LatePenaltyService::class);
$service->forgetWorkStart($userIdentity);
[$monthStart, $monthEnd] = $service->jalaliMonthBounds(Carbon::now());
Attendance::query()
->where('user_identity', $userIdentity)
->whereBetween('entered_at', [$monthStart, $monthEnd])
->orderBy('entered_at')
->get()
->each(fn (Attendance $attendance) => $service->syncAttendance($attendance));
}
private function dailyReport($userId, $userName)
{
$tasks = Tasks::with(['duration'])
->where('created_by', $userId)
->orWhereJsonContains('assignees', $userId)
->get();
$todayStart = now()->startOfDay();
// $todayStart = '2026-04-28 00:00:00'; // ابتدای امروز
$message = " *" . Jalalian::now()->format('j F') . "* "
. "\n * _{$userName}_ * ";
$hasAnyWork = false;
$totalWorkTime = 0;
foreach ($tasks as $task) {
$totalSeconds = 0;
foreach ($task->duration as $item) {
if (
$item->user_id == $userId &&
!empty($item->duration) &&
!empty($item->start) &&
Carbon::parse($item->start)->greaterThanOrEqualTo($todayStart)
) {
$totalWorkTime += $totalSeconds += CarbonInterval::createFromFormat('H:i:s', $item->duration)->totalSeconds;
}
}
if ($totalSeconds > 0) {
$hasAnyWork = true;
$prioritySymbol = [
'none' => '⚪',
'low' => '🟢',
'medium' => '🟡',
'high' => '🟠',
'urgent' => '🔴',
];
$formattedDuration = CarbonInterval::seconds($totalSeconds)->cascade()->format('%H:%I');
$message .= "\n{$prioritySymbol[$task->priority]} {$task->title} {$formattedDuration}";
}
}
// $message .= "\nمجموع زمان ها: ".CarbonInterval::seconds($totalWorkTime)->cascade()->format('%H:%I');
//dd($message);
if ($hasAnyWork) {
// Http::withoutVerifying()->post('https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage', [
// 'chat_id' => '985309723',
// 'text' => $message,
// ]);
$this->send_admins($message);
}
}
}
//function auto_break($user_id, $force_method = null)
//{
// $user = User::where('user_name', $user_id)->first();
+10 -14
View File
@@ -48,17 +48,19 @@ namespace App\Http\Controllers;
$date = Jalalian::fromCarbon(Carbon::parse($data->break_start))->format('Y/m/d');
$start = Carbon::parse($data->break_start)->format('H:i');
$end = Carbon::parse($data->break_end)->format('H:i');
if (!$data->break_end)
if (!$data->break_end) {
$message = "تاریخ: {$date}\n {$user} در ساعت {$start} شروع به استراحت کرد.";
else
}
else {
$message = "تاریخ: {$date}\n {$user} در ساعت {$end} به استراحت پایان داد.";
}
}
if ($status == 'request') {
$message = "{$user} درخواست {$data} ثبت کرد.";
}
$admins = User::whereIn('is_admin', [1,2])->whereNotNull('chatid')->select('chatid', 'bale', 'user_name')->get();
$admins = User::whereIn('is_admin', [1,2])->whereNotNull('bale')->select('chatid', 'bale', 'user_name')->get();
if ($_COOKIE['user_identity'] !== 'test') {
foreach ($admins as $admin) {
if (!$admin->bale) {continue;}
@@ -70,8 +72,8 @@ namespace App\Http\Controllers;
// ]);
if ($admin->bale) {
try {
$response = Http::post('https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage', [
"chat_id" => $admin->bale,
$response = Http::withoutVerifying()->post('https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage', [
"chat_id" => (string)$admin->bale,
'text' => $message,
]);
}catch (\Exception $e){}
@@ -84,7 +86,7 @@ namespace App\Http\Controllers;
}
}else{
try {
$response = Http::post('https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage', [
$response = Http::withoutVerifying()->post('https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage', [
"chat_id" => '985309723',
'text' => $message,
]);
@@ -99,12 +101,7 @@ namespace App\Http\Controllers;
foreach ($admins as $admin) {
try{
Http::/*withOptions([
'curl' => [
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
CURLOPT_TCP_FASTOPEN => 1,
]
])->*/withoutVerifying()->post('https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage', [
Http::withoutVerifying()->post('https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage', [
"chat_id" => $admin->bale,
'text' => $message,
]);
@@ -122,8 +119,7 @@ namespace App\Http\Controllers;
"chat_id" => $admin->bale,
'text' => $message,
]);
} catch (\Exception $e) {
}
} catch (\Exception $e) {}
}
}
}
View File
+257
View File
@@ -0,0 +1,257 @@
<?php
namespace App\Http\Controllers;
use App\Models\Attendance;
use App\Models\LeaveRequest;
use App\Models\User;
use App\Services\LatePenaltyService;
use App\Support\JalaliHelper;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Morilog\Jalali\Jalalian;
class LeaveController extends Controller
{
public function store(Request $request)
{
if (!isset($_COOKIE['user_identity'])) {
return redirect('/')->with('message', [
'color' => '#ff8800ff',
'text' => 'ابتدا باید وارد شوید!',
]);
}
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
if (!$user) {
return redirect('/')->with('message', [
'color' => '#ff8800ff',
'text' => 'کاربر یافت نشد!',
]);
}
$type = $request->input('type');
if (!in_array($type, ['daily', 'hourly'], true)) {
return back()->with('message', [
'color' => '#ff4400ff',
'text' => 'نوع مرخصی نامعتبر است.',
]);
}
if ($type === 'daily') {
$dateFrom = $this->parseJalaliDate($request->input('date_from'));
$dateTo = $this->parseJalaliDate($request->input('date_to'));
if (!$dateFrom || !$dateTo) {
return back()->with('message', [
'color' => '#ff4400ff',
'text' => 'بازه تاریخ مرخصی روزانه الزامی است.',
]);
}
if ($dateTo < $dateFrom) {
return back()->with('message', [
'color' => '#ff4400ff',
'text' => 'تاریخ پایان نمی‌تواند قبل از شروع باشد.',
]);
}
$timeFrom = null;
$timeTo = null;
} else {
$dateFrom = $this->parseJalaliDate($request->input('date'));
$dateTo = $dateFrom;
$timeFrom = $this->normalizeTime($request->input('time_from'));
$timeTo = $this->normalizeTime($request->input('time_to'));
if (!$dateFrom || !$timeFrom || !$timeTo) {
return back()->with('message', [
'color' => '#ff4400ff',
'text' => 'تاریخ و ساعت شروع/پایان مرخصی ساعتی الزامی است.',
]);
}
if ($timeTo <= $timeFrom) {
return back()->with('message', [
'color' => '#ff4400ff',
'text' => 'ساعت پایان باید بعد از ساعت شروع باشد.',
]);
}
}
$leave = LeaveRequest::create([
'user_id' => $user->id,
'user_identity' => $user->user_name,
'type' => $type,
'date_from' => $dateFrom,
'date_to' => $dateTo,
'time_from' => $timeFrom,
'time_to' => $timeTo,
'reason' => $request->input('reason'),
'status' => 'pending',
]);
$user->req_counts = (int) $user->req_counts + 1;
$user->save();
$this->notifyAdminsNewLeave($leave, $user);
return redirect('/')->with('message', [
'color' => '#00c300ff',
'text' => 'درخواست مرخصی ثبت شد و برای تأیید ادمین ارسال گردید.',
]);
}
public function decide(Request $request)
{
if (!isset($_COOKIE['user_identity'])) {
return back()->with('message', [
'color' => '#ff8800ff',
'text' => 'ابتدا باید وارد شوید!',
]);
}
$admin = User::where('user_name', $_COOKIE['user_identity'])->first();
if (!$admin || !in_array((string) $admin->is_admin, ['1', '2'], true)) {
return back()->with('message', [
'color' => '#ff8800ff',
'text' => 'دسترسی ندارید!',
]);
}
$leave = LeaveRequest::find($request->input('leave_id'));
if (!$leave || $leave->status !== 'pending') {
return back()->with('message', [
'color' => '#ff8800ff',
'text' => 'درخواست مرخصی یافت نشد.',
]);
}
$action = $request->input('update');
if (!in_array($action, ['approve', 'reject'], true)) {
return back()->with('message', [
'color' => '#ff4400ff',
'text' => 'عملیات نامعتبر است.',
]);
}
$leave->status = $action === 'approve' ? 'approved' : 'rejected';
$leave->reviewed_by = $admin->id;
$leave->reviewed_at = now();
$leave->save();
$owner = User::where('user_name', $leave->user_identity)->first();
if ($owner && (int) $owner->req_counts > 0) {
$owner->req_counts -= 1;
$owner->save();
}
if ($leave->status === 'approved') {
$this->recalculateLateForLeavePeriod($leave);
}
$statusFa = $leave->status === 'approved' ? 'تایید' : 'رد';
if ($owner) {
$this->notifyUserLeaveDecision($owner, $statusFa);
}
return back()->with('message', [
'color' => '#6aff00ff',
'text' => "درخواست مرخصی با موفقیت {$statusFa} شد.",
]);
}
private function notifyUserLeaveDecision(User $user, string $statusFa): void
{
if (!$user->bale) {
return;
}
try {
Http::withoutVerifying()->post(
'https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage',
[
'chat_id' => $user->bale,
'text' => "درخواست مرخصی شما {$statusFa} شد.",
]
);
} catch (\Exception $e) {
}
}
private function recalculateLateForLeavePeriod(LeaveRequest $leave): void
{
$service = app(LatePenaltyService::class);
$from = Carbon::parse($leave->date_from)->startOfDay();
$to = Carbon::parse($leave->date_to)->endOfDay();
$attendances = Attendance::query()
->where('user_identity', $leave->user_identity)
->whereBetween('entered_at', [$from, $to])
->orderBy('entered_at')
->get();
foreach ($attendances as $attendance) {
$service->syncAttendance($attendance);
}
}
private function notifyAdminsNewLeave(LeaveRequest $leave, User $user): void
{
$typeFa = $leave->type === 'daily' ? 'روزانه' : 'ساعتی';
$from = JalaliHelper::format($leave->date_from, 'Y/m/d');
$to = JalaliHelper::format($leave->date_to, 'Y/m/d');
$message = "درخواست مرخصی {$typeFa}\n";
$message .= "کارمند: {$user->name}\n";
if ($leave->type === 'daily') {
$message .= "از {$from} تا {$to}\n";
} else {
$timeFrom = substr((string) $leave->time_from, 0, 5);
$timeTo = substr((string) $leave->time_to, 0, 5);
$message .= "تاریخ: {$from}\nساعت: {$timeFrom} تا {$timeTo}\n";
}
if ($leave->reason) {
$message .= "دلیل: {$leave->reason}";
}
$this->send_admins($message);
}
private function parseJalaliDate(?string $value): ?string
{
if (!$value) {
return null;
}
$value = \Morilog\Jalali\CalendarUtils::convertNumbers(trim($value), true);
$value = str_replace('/', '-', $value);
try {
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
// اگر سال شمسی باشد (مثلاً 1404) با Jalalian تبدیل می‌شود
$year = (int) substr($value, 0, 4);
if ($year >= 1300 && $year <= 1500) {
return Jalalian::fromFormat('Y-m-d', $value)->toCarbon()->format('Y-m-d');
}
return $value;
}
return Carbon::parse($value)->format('Y-m-d');
} catch (\Throwable $e) {
return null;
}
}
private function normalizeTime(?string $value): ?string
{
if (!$value) {
return null;
}
$value = trim(\Morilog\Jalali\CalendarUtils::convertNumbers($value, true));
if (preg_match('/^(\d{1,2}):(\d{2})/', $value, $m)) {
return sprintf('%02d:%02d:00', (int) $m[1], (int) $m[2]);
}
return null;
}
}
+579 -111
View File
@@ -15,12 +15,98 @@ use GuzzleHttp\Client;
use App\Models\Tasks;
use App\Models\User;
use Carbon\Carbon;
use Morilog\Jalali\Jalalian;
use Carbon\CarbonInterval;
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
{
$lines = [];
@@ -67,17 +153,17 @@ class TaskController extends Controller
? "وضعیت: " . ($stateFa[$s1] ?? ($before['state'] ?? '-')) . " ← " . ($stateFa[$s2] ?? ($after['state'] ?? '-'))
: "وضعیت: " . ($stateFa[$s2] ?? ($after['state'] ?? '-'));
$d1 = $before['start_date'] ?? null;
$d2 = $after['start_date'] ?? null;
$d1 = \App\Support\JalaliHelper::format($before['start_date'] ?? null, 'Y/m/d', '-');
$d2 = \App\Support\JalaliHelper::format($after['start_date'] ?? null, 'Y/m/d', '-');
$lines[] = ($d1 !== $d2)
? "شروع: " . ($d1 ?: '-') . " ← " . ($d2 ?: '-')
: "شروع: " . ($d2 ?: '-');
? "شروع: {$d1} ← {$d2}"
: "شروع: {$d2}";
$t1 = $before['target_date'] ?? null;
$t2 = $after['target_date'] ?? null;
$t1 = \App\Support\JalaliHelper::format($before['target_date'] ?? null, 'Y/m/d', '-');
$t2 = \App\Support\JalaliHelper::format($after['target_date'] ?? null, 'Y/m/d', '-');
$lines[] = ($t1 !== $t2)
? "ددلاین: " . ($t1 ?: '-') . " ← " . ($t2 ?: '-')
: "ددلاین: " . ($t2 ?: '-');
? "ددلاین: {$t1} ← {$t2}"
: "ددلاین: {$t2}";
$desc1 = trim((string)($before['description'] ?? ''));
$desc2 = trim((string)($after['description'] ?? ''));
@@ -101,6 +187,296 @@ class TaskController extends Controller
return implode("\n", $lines);
}
private function sumDurationSeconds($durations, ?int $userId = null): int
{
$totalSeconds = 0;
foreach ($durations as $item) {
if ($userId !== null && (int)$item->user_id !== $userId) {
continue;
}
if ($item->duration === null) {
continue;
}
try {
$totalSeconds += CarbonInterval::createFromFormat('H:i:s', $item->duration)->totalSeconds;
} catch (\Throwable) {
continue;
}
}
return $totalSeconds;
}
private function resolveTaskFinishedAt($durations): ?string
{
$latest = collect($durations)
->pluck('end')
->filter()
->map(fn ($value) => Carbon::parse($value))
->sortDesc()
->first();
return $latest?->toDateTimeString();
}
private function resolveTaskFinishedAtByTaskId(int $taskId): ?string
{
return $this->resolveTaskFinishedAt(
TaskDuration::query()
->where('task_id', $taskId)
->whereNotNull('end')
->get(['end'])
);
}
private function resolveAssigneeNames(Tasks $task, array $usersById = []): array
{
$assignees = is_string($task->assignees) ? json_decode($task->assignees, true) : $task->assignees;
$assignees = is_array($assignees) ? array_map('strval', $assignees) : [];
if (empty($assignees)) {
return [];
}
if (!empty($usersById)) {
$names = [];
foreach ($assignees as $id) {
if (!empty($usersById[$id])) {
$names[] = $usersById[$id];
}
}
return $names;
}
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;
}
try {
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $value, $m)) {
$carbon = Carbon::createFromFormat('Y-m-d', $m[1])->startOfDay();
} else {
$carbon = Carbon::parse($value);
}
if ($carbon->year < 1621 || $carbon->year > 2500) {
return null;
}
return $carbon->format('Y-m-d');
} catch (\Throwable $e) {
return null;
}
}
private function buildTasksPayload($tasks, array $usersById = []): array
{
$payload = [];
foreach ($tasks as $task) {
$payload[$task->id] = [
'id' => $task->id,
'title' => $task->title,
'description' => $task->description,
'start_date' => $this->normalizeTaskDate($task->start_date ? (string) $task->start_date : null),
'target_date' => $this->normalizeTaskDate($task->target_date ? (string) $task->target_date : null),
'finished_at' => $task->finished_at ?? null,
'priority' => $task->priority,
'state' => $task->state,
'app' => $task->app,
'active' => (bool)$task->active,
'creator_name' => $task->creator_name,
'created_at' => $task->created_at,
'assignee_names' => $task->assignee_names ?? $this->resolveAssigneeNames($task, $usersById),
'total_duration' => $task->total_duration ?? '00:00:00',
'comments' => [],
'duration' => [],
];
}
return $payload;
}
private function userCanAccessTask(Tasks $task, User $user): bool
{
if ($user->is_admin == 1) {
return true;
}
$assignees = is_string($task->assignees) ? json_decode($task->assignees, true) : $task->assignees;
$assignees = is_array($assignees) ? array_map('strval', $assignees) : [];
return (int)$task->created_by === (int)$user->id
|| in_array((string)$user->id, $assignees, true);
}
private function buildTaskDetailPayload(Tasks $task, bool $isAdmin, User $user): array
{
$activeQuery = TaskDuration::query()
->where('task_id', $task->id)
->whereNull('end');
if (!$isAdmin) {
$activeQuery->where('user_id', $user->id);
}
return [
'id' => $task->id,
'title' => $task->title,
'description' => $task->description,
'start_date' => $this->normalizeTaskDate($task->start_date ? (string) $task->start_date : null),
'target_date' => $this->normalizeTaskDate($task->target_date ? (string) $task->target_date : null),
'finished_at' => $this->resolveTaskFinishedAtByTaskId($task->id),
'priority' => $task->priority,
'state' => $task->state,
'app' => $task->app,
'active' => $activeQuery->exists(),
'creator_name' => $task->creator?->name ?? 'نامشخص',
'created_at' => $task->created_at,
'assignee_names' => $this->resolveAssigneeNames($task),
'duration' => $task->duration->map(fn ($d) => [
'id' => $d->id,
'task_id' => $d->task_id,
'user_id' => $d->user_id,
'start' => $d->start,
'end' => $d->end,
'duration' => $d->duration,
])->values()->all(),
'comments' => $task->comments->map(fn ($c) => [
'id' => $c->id,
'body' => $c->body,
'created_at' => $c->created_at,
'user' => $c->user ? ['name' => $c->user->name] : null,
])->values()->all(),
];
}
private function loadTasksPageData(User $user): array
{
$isAdmin = $user->is_admin == 1;
$query = Tasks::query()
->select([
'id',
'title',
'description',
'start_date',
'target_date',
'priority',
'state',
'app',
'created_by',
'assignees',
'created_at',
'updated_at',
])
->with([
'appName:id,name_fa,type',
'creator:id,name',
])
->orderByRaw('target_date IS NULL')
->orderBy('target_date', 'asc')
->orderBy('updated_at', 'desc');
if (!$isAdmin) {
$query->where(function ($q) use ($user) {
$q->where('created_by', $user->id)
->orWhereJsonContains('assignees', (string)$user->id);
});
}
$tasks = $query->get();
$taskIds = $tasks->pluck('id');
$assigneeIds = [];
foreach ($tasks as $task) {
$ids = is_string($task->assignees) ? json_decode($task->assignees, true) : $task->assignees;
if (is_array($ids)) {
$assigneeIds = array_merge($assigneeIds, array_map('strval', $ids));
}
}
$usersById = !empty($assigneeIds)
? User::whereIn('id', array_unique($assigneeIds))
->get()
->mapWithKeys(fn ($user) => [(string)$user->id => $user->name])
->all()
: [];
if ($taskIds->isEmpty()) {
return [
'tasks' => $tasks,
'tasksPayload' => [],
'assigneeFilterOptions' => [],
];
}
$activeQuery = TaskDuration::query()
->select(['task_id'])
->whereIn('task_id', $taskIds)
->whereNull('end');
if (!$isAdmin) {
$activeQuery->where('user_id', $user->id);
}
$activeTaskIds = $activeQuery->pluck('task_id')->flip();
$durationQuery = TaskDuration::query()
->select(['task_id', 'user_id', 'duration', 'end'])
->whereIn('task_id', $taskIds)
->whereNotNull('duration');
if (!$isAdmin) {
$durationQuery->where('user_id', $user->id);
}
$durationsByTaskId = $durationQuery->get()->groupBy('task_id');
$finishedAtByTaskId = TaskDuration::query()
->select(['task_id', 'end'])
->whereIn('task_id', $taskIds)
->whereNotNull('end')
->get()
->groupBy('task_id');
foreach ($tasks as $task) {
$task->active = $activeTaskIds->has($task->id);
$task->creator_name = $task->creator?->name ?? 'نامشخص';
$task->assignee_names = $this->resolveAssigneeNames($task, $usersById);
$ids = is_string($task->assignees) ? json_decode($task->assignees, true) : $task->assignees;
$task->assignee_ids = is_array($ids) ? array_map('strval', $ids) : [];
$task->finished_at = $this->resolveTaskFinishedAt($finishedAtByTaskId->get($task->id, collect()));
$totalSeconds = $this->sumDurationSeconds(
$durationsByTaskId->get($task->id, collect()),
$isAdmin ? null : $user->id
);
$task->total_duration = CarbonInterval::seconds($totalSeconds)->cascade()->format('%H:%I:%S');
}
$assigneeFilterOptions = collect($usersById)
->map(fn ($name, $id) => ['id' => (string)$id, 'name' => $name])
->sortBy('name', SORT_NATURAL)
->values()
->all();
return [
'tasks' => $tasks,
'tasksPayload' => $this->buildTasksPayload($tasks, $usersById),
'assigneeFilterOptions' => $assigneeFilterOptions,
];
}
// public function tasks()
// {
// $user = User::where('user_name', $_COOKIE['user_identity'])->first();
@@ -135,53 +511,40 @@ class TaskController extends Controller
public function tasks()
{
if (!isset($_COOKIE['user_identity'])) {
return redirect('/')->with('message', [
'color' => 'red',
'text' => 'ابتدا باید وارد شوید!'
]);
}
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
if ($user) {
if ($user->is_admin == 1) {
$tasks = Tasks::with(['duration', 'comments.user'])
->orderBy('target_date', 'desc')
->get();
// dd($tasks);
foreach ($tasks as $task) {
$totalSeconds = 0;
foreach ($task->duration as $item) {
if ($item->duration !== null) {
$totalSeconds += CarbonInterval::createFromFormat('H:i:s', $item->duration)->totalSeconds;
}
}
$task->totalDuration = CarbonInterval::seconds($totalSeconds)->cascade()->format('%H:%I:%S');
$active_task = TaskDuration::where('task_id', $task->id)->orderBy('start', 'desc')->first();
$task->active = !empty($active_task) && $active_task->end == null;
}
} else {
$tasks = Tasks::with(['duration', 'comments.user'])->where('created_by', $user->id)
->orWhereJsonContains('assignees', (string)$user->id)
->orderBy('target_date', 'desc')
->get();
// dd($tasks);
foreach ($tasks as $task) {
$totalSeconds = 0;
foreach ($task->duration as $item) {
if ($user->id == $item->user_id && $item->duration !== null) {
$totalSeconds += CarbonInterval::createFromFormat('H:i:s', $item->duration)->totalSeconds;
}
}
$task->totalDuration = CarbonInterval::seconds($totalSeconds)->cascade()->format('%H:%I:%S');
$active_task = TaskDuration::where('task_id', $task->id)->where('user_id', $user->id)->orderBy('start', 'desc')->first();
$task->active = !empty($active_task) && $active_task->end == null;
}
}
$count = count($tasks);
// dd((new Tasks())->filterByState($tasks)['backlog'][0], 'count ='.$count);
['tasks' => $tasks, 'tasksPayload' => $tasksPayload, 'assigneeFilterOptions' => $assigneeFilterOptions] = $this->loadTasksPageData($user);
$count = $tasks->count();
return view('tasks.tasks', [
'tasks' => (new Tasks())->filterByState($tasks),
'tasksPayload' => $tasksPayload,
'assigneeFilterOptions' => $assigneeFilterOptions,
'count' => $count,
'user_id' => $user->id,
'isAdminLevel1' => $user->is_admin == 1,
'mentionUsers' => User::select('id', 'name', 'user_name')->get(),
'appsList' => App::select('id', 'name_fa', 'type')->get(),
'priorityLabels' => [
'urgent' => 'فوری',
'high' => 'بالا',
'medium' => 'متوسط',
'low' => 'پایین'
],
'priorityColors' => [
'urgent' => 'bg-red-100 text-red-800',
'high' => 'bg-orange-100 text-orange-800',
'medium' => 'bg-yellow-100 text-yellow-800',
'low' => 'bg-green-100 text-green-800'
],
]);
} else {
return back()->with('message', [
@@ -199,6 +562,37 @@ class TaskController extends Controller
}
public function taskDetailsJson($id)
{
if (!isset($_COOKIE['user_identity'])) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
if (!$user) {
return response()->json(['error' => 'User not found'], 404);
}
$isAdmin = $user->is_admin == 1;
$task = Tasks::query()
->with([
'creator:id,name',
'comments.user:id,name',
'duration' => function ($q) use ($isAdmin, $user) {
if (!$isAdmin) {
$q->where('user_id', $user->id);
}
},
])
->find($id);
if (!$task || !$this->userCanAccessTask($task, $user)) {
return response()->json(['error' => 'Not found'], 404);
}
return response()->json($this->buildTaskDetailPayload($task, $isAdmin, $user));
}
public function editTask(Request $request)
{
// dd($request->all());
@@ -227,7 +621,26 @@ class TaskController extends Controller
} else {
unset($data['assignees']);
}
unset($data['_token'], $data['id'], $data['start_date_display'], $data['target_date_display']);
unset(
$data['_token'],
$data['id'],
$data['start_date_display'],
$data['target_date_display'],
$data['completed_at'],
$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);
$after = $task->toArray();
@@ -280,29 +693,42 @@ class TaskController extends Controller
}
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
$data = $request->all();
$data['created_by'] = $user->id;
$data['assignees'] = !empty($data['assignees']) ? json_encode($data['assignees']) : null;
$startDate = $this->normalizeTaskDate($request->input('start_date'))
?: now()->format('Y-m-d');
$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);
$message = "تسک جدیدی با عنوان {$data['title']} برای شما ایجاد شد.";
$deadline = !empty($data['target_date']) ? Jalalian::fromCarbon(Carbon::parse($data['target_date']))->format('Y-m-d') : null;
$deadline = \App\Support\JalaliHelper::format($data['target_date'] ?? null, 'Y/m/d', '');
$deadline = $deadline !== '' ? $deadline : null;
$startDateJalali = \App\Support\JalaliHelper::format($data['start_date'] ?? null, 'Y/m/d', '');
$app = App::find($data['app']);
$message_admin = "ایجاد تسک: «{$data['title']}»";
$message_admin .= !empty($app) ? "\nاپ {$app->name_fa}" : null;
$message_admin .= !empty($deadline) ? "\nددلاین {$deadline}" : 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 .= $startDateJalali !== '' ? "\nتاریخ شروع {$startDateJalali}" : 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['assignees']) ? "\nتوسط " . $user->name . " ساخته شد و به" : null;
if (!empty($data['assignees'])) {
if (!empty($request->assignees)) {
foreach ($request->assignees as $assignee) {
$user = User::where('id', $assignee)->first();
$message_admin .= "\n$user->name";
if ($user && $user->bale !== null) {
$this->send_bale($user->bale, $message);
$assigneeUser = User::where('id', $assignee)->first();
$message_admin .= "\n" . ($assigneeUser->name ?? $assignee);
if ($assigneeUser && $assigneeUser->bale !== null) {
$this->send_bale($assigneeUser->bale, $message);
}
}
$message_admin .= "\nاختصاص یافت.";
@@ -312,7 +738,7 @@ class TaskController extends Controller
// dd($data, $message, $message_admin);
$this->send_level1_admins($message_admin);
return back()->with('message', [
return redirect('/tasks')->with('message', [
'color' => 'green',
'text' => $message
]);
@@ -321,6 +747,10 @@ class TaskController extends Controller
public function activeTask(Request $request)
{
// dd($request);
if (!isset($_COOKIE['user_identity'])) {
return response()->json(['action' => 'unauthorized', 'status' => false], 401);
}
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
$task = Tasks::find($request->task_id);
if (!$task) {
@@ -337,49 +767,18 @@ class TaskController extends Controller
// اگر رکورد باز داریم => پایان
if (!empty($taskDU) && ($taskDU->start !== null && $taskDU->end == null)) {
// dd('in if');
$attendance = new Attendance();
$s = Carbon::parse($taskDU->start)->format('H:i:s');
$e = Carbon::parse(now())->format('H:i:s');
$duration = $taskDU->duration;
if ($e > $s) {
$diff = $attendance->duration($s, $e);
if ($duration !== null) {
[$h1, $m1, $s1] = explode(':', $duration);
$durationSeconds = $h1 * 3600 + $m1 * 60 + $s1;
[$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,
]);
} else
return response()->json([
'action' => 'end',
'status' => false
]);
$this->closeDurationRecord($taskDU, Carbon::now());
$user->active_task = null;
$user->save();
$this->send_level1_admins("پایان تسک: «{$task->title}»\nتوسط: {$user->name}\nمدت این نوبت: {$taskDU->duration}");
return response()->json([
'action' => 'ended',
'status' => true,
'duration' => $taskDU->duration,
'start' => $taskDU->start,
'end' => $taskDU->end,
'finished_at' => $this->resolveTaskFinishedAtByTaskId($task->id),
]);
} else {
// شروع => همیشه یک رکورد جدید بساز
$new_task_duration = TaskDuration::create([
@@ -403,6 +802,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)
{
@@ -529,12 +1002,7 @@ class TaskController extends Controller
private function send_bale($chat_id, $message)
{
try {
Http::withOptions([
'curl' => [
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
CURLOPT_TCP_FASTOPEN => 1,
]
])->withoutVerifying()->post('https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage', [
Http::withoutVerifying()->post('https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage', [
"chat_id" => $chat_id,
'text' => $message,
]);
View File
Regular → Executable
+8 -2
View File
@@ -25,7 +25,13 @@ class App extends Model
];
protected $TYPE = [
1=>'خانگی',
2=>'همکار'
1=>'(خانگی)',
2=>'(همکار)'
];
public function getTypeFaAttribute(): string
{
return $this->TYPE[$this->type] ?? '';
}
}
Regular → Executable
+3
View File
@@ -25,6 +25,9 @@ class Attendance extends Model
'entered_at',
'exited_at',
'requested',
'late_minutes',
'daily_fine_minutes',
'daily_fine_amount',
];
public function setEntryLocationAttribute($value)
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Holiday extends Model
{
protected $fillable = [
'date',
'title',
];
protected function casts(): array
{
return [
'date' => 'date',
];
}
}
+30
View File
@@ -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',
];
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class LeaveRequest extends Model
{
protected $fillable = [
'user_id',
'user_identity',
'type',
'date_from',
'date_to',
'time_from',
'time_to',
'reason',
'status',
'reviewed_by',
'reviewed_at',
];
protected function casts(): array
{
return [
'date_from' => 'date',
'date_to' => 'date',
'reviewed_at' => 'datetime',
];
}
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function reviewer()
{
return $this->belongsTo(User::class, 'reviewed_by');
}
public function isDaily(): bool
{
return $this->type === 'daily';
}
public function isHourly(): bool
{
return $this->type === 'hourly';
}
public function isApproved(): bool
{
return $this->status === 'approved';
}
}
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
+7 -2
View File
@@ -62,8 +62,13 @@ class Tasks extends Model
return $this->belongsTo(User::class);
}
public function app(){
return $this->belogsTo(App::class);
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function appName(){
return $this->belongsTo(App::class, 'app' , 'id');
}
public function duration(){
+16 -7
View File
@@ -29,12 +29,15 @@ class User extends Authenticatable
'chatid',
'name',
'number',
'birthday',
'work_start_time',
'password',
'user_name',
'is_admin',
'plain_id',
'break',
'req_counts',
'active_task'
];
@@ -52,7 +55,9 @@ class User extends Authenticatable
*/
protected function casts(): array
{
return [];
return [
'birthday' => 'date',
];
}
public function task()
@@ -77,7 +82,7 @@ class User extends Authenticatable
{
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');
@@ -103,17 +108,21 @@ class User extends Authenticatable
foreach ($data->attendances as $att) {
if ($att->entered_at) {
$date = \Morilog\Jalali\Jalalian::fromCarbon(\Carbon\Carbon::parse($att->entered_at));
$month = $date->format('%B');
$date = \App\Support\JalaliHelper::fromDateTime($att->entered_at);
if (!$date) {
continue;
}
$key = sprintf('%04d-%02d', $date->getYear(), $date->getMonth());
if (!isset($result[$month])) {
$result[$month] = [];
if (!isset($result[$key])) {
$result[$key] = [];
}
$result[$month][] = $att;
$result[$key][] = $att;
}
}
krsort($result);
return $result;
}
Regular → Executable
View File
+366
View File
@@ -0,0 +1,366 @@
<?php
namespace App\Services;
use App\Models\Attendance;
use App\Models\Holiday;
use App\Models\LatePeriodFine;
use App\Models\LeaveRequest;
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;
/** @var array<string, string> */
private array $workStartCache = [];
/** ساعت شروع کار کاربر (H:i)؛ اگر ادمین تعیین نکرده باشد ۰۸:۳۰ */
public function workStartFor(?string $userIdentity): string
{
if (!$userIdentity) {
return self::WORK_START;
}
if (!array_key_exists($userIdentity, $this->workStartCache)) {
$value = null;
try {
$value = User::where('user_name', $userIdentity)->value('work_start_time');
} catch (\Throwable $e) {
}
$this->workStartCache[$userIdentity] = $value ? substr((string) $value, 0, 5) : self::WORK_START;
}
return $this->workStartCache[$userIdentity];
}
public function forgetWorkStart(string $userIdentity): void
{
unset($this->workStartCache[$userIdentity]);
}
/** جمعه و تعطیلات رسمی روز کاری نیستند؛ ورود در این روزها اضافه‌کاری است. */
public function isWorkDay(Carbon $date): bool
{
if ($date->isFriday()) {
return false;
}
try {
return !Holiday::query()
->whereDate('date', $date->toDateString())
->exists();
} catch (\Throwable $e) {
return true;
}
}
public function calculateFromEnteredAt(?string $enteredAt, ?string $userIdentity = null): array
{
if (!$enteredAt) {
return [
'late_minutes' => 0,
'grace_late_minutes' => 0,
'daily_fine_minutes' => 0,
'daily_fine_amount' => 0,
];
}
// فقط تا دقیقه؛ ثانیه نادیده گرفته می‌شود
$entered = Carbon::parse($enteredAt)->startOfMinute();
// جمعه / تعطیل → بدون تأخیر و جریمه
if (!$this->isWorkDay($entered)) {
return [
'late_minutes' => 0,
'grace_late_minutes' => 0,
'daily_fine_minutes' => 0,
'daily_fine_amount' => 0,
];
}
$threshold = $entered->copy()->setTimeFromTimeString($this->workStartFor($userIdentity))->startOfMinute();
if ($userIdentity) {
$leaveThreshold = $this->approvedLeaveWorkThreshold($userIdentity, $entered);
if ($leaveThreshold === false) {
return [
'late_minutes' => 0,
'grace_late_minutes' => 0,
'daily_fine_minutes' => 0,
'daily_fine_amount' => 0,
];
}
if ($leaveThreshold instanceof Carbon) {
$threshold = $leaveThreshold->copy()->startOfMinute();
}
}
if ($entered->lte($threshold)) {
return [
'late_minutes' => 0,
'grace_late_minutes' => 0,
'daily_fine_minutes' => 0,
'daily_fine_amount' => 0,
];
}
$lateMinutes = (int) $threshold->diffInMinutes($entered);
// تأخیر داخل بازه ارفاق روزانه (۸:۳۰ تا ۸:۴۵) — مبنای جریمه هفتگی
$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,
];
}
/**
* مرخصی تأییدشده: false = معاف کامل، Carbon = شروع مؤثر کار، null = بدون تغییر.
*/
public function approvedLeaveWorkThreshold(string $userIdentity, Carbon $entered): Carbon|false|null
{
$day = $entered->toDateString();
$workStart = $entered->copy()->setTimeFromTimeString($this->workStartFor($userIdentity))->startOfMinute();
$leaves = LeaveRequest::query()
->where('user_identity', $userIdentity)
->where('status', 'approved')
->whereDate('date_from', '<=', $day)
->whereDate('date_to', '>=', $day)
->get();
$latestMorningLeaveEnd = null;
foreach ($leaves as $leave) {
if ($leave->type === 'daily') {
return false;
}
if ($leave->type !== 'hourly' || !$leave->time_from || !$leave->time_to) {
continue;
}
$leaveStart = Carbon::parse($day . ' ' . $leave->time_from)->startOfMinute();
$leaveEnd = Carbon::parse($day . ' ' . $leave->time_to)->startOfMinute();
// مرخصی صبح که شروع کار را پوشش می‌دهد
if ($leaveStart->lte($workStart) && $leaveEnd->gt($workStart)) {
if ($entered->lte($leaveEnd)) {
return false;
}
if ($latestMorningLeaveEnd === null || $leaveEnd->gt($latestMorningLeaveEnd)) {
$latestMorningLeaveEnd = $leaveEnd;
}
}
}
return $latestMorningLeaveEnd;
}
/** دقایق تأخیر داخل بازه ۸:۳۰–۸:۴۵ برای یک رکورد */
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->user_identity);
$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
{
$carbon = $date ?? Carbon::now();
if ($carbon->year < 1000 || $carbon->year > 3000) {
$carbon = Carbon::now();
}
$j = Jalalian::fromCarbon($carbon);
$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;
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
namespace App\Support;
use Carbon\Carbon;
use Morilog\Jalali\Jalalian;
use Throwable;
class JalaliHelper
{
public static function fromDateTime($value): ?Jalalian
{
if ($value === null || $value === '' || $value === '0000-00-00' || str_starts_with((string) $value, '0000-00-00')) {
return null;
}
try {
$carbon = $value instanceof Carbon ? $value->copy() : Carbon::parse($value);
// سال میلادی نامعتبر / خیلی قدیمی → تبدیل شمسی ممکن است year=0 بدهد
if ($carbon->year < 1621 || $carbon->year > 2500) {
return null;
}
$jalali = Jalalian::fromCarbon($carbon);
if ($jalali->getYear() < 1000 || $jalali->getYear() > 3000) {
return null;
}
return $jalali;
} catch (Throwable $e) {
return null;
}
}
public static function format($value, string $format = 'Y/m/d', string $fallback = '-'): string
{
$jalali = self::fromDateTime($value);
return $jalali ? $jalali->format($format) : $fallback;
}
/** تاریخ میلادی امن به صورت Y-m-d */
public static function toGregorianDate($value, string $fallback = ''): string
{
if ($value === null || $value === '' || str_starts_with((string) $value, '0000-00-00')) {
return $fallback;
}
try {
$carbon = $value instanceof Carbon ? $value->copy() : Carbon::parse($value);
if ($carbon->year < 1000 || $carbon->year > 3000) {
return $fallback;
}
return $carbon->format('Y-m-d');
} catch (Throwable $e) {
return $fallback;
}
}
}
+1 -1
View File
@@ -10,7 +10,7 @@
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"morilog/jalali": "^3.4",
"predis/predis": "^3.2"
"predis/predis": "^3.4"
},
"require-dev": {
"fakerphp/faker": "^1.23",
Generated
+39 -30
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "611e1c3c1211e46afd5c8aa1c11d2332",
"content-hash": "9bc7864e4c3a1fb75efe455ec5f4994c",
"packages": [
{
"name": "beberlei/assert",
@@ -2348,25 +2348,27 @@
},
{
"name": "nette/schema",
"version": "v1.3.2",
"version": "v1.3.5",
"source": {
"type": "git",
"url": "https://github.com/nette/schema.git",
"reference": "da801d52f0354f70a638673c4a0f04e16529431d"
"reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d",
"reference": "da801d52f0354f70a638673c4a0f04e16529431d",
"url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002",
"reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002",
"shasum": ""
},
"require": {
"nette/utils": "^4.0",
"php": "8.1 - 8.4"
"php": "8.1 - 8.5"
},
"require-dev": {
"nette/tester": "^2.5.2",
"phpstan/phpstan-nette": "^1.0",
"nette/phpstan-rules": "^1.0",
"nette/tester": "^2.6",
"phpstan/extension-installer": "^1.4@stable",
"phpstan/phpstan": "^2.1.39@stable",
"tracy/tracy": "^2.8"
},
"type": "library",
@@ -2376,6 +2378,9 @@
}
},
"autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [
"src/"
]
@@ -2404,35 +2409,37 @@
],
"support": {
"issues": "https://github.com/nette/schema/issues",
"source": "https://github.com/nette/schema/tree/v1.3.2"
"source": "https://github.com/nette/schema/tree/v1.3.5"
},
"time": "2024-10-06T23:10:23+00:00"
"time": "2026-02-23T03:47:12+00:00"
},
{
"name": "nette/utils",
"version": "v4.0.7",
"version": "v4.1.4",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2"
"reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2",
"reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2",
"url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7",
"reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7",
"shasum": ""
},
"require": {
"php": "8.0 - 8.4"
"php": "8.2 - 8.5"
},
"conflict": {
"nette/finder": "<3",
"nette/schema": "<1.2.2"
},
"require-dev": {
"jetbrains/phpstorm-attributes": "dev-master",
"jetbrains/phpstorm-attributes": "^1.2",
"nette/phpstan-rules": "^1.0",
"nette/tester": "^2.5",
"phpstan/phpstan": "^1.0",
"phpstan/extension-installer": "^1.4@stable",
"phpstan/phpstan": "^2.1@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
@@ -2446,10 +2453,13 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.0-dev"
"dev-master": "4.1-dev"
}
},
"autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [
"src/"
]
@@ -2490,9 +2500,9 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.0.7"
"source": "https://github.com/nette/utils/tree/v4.1.4"
},
"time": "2025-06-03T04:55:08+00:00"
"time": "2026-05-11T20:49:54+00:00"
},
{
"name": "nikic/php-parser",
@@ -2716,16 +2726,16 @@
},
{
"name": "predis/predis",
"version": "v3.2.0",
"version": "v3.4.2",
"source": {
"type": "git",
"url": "https://github.com/predis/predis.git",
"reference": "9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8"
"reference": "2033429520d8997a7815a2485f56abe6d2d0e075"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/predis/predis/zipball/9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8",
"reference": "9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8",
"url": "https://mirror2.chabokan.net/composer/p/predis/predis/v3.4.2",
"reference": "2033429520d8997a7815a2485f56abe6d2d0e075",
"shasum": ""
},
"require": {
@@ -2747,15 +2757,14 @@
"Predis\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Till Krüss",
"homepage": "https://till.im",
"role": "Maintainer"
"role": "Maintainer",
"homepage": "https://till.im"
}
],
"description": "A flexible and feature-complete Redis/Valkey client for PHP.",
@@ -2767,7 +2776,7 @@
],
"support": {
"issues": "https://github.com/predis/predis/issues",
"source": "https://github.com/predis/predis/tree/v3.2.0"
"source": "https://github.com/predis/predis/tree/v3.4.2"
},
"funding": [
{
@@ -2775,7 +2784,7 @@
"type": "github"
}
],
"time": "2025-08-06T06:41:24+00:00"
"time": "2026-03-09T20:33:04+00:00"
},
{
"name": "psr/clock",
@@ -8296,5 +8305,5 @@
"php": "^8.2"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}
+8
View File
@@ -40,6 +40,14 @@ return [
'driver' => 'session',
'provider' => 'users',
],
'user' => [
'driver' => 'session',
'provider' => 'users',
],
'customer' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [
'driver' => 'jwt',
'provider' => 'admins',
Regular → Executable
View File
@@ -0,0 +1,89 @@
<?php
/**
* تعطیلات رسمی ایران بر اساس تقویم رسمی (مناسبت‌های ملی و مذهبی).
* جمعه‌ها جداگانه در LatePenaltyService محاسبه می‌شوند و اینجا نیستند.
*
* منابع تقریبی: تقویم رسمی / فهرست‌های منتشرشده برای ۱۴۰۵ و ۱۴۰۶.
* تاریخ‌های قمری ممکن است با اعلام رؤیت هلال یک روز جابه‌جا شوند.
*
* @return array<int, list<array{month:int, day:int, title:string}>>
*/
return [
1405 => [
['month' => 1, 'day' => 1, 'title' => 'آغاز نوروز / عید سعید فطر'],
['month' => 1, 'day' => 2, 'title' => 'عید نوروز / تعطیل عید فطر'],
['month' => 1, 'day' => 3, 'title' => 'عید نوروز'],
['month' => 1, 'day' => 4, 'title' => 'عید نوروز'],
['month' => 1, 'day' => 12, 'title' => 'روز جمهوری اسلامی ایران'],
['month' => 1, 'day' => 13, 'title' => 'روز طبیعت (سیزده‌بدر)'],
['month' => 1, 'day' => 24, 'title' => 'شهادت امام جعفر صادق (ع)'],
['month' => 3, 'day' => 3, 'title' => 'شهادت امام محمد باقر (ع)'],
['month' => 3, 'day' => 6, 'title' => 'عید سعید قربان'],
['month' => 3, 'day' => 14, 'title' => 'عید سعید غدیر خم / رحلت امام خمینی (ره)'],
['month' => 3, 'day' => 15, 'title' => 'قیام ۱۵ خرداد'],
['month' => 4, 'day' => 3, 'title' => 'تاسوعای حسینی'],
['month' => 4, 'day' => 4, 'title' => 'عاشورای حسینی'],
['month' => 5, 'day' => 13, 'title' => 'اربعین حسینی'],
['month' => 5, 'day' => 21, 'title' => 'رحلت رسول اکرم (ص) و شهادت امام حسن مجتبی (ع)'],
['month' => 5, 'day' => 22, 'title' => 'شهادت امام رضا (ع)'],
['month' => 5, 'day' => 30, 'title' => 'شهادت امام حسن عسکری (ع)'],
['month' => 6, 'day' => 8, 'title' => 'میلاد رسول اکرم (ص) و امام جعفر صادق (ع)'],
['month' => 8, 'day' => 22, 'title' => 'شهادت حضرت فاطمه زهرا (س)'],
['month' => 10, 'day' => 2, 'title' => 'ولادت امام علی (ع) و روز پدر'],
['month' => 10, 'day' => 16, 'title' => 'مبعث حضرت رسول اکرم (ص)'],
['month' => 11, 'day' => 4, 'title' => 'ولادت حضرت قائم (عج) / نیمه شعبان'],
['month' => 11, 'day' => 22, 'title' => 'پیروزی انقلاب اسلامی ایران'],
['month' => 12, 'day' => 9, 'title' => 'شهادت امام علی (ع)'],
['month' => 12, 'day' => 19, 'title' => 'عید سعید فطر'],
['month' => 12, 'day' => 20, 'title' => 'تعطیل به مناسبت عید فطر'],
['month' => 12, 'day' => 29, 'title' => 'روز ملی شدن صنعت نفت ایران'],
],
1406 => [
['month' => 1, 'day' => 1, 'title' => 'آغاز نوروز'],
['month' => 1, 'day' => 2, 'title' => 'عید نوروز'],
['month' => 1, 'day' => 3, 'title' => 'عید نوروز'],
['month' => 1, 'day' => 4, 'title' => 'عید نوروز'],
['month' => 1, 'day' => 12, 'title' => 'روز جمهوری اسلامی ایران'],
['month' => 1, 'day' => 13, 'title' => 'روز طبیعت (سیزده‌بدر)'],
['month' => 1, 'day' => 14, 'title' => 'شهادت امام جعفر صادق (ع)'],
['month' => 2, 'day' => 24, 'title' => 'شهادت امام محمد باقر (ع)'],
['month' => 2, 'day' => 27, 'title' => 'عید سعید قربان'],
['month' => 3, 'day' => 4, 'title' => 'عید سعید غدیر خم'],
['month' => 3, 'day' => 14, 'title' => 'رحلت امام خمینی (ره)'],
['month' => 3, 'day' => 15, 'title' => 'قیام ۱۵ خرداد'],
['month' => 3, 'day' => 24, 'title' => 'تاسوعای حسینی'],
['month' => 3, 'day' => 25, 'title' => 'عاشورای حسینی'],
['month' => 5, 'day' => 3, 'title' => 'اربعین حسینی'],
['month' => 5, 'day' => 11, 'title' => 'رحلت رسول اکرم (ص) و شهادت امام حسن مجتبی (ع)'],
['month' => 5, 'day' => 12, 'title' => 'شهادت امام رضا (ع)'],
['month' => 5, 'day' => 20, 'title' => 'شهادت امام حسن عسکری (ع)'],
['month' => 5, 'day' => 29, 'title' => 'میلاد رسول اکرم (ص) و امام جعفر صادق (ع)'],
['month' => 8, 'day' => 12, 'title' => 'شهادت حضرت فاطمه زهرا (س)'],
['month' => 9, 'day' => 21, 'title' => 'ولادت امام علی (ع) و روز پدر'],
['month' => 10, 'day' => 5, 'title' => 'مبعث حضرت رسول اکرم (ص)'],
['month' => 10, 'day' => 23, 'title' => 'ولادت حضرت قائم (عج) / نیمه شعبان'],
['month' => 11, 'day' => 22, 'title' => 'پیروزی انقلاب اسلامی ایران'],
['month' => 11, 'day' => 28, 'title' => 'شهادت امام علی (ع)'],
['month' => 12, 'day' => 8, 'title' => 'عید سعید فطر'],
['month' => 12, 'day' => 9, 'title' => 'تعطیل به مناسبت عید فطر'],
['month' => 12, 'day' => 29, 'title' => 'روز ملی شدن صنعت نفت ایران'],
],
];
View File
View File
@@ -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']);
});
}
};
@@ -0,0 +1,35 @@
<?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::create('leave_requests', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id')->nullable();
$table->string('user_identity');
$table->string('type'); // daily | hourly
$table->date('date_from');
$table->date('date_to');
$table->time('time_from')->nullable();
$table->time('time_to')->nullable();
$table->text('reason')->nullable();
$table->string('status')->default('pending'); // pending | approved | rejected
$table->unsignedBigInteger('reviewed_by')->nullable();
$table->timestamp('reviewed_at')->nullable();
$table->timestamps();
$table->index(['user_identity', 'status']);
$table->index(['date_from', 'date_to']);
});
}
public function down(): void
{
Schema::dropIfExists('leave_requests');
}
};
@@ -0,0 +1,23 @@
<?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::create('holidays', function (Blueprint $table) {
$table->id();
$table->date('date')->unique();
$table->string('title')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('holidays');
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasTable('holidays')) {
return;
}
Artisan::call('holidays:seed');
}
public function down(): void
{
// تعطیلات seed‌شده را عمداً حذف نمی‌کنیم
}
};
@@ -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->time('work_start_time')->nullable();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('work_start_time');
});
}
};
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Artisan;
class HolidaySeeder extends Seeder
{
public function run(): void
{
Artisan::call('holidays:seed');
}
}
Generated Regular → Executable
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "a.montask",
"name": "a_montask",
"lockfileVersion": 3,
"requires": true,
"packages": {
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File

Before

Width:  |  Height:  |  Size: 496 KiB

After

Width:  |  Height:  |  Size: 496 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.9 MiB

View File

Before

Width:  |  Height:  |  Size: 910 B

After

Width:  |  Height:  |  Size: 910 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 943 B

After

Width:  |  Height:  |  Size: 943 B

View File

Before

Width:  |  Height:  |  Size: 1020 B

After

Width:  |  Height:  |  Size: 1020 B

View File

Before

Width:  |  Height:  |  Size: 435 B

After

Width:  |  Height:  |  Size: 435 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 572 B

After

Width:  |  Height:  |  Size: 572 B

View File

Before

Width:  |  Height:  |  Size: 1017 B

After

Width:  |  Height:  |  Size: 1017 B

View File

Before

Width:  |  Height:  |  Size: 914 B

After

Width:  |  Height:  |  Size: 914 B

View File

Before

Width:  |  Height:  |  Size: 913 B

After

Width:  |  Height:  |  Size: 913 B

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 888 B

After

Width:  |  Height:  |  Size: 888 B

View File

Before

Width:  |  Height:  |  Size: 773 B

After

Width:  |  Height:  |  Size: 773 B

View File

Before

Width:  |  Height:  |  Size: 549 B

After

Width:  |  Height:  |  Size: 549 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Some files were not shown because too many files have changed in this diff Show More