Improve tasks UI, dark theme, performance fixes, and admin pages polish

This commit is contained in:
2026-06-02 18:45:01 +03:30
parent 54aa8f40dd
commit 300575930f
18 changed files with 2598 additions and 780 deletions
+147 -41
View File
@@ -22,6 +22,128 @@ use App\Models\Pivot_Tasks;
class AdminController extends Controller
{
private function loadPendingRequests($users): array
{
$records = [];
try {
Redis::connection()->ping();
} catch (\Throwable) {
foreach ($users as $user) {
$records[] = [
'user_name' => $user->user_name,
'name' => $user->name,
'attendances' => [],
];
}
return $records;
}
foreach ($users as $user) {
$rec = [
'user_name' => $user->user_name,
'name' => $user->name,
'attendances' => [],
];
foreach (Redis::keys("att-tempreq:{$user->user_name}:*") as $key) {
if (str_contains($key, ':creates:')) {
continue;
}
$raw = Redis::get($key);
if (!$raw) {
continue;
}
$get = json_decode($raw);
if ($get && ($get->function ?? null) !== 'rejected') {
$rec['attendances'][] = $get;
}
}
$createRequests = Redis::lrange("att-tempreq:creates:{$user->user_name}", 0, -1);
foreach ($createRequests as $reqJson) {
$get = json_decode($reqJson);
if ($get && ($get->function ?? null) !== 'rejected') {
$rec['attendances'][] = $get;
}
}
$records[] = $rec;
}
return $records;
}
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);
$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,60 +151,44 @@ 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'])
->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);
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" => 'شما به پنل ادمین وارد شدید✔️'
+42 -30
View File
@@ -26,14 +26,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();
@@ -48,23 +48,27 @@ 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;
$att->date = $date != null ? $date->format('Y/m/d') : null;
@@ -83,33 +87,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) {
+277 -46
View File
@@ -101,6 +101,236 @@ 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 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 buildTasksPayload($tasks, array $usersById = []): array
{
$payload = [];
foreach ($tasks as $task) {
$payload[$task->id] = [
'id' => $task->id,
'title' => $task->title,
'description' => $task->description,
'start_date' => $task->start_date,
'target_date' => $task->target_date,
'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' => $task->start_date,
'target_date' => $task->target_date,
'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'])
->whereIn('task_id', $taskIds)
->whereNotNull('duration');
if (!$isAdmin) {
$durationQuery->where('user_id', $user->id);
}
$durationsByTaskId = $durationQuery->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) : [];
$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,61 +365,27 @@ 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', 'appName'])
->orderByRaw('target_date IS NULL')
->orderBy('target_date', 'asc')
->orderBy('updated_at', '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)
// ->orderByRaw('target_date IS NULL')
// ->orderBy('target_date', 'asc')
->orderBy('updated_at', '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')->get(),
'appsList' => App::select('id', 'name_fa', 'type')->get(),
'priorityLabels' => [
'urgent' => 'فوری',
'high' => 'بالا',
@@ -220,6 +416,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());
@@ -342,6 +569,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) {
+5
View File
@@ -62,6 +62,11 @@ class Tasks extends Model
return $this->belongsTo(User::class);
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function appName(){
return $this->belongsTo(App::class, 'app' , 'id');
}
Generated
+32 -22
View File
@@ -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",
@@ -8288,12 +8298,12 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.2"
},
"platform-dev": [],
"plugin-api-version": "2.6.0"
"platform-dev": {},
"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',
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "a.montask",
"name": "a_montask",
"lockfileVersion": 3,
"requires": true,
"packages": {
+37 -111
View File
@@ -1,63 +1,9 @@
<?php
use App\Models\Attendance;
use App\Models\AttendanceBreak;
use Morilog\Jalali\Jalalian;
use Carbon\Carbon;
//dd($reqs);
$now = Jalalian::fromCarbon(Carbon::now());
foreach ($users as $user) {
$totaltime = 0;
$tsec = 0;
foreach ($user->attendances as $att) {
$date = $att->entered_at != null ? Jalalian::fromCarbon(Carbon::parse($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, $s] = explode(':', $att->duration);
$tsec += $h * 3600 + $m * 60 + $s;
}
}
$hours = floor($tsec / 3600);
$minutes = floor(($tsec % 3600) / 60);
$seconds = $tsec % 60;
$totalTime = sprintf('%02d:%02d', $hours, $minutes);
$user->totaltime = $totalTime;
}
//dd($users);
$now = now()->format('Y-m-d');
foreach ($users as $u) {
$us = $u->attendances()
->whereNull('exited_at')
->latest('entered_at')
->first();
// dump($us);
if ($us) {
$us->entered_at = Carbon::parse($us->entered_at)->format('Y-m-d');
if ($us->entered_at == $now)
$us = true;
else
$us = false;
$u->working = $us ? 1 : 0;
} else
$u->working = 0;
foreach ($u->attendances as $u_att) {
$break = AttendanceBreak::wherenull('break_end')->where('attendance_id', $u_att->id)->exists();
if ($break)
$u->working = 2;
}
}
//dd($users);
$users = $users->sortByDesc('working');
//$users = json_decode(json: $users);
// $users = collect($users);
$usersChunked = $users->chunk(3);
//dd($users);
?>
@@ -66,43 +12,35 @@ $usersChunked = $users->chunk(3);
<div class="container w-full flex flex-col items-center">
@if(!collect($reqs)->pluck('attendances')->flatten()->isEmpty())
<div class="mt-10 bg-white rounded-lg shadow-md max-w-[350px] flex flex-col items-center">
<p style="min-font-size: 30; text-align: center; margin-bottom: 10px">درخواست ها</p>
{{-- {{dd($reqs)}}--}}
<div class="ui-requests-card flex flex-col items-center">
<p>درخواست ها</p>
@foreach ($reqs as $u)
@if(!empty($u['attendances']))
<button type="button"
class="w-[150px] p-3 font-bold text-white bg-blue-500 hover:bg-blue-700 rounded transition duration-150 text-center "
onclick="togglerequests('attendance-{{ $loop->index }}')"
style="margin: 10px 0; text-align: center; background-color: #00bcd4">
class="ui-chip-btn"
onclick="togglerequests('attendance-{{ $loop->index }}')">
{{ $u['name'] }}
</button>
{{-- @else--}}
{{-- <p class="text-stone-400" style="text-align: center">درخواستی ثبت نشده</p>--}}
{{-- @break--}}
@endif
<div id="attendance-{{ $loop->index }}" class="hidden-content pt-4">
<table style="max-width: 350px; margin: auto; text-align: center" class="w-[350px] table-auto">
<div id="attendance-{{ $loop->index }}" class="hidden-content pt-4 w-full">
<div class="ui-table-wrap">
<table class="w-full table-auto">
<thead>
<tr class="text-sm" style="background-color: #00a3ba; color: white; text-align: center;">
<th class="px-2 py-2 border w-1/6">درخواست</th>
<th class="px-2 py-2 border w-1/6">تاریخ</th>
<th class="px-2 py-2 border w-1/6">ورود</th>
<th class="px-2 py-2 border w-1/6">خروج</th>
{{-- @if($userType == 'admin')--}}
<th class="px-2 py-2 border w-1/6">تعیین وضعیت</th>
{{-- @endif--}}
<tr>
<th class="px-2 py-2 w-1/6">درخواست</th>
<th class="px-2 py-2 w-1/6">تاریخ</th>
<th class="px-2 py-2 w-1/6">ورود</th>
<th class="px-2 py-2 w-1/6">خروج</th>
<th class="px-2 py-2 w-1/6">تعیین وضعیت</th>
</tr>
</thead>
<tbody>
{{-- {{dd($u['attendances'])}}--}}
@forelse($u['attendances'] as $rec)
<tr style="background-color: {{$rec->id=='new' ? 'lightblue' : 'white'}}">
<tr class="{{ $rec->id=='new' ? 'is-new' : '' }}">
{{--{{dd($rec)}}--}}
<td class="px-4 py-2 border">{{ match ($rec->function) {
'edit' => "تغییر ساعت",
@@ -138,8 +76,7 @@ $usersChunked = $users->chunk(3);
<input type="hidden" name="update" id="update" value="approve">
<input type="hidden" name="data" id="data"
value="{{json_encode($rec)}}">
<button class="bg-white rounded w-full"
style="background-color: #81ff65; margin-bottom: 5px">
<button type="submit" class="ui-btn-approve">
تایید
</button>
</form>
@@ -148,8 +85,7 @@ $usersChunked = $users->chunk(3);
<input type="hidden" name="update" id="update" value="reject">
<input type="hidden" name="data" id="data"
value="{{json_encode($rec)}}">
<button class="bg-white rounded w-full"
style="background-color: #ff6565">
<button type="submit" class="ui-btn-reject">
رد
</button>
</form>
@@ -163,7 +99,8 @@ $usersChunked = $users->chunk(3);
@endforelse
</tbody>
</table>
</div> {{-- پایان hidden-content --}}
</div>
</div>
@endforeach
{{-- @if(collect($reqs)->pluck('attendances')->flatten()->isEmpty())--}}
{{-- <p class="text-stone-400" style="text-align: center">درخواستی ثبت نشده</p>--}}
@@ -175,38 +112,29 @@ $usersChunked = $users->chunk(3);
<div class="grid grid-cols-2 grid-rows-none ">
@foreach ($usersChunked as $chunk)
@foreach ($chunk as $u)
<div class="form m-3 justify-evenly rounded-lg shadow-md "
style="align-items: center; text-align: center; border: 2px solid
{{ $u->active_task ? '#00ff15' : ($u->working == 1 ? '#16c8ce' : ($u->working == 2 ? 'orange' : 'white' ))}}; padding: 10px; ">
@php
@endphp
<a href="/admin/{{ $u->user_name }}"
class=" rounded-xl"
style="text-decoration: none; color: inherit;">
<p style="font-weight: bold ; color:#333;">
{{ $u->name ?? $u->user_name }}
</p>
@php
$cardClass = 'ui-user-card m-3';
if ($u->active_task) {
$cardClass .= ' is-active';
} elseif ($u->working == 1) {
$cardClass .= ' is-working';
} elseif ($u->working == 2) {
$cardClass .= ' is-break';
}
@endphp
<div class="{{ $cardClass }}">
<a href="/admin/{{ $u->user_name }}">
<p class="ui-user-name">{{ $u->name ?? $u->user_name }}</p>
@if($u->active_task)
<p>وظیفه: {{ $u->active_task }}</p>
@endif
<p>
کارکرد ماه جاری:
{{ $u->totaltime }}
</p>
<p>
تعداد درخواست ها:
{{ $u->req_counts }}
</p>
<p>کارکرد ماه جاری: {{ $u->totaltime }}</p>
<p>تعداد درخواست ها: {{ $u->req_counts }}</p>
</a>
@if($userType == 'admin')
<button type="button"
class="rounded-lg bg-cyan-500 hover:bg-cyan-600"
onclick="window.location.href='{{ url('attendance/profile').'?username='.$u->user_name }}'"
style=" border: none; color: white; padding: 5px 10px; text-align: center; text-decoration: none;
display: inline-block; font-size: 16px; margin: 4px 2px; cursor: pointer; ">
class="ui-chip-btn mt-2 !min-w-0 !w-auto !text-sm"
onclick="window.location.href='{{ url('attendance/profile').'?username='.$u->user_name }}'">
ویرایش
</button>
@endif
@@ -214,10 +142,8 @@ $usersChunked = $users->chunk(3);
@endforeach
@endforeach
<a class="form m-3 justify-evenly rounded-lg shadow-md " href="{{route('admin.new')}}"
style="align-items: center; text-align: center; ">
<i class="fa-regular fa-plus "
style="font-size: 80px; width: 90%; height: 90%;"></i>
<a class="ui-user-card m-3 flex items-center justify-center min-h-[140px]" href="{{route('admin.new')}}">
<i class="fa-regular fa-plus text-5xl text-gray-400"></i>
</a>
</div>
+12 -14
View File
@@ -224,7 +224,7 @@ SVG;
@else
<div class="bg-white flex justify-flex p-4 rounded-xl max-w-[350px] flex-col items-center">
<div class="ui-panel flex p-4 rounded-xl max-w-[350px] flex-col items-center">
<img src="{{$user->picture ?? $gravatarUrl}}" alt="Profile Picture" class="w-16 h-16 rounded-full ">
<label>
@@ -361,7 +361,7 @@ SVG;
<div class="w-full flex justify-center mt-4" style="">
<div class="overflow-hidden border border-gray-300 rounded-[23px] bg-white max-w-[350px]">
<div class="overflow-hidden border border-gray-300 rounded-[23px] ui-panel max-w-[350px]">
<div class=" border-b border-gray-300 text-center w-full">
@@ -369,7 +369,7 @@ SVG;
<div class="w-[350px]">
@if(isset($_COOKIE['user_identity']))
<table class="min-w-full bg-white shadow-md rounded-lg table-auto">
<table class="min-w-full ui-attendance-table table-auto">
<thead class=" text-black sticky top-0 z-10"
style="background: rgba(244,244,244,0.82);border-bottom: 1px solid;border-color: #e1e1e1 ">
<tr>
@@ -456,10 +456,10 @@ SVG;
<!-- ATTENDANCE Modal -->
<div id="modal-container"
class="fixed inset-0 z-50 hidden bg-black bg-opacity-60 flex items-center justify-center p-4">
<div id="modal-content" class=" bg-white p-4 sm:p-6 rounded-lg shadow-lg max-w-sm w-full relative">
<h2 id="modal-title" class="text-xl sm:text-2xl font-semibold mb-4 text-center"></h2>
<p id="modal-body" class="text-gray-700 mb-4 text-center"></p>
class="ui-modal-backdrop centered hidden">
<div id="modal-content" class="ui-modal-panel max-w-sm w-full">
<h2 id="modal-title" class="ui-title"></h2>
<p id="modal-body" class="ui-subtitle"></p>
<div class="flex flex-col gap-4" style="align-items: center">
<div class="flex justify-center gap-2">
@@ -559,16 +559,14 @@ SVG;
{{-- START TASK MODAL --}}
<div id="taskModal"
class="hidden fixed inset-0 bg-black/40 items-start justify-center z-50 flex backdrop-blur-sm scroll-auto pt-10">
<div class="relative bg-white w-[350px] rounded-2xl shadow-lg p-5 text-center">
<button onclick="closeTaskModal()"
class="absolute top-3 right-3 text-gray-400 hover:text-gray-600 transition">
class="ui-modal-backdrop hidden">
<div class="ui-modal-panel">
<button type="button" onclick="closeTaskModal()" class="ui-modal-close" aria-label="بستن">
&times;
</button>
<h2 class="text-lg font-bold mb-3" id="modalTitle">انتخاب وظیفه</h2>
<p class="text-gray-600 mb-4" id="modalDesc">یکی از وظایف زیر را انتخاب کنید:</p>
<h2 class="ui-title" id="modalTitle">انتخاب وظیفه</h2>
<p class="ui-subtitle" id="modalDesc">یکی از وظایف زیر را انتخاب کنید:</p>
<div class="grid flex-col gap-2 grid-cols-2 pb-3 items-center justify-items-end">
@if(!empty($user->tasks))
+2 -3
View File
@@ -3,10 +3,9 @@
<div class=" flex flex-col items-center justify-center p-4 " dir="rtl"
style="padding-top: 50px">
<div class="bg-white p-6 md:p-8 rounded-2xl shadow-2xl w-[350px] max-w-sm border-t-8 border-[#16c8ce]">
<div class="ui-card ui-card-body w-[350px] max-w-sm" style="border-top: 4px solid var(--primary-color);">
<div class="text-center mb-6">
<h1 class="text-2xl font-extrabold text-gray-800 border-b pb-2">کاربر جدید</h1>
<h1 class="ui-title border-b border-gray-300 pb-2">کاربر جدید</h1>
</div>
<form id="attendanceForm" action="{{route('admin.new')}}" method="post" class="space-y-4">
+273 -86
View File
@@ -3,21 +3,17 @@
use Morilog\Jalali\Jalalian;
use Carbon\Carbon;
// پیدا کردن نام ماه قبل به تاریخ شمسی (مثلا اگر الان اسفند است، خروجی "بهمن" خواهد بود)
$previousMonthName = Jalalian::now()->subMonths(1)->format('%B');
// محاسبه مجموع کارکرد برای هر کارمند به تفکیک ماه
$employeeTotals = [];
foreach ($data as $employee) {
// تبدیل آبجکت به آرایه در صورت نیاز برای حلقه
$recordsGrouped = is_array($employee->records) ? $employee->records : (array)$employee->records;
$monthlyTotals = [];
foreach ($recordsGrouped as $month => $attendances) {
$tsec = 0;
foreach ($attendances as $attendance) {
// اگر duration موجود باشه استفاده می‌کنیم
if (!empty($attendance->duration)) {
[$h, $m, $s] = explode(':', $attendance->duration);
$tsec += (int)$h * 3600 + (int)$m * 60 + (int)$s;
@@ -26,29 +22,239 @@ foreach ($data as $employee) {
$hours = floor($tsec / 3600);
$minutes = floor(($tsec % 3600) / 60);
// ذخیره زمان برای این ماه
$monthlyTotals[$month] = sprintf('%02d:%02d', $hours, $minutes);
}
// ذخیره کل ماه‌ها برای این کارمند
$employeeTotals[$employee->id] = $monthlyTotals;
}
?>
@extends('app')
@section('content')
<div class="container flex flex-col items-center justify-center mx-auto pb-10">
@section('style')
<style>
.admin-show-page,
.ui-page {
width: 100%;
max-width: 420px;
margin: 0 auto;
padding: 0 0.5rem 1.5rem;
}
.admin-show-empty,
.ui-empty {
margin-top: 5rem;
text-align: center;
color: var(--text-muted);
font-size: 1.1rem;
font-weight: 700;
}
.employee-picker {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.45rem;
margin: 0.75rem 0 1rem;
padding: 0.75rem;
background: var(--surface-glass);
border: 1px solid var(--border-glass);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.emp-btn {
padding: 0.5rem 0.85rem;
border-radius: 9999px;
font-size: 0.82rem;
font-weight: 700;
border: 1px solid var(--border-input);
background: var(--surface-input);
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.emp-btn:hover {
color: var(--text-primary);
background: var(--surface-tab-hover);
border-color: color-mix(in srgb, var(--primary-color) 35%, var(--border-input));
}
.emp-btn-active {
color: #fff;
background: var(--primary-color);
border-color: color-mix(in srgb, var(--primary-color) 80%, white);
box-shadow: 0 6px 18px color-mix(in srgb, var(--primary-color) 35%, transparent);
}
.employee-panel {
display: none;
flex-direction: column;
align-items: center;
width: 100%;
}
.employee-panel.is-visible {
display: flex;
}
.employee-header {
width: 100%;
max-width: 350px;
display: flex;
flex-direction: column;
gap: 0.65rem;
margin-bottom: 0.85rem;
}
.employee-header h2 {
margin: 0;
font-size: 1.15rem;
font-weight: 800;
color: var(--text-primary);
}
.employee-stat {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 0.35rem;
padding: 0.55rem 0.85rem;
border-radius: var(--radius-lg);
background: color-mix(in srgb, var(--primary-color) 12%, var(--surface-card));
border: 1px solid color-mix(in srgb, var(--primary-color) 28%, var(--border-subtle));
color: var(--text-secondary);
font-size: 0.78rem;
}
.employee-stat strong {
font-size: 0.95rem;
font-weight: 800;
color: color-mix(in srgb, var(--primary-color) 70%, var(--text-primary));
font-variant-numeric: tabular-nums;
}
.month-tabs {
width: 100%;
max-width: 350px;
display: flex;
gap: 0.35rem;
overflow-x: auto;
padding: 0.45rem;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
background: var(--surface-tabs);
border: 1px solid var(--border-glass);
border-bottom: none;
scrollbar-width: none;
}
.month-tabs::-webkit-scrollbar {
display: none;
}
.month-tab {
flex-shrink: 0;
padding: 0.5rem 0.85rem;
border: none;
border-radius: 0.75rem;
background: transparent;
color: var(--text-muted);
font-size: 0.82rem;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
}
.month-tab:hover {
color: var(--text-primary);
background: var(--surface-tab-hover);
}
.month-tab-active {
color: color-mix(in srgb, var(--primary-color) 78%, var(--text-primary));
background: var(--surface-tab-active);
box-shadow: var(--shadow-soft);
}
.attendance-card {
width: 100%;
max-width: 350px;
background: var(--surface-glass);
border: 1px solid var(--border-glass);
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
box-shadow: var(--shadow-soft);
overflow: hidden;
}
.attendance-table {
width: 100%;
border-collapse: collapse;
text-align: center;
}
.attendance-table thead th {
padding: 0.65rem 0.35rem;
font-size: 0.72rem;
font-weight: 700;
color: var(--text-muted);
background: var(--surface-tabs);
border-bottom: 1px solid var(--border-subtle);
}
.attendance-table thead th:not(:last-child) {
border-left: 1px solid var(--border-subtle);
}
.attendance-table tbody td {
padding: 0.5rem 0.25rem;
font-size: 0.82rem;
color: var(--text-primary);
border-bottom: 1px solid var(--border-subtle);
font-variant-numeric: tabular-nums;
}
.attendance-table tbody td:not(:last-child) {
border-left: 1px solid var(--border-subtle);
}
.attendance-table tbody tr:hover td {
background: var(--surface-tab-hover);
}
.attendance-table tfoot td {
padding: 0.75rem 0.5rem;
font-size: 0.85rem;
font-weight: 800;
color: var(--text-primary);
background: color-mix(in srgb, var(--primary-color) 10%, var(--surface-card));
border-top: 2px solid var(--border-subtle);
}
.panel-empty {
margin: 2.5rem 0;
text-align: center;
color: var(--text-muted);
font-weight: 700;
}
</style>
@endsection
@section('content')
<div class="admin-show-page">
@if(empty($data) || count($data) == 0)
<div class="mt-80 text-gray-500 text-xl font-bold italic">
<div class="admin-show-empty">
<h1>اطلاعاتی موجود نمیباشد</h1>
</div>
@else
<div class="flex flex-wrap justify-center gap-2 mt-10 mb-6 max-w-3xl">
<div class="employee-picker">
@foreach($data as $employee)
<button class="emp-btn px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-cyan-500 hover:text-white transition-colors duration-300 font-bold shadow-sm"
<button type="button"
class="emp-btn"
data-emp-id="{{ $employee->id }}"
onclick="openEmployee(event, 'emp_{{ $employee->id }}', '{{ $employee->id }}')">
{{ $employee->name }}
</button>
@@ -56,15 +262,13 @@ foreach ($data as $employee) {
</div>
@foreach($data as $employee)
<div id="emp_{{ $employee->id }}" class="employee-content hidden flex-col items-center justify-center w-full">
<div class="flex justify-between items-center mb-4 w-[350px]">
<h2 class="text-xl font-bold text-cyan-700">{{ $employee->name }}</h2>
<span class="text-xs bg-cyan-50 text-cyan-800 px-3 py-1.5 rounded-full border border-cyan-200 shadow-sm flex items-center gap-1">
<div id="emp_{{ $employee->id }}" class="employee-panel">
<div class="employee-header">
<h2>{{ $employee->name }}</h2>
<div class="employee-stat">
<span>کارکرد ماه قبل ({{ $previousMonthName }}):</span>
<strong class="text-sm font-black">{{ $employeeTotals[$employee->id][$previousMonthName] ?? '00:00' }}</strong>
</span>
<strong>{{ $employeeTotals[$employee->id][$previousMonthName] ?? '00:00' }}</strong>
</div>
</div>
@php
@@ -72,13 +276,14 @@ foreach ($data as $employee) {
@endphp
@if(empty($recordsGrouped))
<div class="my-10 text-gray-500 text-lg font-bold italic">
<div class="panel-empty">
رکورد ترددی برای این شخص ثبت نشده است.
</div>
@else
<div class="tab rounded-t-2xl max-w-[350px] mx-auto flex overflow-x-auto whitespace-nowrap hide-scrollbar" style="max-width: 350px">
<div class="month-tabs">
@foreach(array_keys($recordsGrouped) as $month)
<button class="tablinks-{{ $employee->id }} active rounded-t-2xl cursor-pointer px-4 py-2 bg-gray-100 border-b-2 border-transparent hover:bg-gray-200 flex-shrink-0"
<button type="button"
class="month-tab tablinks-{{ $employee->id }}"
onclick="openMonth(event, 'month_{{ $employee->id }}_{{ $month }}', '{{ $employee->id }}')">
{{ $month }}
</button>
@@ -86,43 +291,37 @@ foreach ($data as $employee) {
</div>
@foreach($recordsGrouped as $month => $records)
<div id="month_{{ $employee->id }}_{{ $month }}" class="tabcontent-{{ $employee->id }} hidden w-[350px] bg-white rounded-b-xl rounded-tl-2xl cursor-default leading-7 shadow-lg border border-gray-100">
<table style="width:100%; border-collapse: collapse; text-align: center">
<div id="month_{{ $employee->id }}_{{ $month }}"
class="attendance-card tabcontent-{{ $employee->id }}"
style="display: none;">
<table class="attendance-table">
<thead>
<tr style="border-bottom: 1px solid #ddd; background-color: #f8f9fa;">
<th style="border-right: 1px solid #ddd; padding: 8px 0;">تاریخ</th>
<th style="border-right: 1px solid #ddd;">روز</th>
<th style="border-right: 1px solid #ddd;">ورود</th>
<th style="border-right: 1px solid #ddd;">خروج</th>
<th style="border-right: 1px solid #ddd;">مدت</th>
<tr>
<th>تاریخ</th>
<th>روز</th>
<th>ورود</th>
<th>خروج</th>
<th>مدت</th>
</tr>
</thead>
<tbody>
@foreach($records as $row)
<tr class="hover:bg-cyan-100 transition-colors" style="border-bottom: 1px solid #ddd;">
<td style="border-right: 1px solid #ddd; padding: 4px 0;">
{{ Jalalian::fromCarbon(\Carbon\Carbon::parse($row->entered_at))->format('m/d') ?? '-' }}
</td>
<td style="border-right: 1px solid #ddd;">
{{ Jalalian::fromCarbon(\Carbon\Carbon::parse($row->entered_at))->format('%A') ?? '-' }}
</td>
<td style="border-right: 1px solid #ddd;">
{{ \Carbon\Carbon::parse($row->entered_at)->format('H:i') ?? '-' }}
</td>
<td style="border-right: 1px solid #ddd;">
{{ $row->exited_at ? \Carbon\Carbon::parse($row->exited_at)->format('H:i') : '-' }}
</td>
<td style="border-right: 1px solid #ddd;">
{{ $row->duration ? \Carbon\Carbon::parse($row->duration)->format('H:i') : '-' }}
</td>
<tr>
<td>{{ Jalalian::fromCarbon(Carbon::parse($row->entered_at))->format('m/d') ?? '-' }}</td>
<td>{{ Jalalian::fromCarbon(Carbon::parse($row->entered_at))->format('%A') ?? '-' }}</td>
<td>{{ Carbon::parse($row->entered_at)->format('H:i') ?? '-' }}</td>
<td>{{ $row->exited_at ? Carbon::parse($row->exited_at)->format('H:i') : '-' }}</td>
<td>{{ $row->duration ? Carbon::parse($row->duration)->format('H:i') : '-' }}</td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr>
<td colspan="5" class="bg-cyan-50 font-bold text-gray-700 rounded-b-xl" style="border-top: 2px solid #ddd; text-align: center; padding: 10px 0;">
<td colspan="5">
مجموع کارکرد این ماه: {{ $employeeTotals[$employee->id][$month] ?? '00:00' }}
</td>
</tr>
</tbody>
</tfoot>
</table>
</div>
@endforeach
@@ -132,58 +331,46 @@ foreach ($data as $employee) {
@endif
</div>
<style>
/* مخفی کردن اسکرول‌بار برای تب‌های ماه‌ها اگر تعدادشان زیاد شد */
.hide-scrollbar::-webkit-scrollbar { display: none; }
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
</style>
<script>
function openEmployee(evt, empContainerId, empId) {
let employeeContents = document.getElementsByClassName("employee-content");
for (let i = 0; i < employeeContents.length; i++) {
employeeContents[i].style.display = "none";
document.querySelectorAll('.employee-panel').forEach(el => el.classList.remove('is-visible'));
document.querySelectorAll('.emp-btn').forEach(btn => btn.classList.remove('emp-btn-active'));
const panel = document.getElementById(empContainerId);
if (panel) panel.classList.add('is-visible');
if (evt.currentTarget) {
evt.currentTarget.classList.add('emp-btn-active');
}
let empBtns = document.getElementsByClassName("emp-btn");
for (let i = 0; i < empBtns.length; i++) {
empBtns[i].classList.remove("bg-cyan-500", "text-white", "shadow-md");
empBtns[i].classList.add("bg-gray-200", "text-gray-700");
}
document.getElementById(empContainerId).style.display = "flex";
evt.currentTarget.classList.remove("bg-gray-200", "text-gray-700");
evt.currentTarget.classList.add("bg-cyan-500", "text-white", "shadow-md");
let firstMonthTab = document.querySelector("#" + empContainerId + " .tablinks-" + empId);
const firstMonthTab = document.querySelector('#' + empContainerId + ' .tablinks-' + empId);
if (firstMonthTab) {
firstMonthTab.click();
}
}
function openMonth(evt, monthId, empId) {
let tabcontents = document.getElementsByClassName("tabcontent-" + empId);
for (let i = 0; i < tabcontents.length; i++) {
tabcontents[i].style.display = "none";
}
document.querySelectorAll('.tabcontent-' + empId).forEach(el => {
el.style.display = 'none';
});
let tablinks = document.getElementsByClassName("tablinks-" + empId);
for (let i = 0; i < tablinks.length; i++) {
tablinks[i].classList.remove("bg-white", "border-cyan-500");
tablinks[i].classList.add("bg-gray-100", "border-transparent");
}
document.querySelectorAll('.tablinks-' + empId).forEach(btn => {
btn.classList.remove('month-tab-active');
});
document.getElementById(monthId).style.display = "block";
evt.currentTarget.classList.remove("bg-gray-100", "border-transparent");
evt.currentTarget.classList.add("bg-white", "border-cyan-500");
const monthPanel = document.getElementById(monthId);
if (monthPanel) monthPanel.style.display = 'block';
if (evt.currentTarget) {
evt.currentTarget.classList.add('month-tab-active');
}
}
document.addEventListener("DOMContentLoaded", function () {
let firstEmployeeBtn = document.querySelector(".emp-btn");
document.addEventListener('DOMContentLoaded', function () {
const firstEmployeeBtn = document.querySelector('.emp-btn');
if (firstEmployeeBtn) {
firstEmployeeBtn.click();
}
});
</script>
@endsection
@endsection
File diff suppressed because it is too large Load Diff
+50 -78
View File
@@ -37,17 +37,7 @@ if (!empty($user->tasks)) {
return array_search($task->state, $state_order);
});
}
//dd(count($break = $user->attendanceBreaks));
if (count($user->attendanceBreaks) > 0) {
$break = $user->attendanceBreaks[0];
if ($break->break_end == null) {
$now = Carbon::now()->format('Y-m-d H:i:s');
$attendances = new Attendance();
$duration = $attendances->duration($break->break_start, $now);
}
}
//dd($now, $break->break_start, $duration);
// $duration for break timer is passed from AttendanceController
?>
@extends('app')
@@ -123,7 +113,7 @@ if (count($user->attendanceBreaks) > 0) {
@else
<div class="bg-white flex justify-flex p-4 rounded-xl max-w-[350px] flex-col items-center">
<div class="ui-panel flex p-4 rounded-xl max-w-[350px] flex-col items-center">
<label class="text-xl font-bold pb-2">
@@ -169,33 +159,27 @@ if (count($user->attendanceBreaks) > 0) {
</div>
@else
{{-- دکمه‌های تب (نام ماه‌ها) --}}
<div class="tab rounded-t-2xl max-w-[350px] mx-auto flex overflow-x-auto"
style="margin-top: 0px; max-width: 350px">
<div class="ui-month-tabs">
@foreach($groupedData->keys() as $month)
<button class="tablinks rounded-t-2xl cursor-pointer px-4 py-2 hover:bg-gray-200 transition flex-shrink-0"
<button type="button" class="tablinks"
onclick="openCity(event, '{{$month}}')">
{{$month}}
</button>
@endforeach
</div>
{{-- محتوای تب‌ها (جدول‌ها) --}}
@foreach($groupedData as $month => $records)
<div id="{{$month}}"
class="tabcontent w-[350px] bg-white rounded-b-xl rounded-tl-2xl cursor-default leading-7 shadow-md">
<table class="table-auto"
style="width:100%; border-collapse: collapse; text-align: center">
<div id="{{$month}}" class="tabcontent ui-attendance-card">
<table class="ui-attendance-table">
<thead>
<tr style="border-bottom: 1px solid #ddd; background: rgba(244,244,244,0.82);"
class="border-1 border-slate-300">
<tr>
@if($_COOKIE['user_identity'] == 1370)
<th style="border-right: 1px solid #ddd; padding: 8px;">نام</th>
<th>نام</th>
@endif
<th style="border-right: 1px solid #ddd; padding: 4px;">تاریخ</th>
<th style="border-right: 1px solid #ddd; padding: 4px;">ورود</th>
<th style="border-right: 1px solid #ddd; padding: 4px;">خروج</th>
<th style="border-right: 1px solid #ddd; padding: 4px;">مدت</th>
<th>تاریخ</th>
<th>ورود</th>
<th>خروج</th>
<th>مدت</th>
</tr>
</thead>
@@ -240,8 +224,7 @@ if (count($user->attendanceBreaks) > 0) {
{{-- ردیف جمع کل (اختیاری - چون متغیر آن موجود نبود کامنت کردم که خطا ندهد) --}}
<tr>
<td colspan="5" class="rounded-b-xl bg-gray-50 font-bold"
style="border-top: 1px solid #ddd; text-align: center; padding: 10px;">
<td colspan="5" class="table-summary">
تعداد رکورد: {{ count($records) }}
</td>
</tr>
@@ -265,10 +248,10 @@ if (count($user->attendanceBreaks) > 0) {
<!-- ATTENDANCE Modal -->
<div id="modal-container"
class="fixed inset-0 z-50 hidden bg-black bg-opacity-60 flex items-center justify-center p-4">
<div id="modal-content" class=" bg-white p-4 sm:p-6 rounded-lg shadow-lg max-w-sm w-full relative">
<h2 id="modal-title" class="text-xl sm:text-2xl font-semibold mb-4 text-center"></h2>
<p id="modal-body" class="text-gray-700 mb-4 text-center"></p>
class="ui-modal-backdrop centered hidden">
<div id="modal-content" class="ui-modal-panel max-w-sm w-full">
<h2 id="modal-title" class="ui-title"></h2>
<p id="modal-body" class="ui-subtitle"></p>
<div class="flex flex-col gap-4" style="align-items: center">
<div class="flex justify-center gap-2">
@@ -376,23 +359,18 @@ if (count($user->attendanceBreaks) > 0) {
{{-- START TASK MODAL --}}
<div id="taskModal"
class="hidden fixed inset-0 bg-black/40 items-start justify-center z-50 flex backdrop-blur-sm scroll-auto pt-10"
class="ui-modal-backdrop hidden"
onclick="closeOnOutsideClick(event)">
<div class="relative bg-white w-[350px] rounded-2xl shadow-lg p-5 text-center">
<button onclick="closeTaskModal()"
class="absolute top-3 right-3 text-gray-400 hover:text-gray-600 transition">
<div class="ui-modal-panel">
<button type="button" onclick="closeTaskModal()" class="ui-modal-close" aria-label="بستن">
<i class="fa-solid fa-xmark"></i>
</button>
<h2 class="text-lg font-bold mb-3" id="modalTitle">انتخاب وظیفه</h2>
<p class="text-gray-600 mb-4" id="modalDesc">یکی از وظایف زیر را انتخاب کنید:</p>
<input class="h-12 w-full bg-transparent pl-11 pr-4 text-sm text-custom-text-100
placeholder:text-custom-text-400 focus:ring-0 rounded-3xl border-2 border-gray-300 py-2"
placeholder="جست و جو..." tabindex="1" id="headlessui-combobox-input-:r13m:" role="combobox"
aria-expanded="false" aria-autocomplete="list" data-headlessui-state="" type="text" value=""
aria-controls="headlessui-combobox-options-:r13n:"
aria-activedescendant="headlessui-combobox-option-:r13s:" data-sharkid="__4">
<h2 class="ui-title" id="modalTitle">انتخاب وظیفه</h2>
<p class="ui-subtitle" id="modalDesc">یکی از وظایف زیر را انتخاب کنید:</p>
<input class="ui-input mb-3"
placeholder="جست و جو..." type="text" value=""
id="taskSearchInput">
{{-- <div class="grid flex-col gap-2 grid-cols-2 pb-3 items-center justify-items-end">--}}
@if(isset($user->tasks) && count($user->tasks) >= 1)
@@ -400,11 +378,9 @@ if (count($user->attendanceBreaks) > 0) {
<form action="{{url("active_task")}}" method="POST" class=" ">@csrf
{{-- <input type="hidden" name="pid" value="{{ $user->tasks->pivot->id }}" class="">--}}
<input type="hidden" name="uid" value="{{ $user->id }}" class="">
<div class=" overflow-y-scroll"
style="max-height: calc(100vh - 300px);">
<div class="ui-task-list">
@foreach($user->tasks as $item)
<div class="inline-flex items-center bg-gray-300 hover:bg-cyan-600 rounded-lg m-2 p-2 justify-between"
style="display: flex">
<label class="ui-task-item">
@if($user->active_task)
@if( $user->active_task == $item->title )
<input type="radio" name="task_id" checked="true"
@@ -425,8 +401,7 @@ if (count($user->attendanceBreaks) > 0) {
<div class="px-2.5" style="direction: rtl">{{ $item->title }}</div>
<span class="block h-1.5 w-1.5 flex-shrink-0 rounded-full" style="background-color:
{!! array_key_exists($item->state,$state) ? $state[$item->state] : "" !!};"></span>
</div>
</label>
@endforeach
</div>
@@ -446,21 +421,17 @@ if (count($user->attendanceBreaks) > 0) {
</form>
@else
<div class="inline-flex justify-center items-center"
style="height: calc(100vh - 400px);">
<div class="flex justify-center items-center py-10">
<p class="font-bold text-gray-400">
وظیفه ای وجود ندارد
</p>
</div>
@endif
{{-- افزودن تسک--}}
<form>
<input class="h-12 w-[200px] bg-transparent pl-11 pr-4 text-sm text-custom-text-100
placeholder:text-custom-text-400 focus:ring-0 rounded-3xl border-2 border-gray-300 py-2"
placeholder="عنوان وظیفه" tabindex="1" id="taskTitle" role="combobox"
aria-expanded="false" aria-autocomplete="list" data-headlessui-state="" type="text" value=""
aria-controls="headlessui-combobox-options-:r13n:"
aria-activedescendant="headlessui-combobox-option-:r13s:" data-sharkid="__4">
<form class="mt-3 flex items-center justify-center gap-2 flex-wrap">
<input class="ui-input !w-[200px]"
placeholder="عنوان وظیفه" type="text" value=""
id="taskTitle">
<button id="createTask"
class="mt-4 inline-block bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-2.5 fa-notdog fa-solid fa-plus rounded-full">
</button>
@@ -468,10 +439,13 @@ if (count($user->attendanceBreaks) > 0) {
</form>
</div>
</div>
@endsection
</div>
@section('script')
@endsection
@section('script')
<script>
const existed = {{ $existed ? 'true' : 'false' }};
@@ -735,23 +709,21 @@ if (count($user->attendanceBreaks) > 0) {
}
//SEARCH BAR IN TASK MODAL
const searchInput = document.getElementById("headlessui-combobox-input-:r13m:");
const listItems = document.querySelectorAll('#taskModal .inline-flex.items-center');
const searchInput = document.getElementById("taskSearchInput");
const listItems = document.querySelectorAll('#taskModal .ui-task-item');
searchInput.addEventListener("input", function () {
const filter = searchInput.value.toLowerCase();
listItems.forEach(item => {
const nameDiv = item.querySelector('div');
if (nameDiv) {
const name = nameDiv.textContent.toLowerCase();
if (name.includes(filter)) {
item.style.display = 'flex';
} else {
item.style.display = 'none';
if (searchInput && listItems.length) {
searchInput.addEventListener("input", function () {
const filter = searchInput.value.toLowerCase();
listItems.forEach(item => {
const nameDiv = item.querySelector('div');
if (nameDiv) {
const name = nameDiv.textContent.toLowerCase();
item.style.display = name.includes(filter) ? 'flex' : 'none';
}
}
});
});
});
}
const taskTitle = document.getElementById('taskTitle');
+3 -4
View File
@@ -2,7 +2,8 @@
@section('content')
{{-- {{dd($user)}}--}}
<div class="w-full mt-[90px] justify-center container flex">
<div class="bg-white w-[350px] p-6 rounded-xl ">
<div class="ui-card">
<div class="ui-card-body">
<img src="/assets/img/bg/bg-shape.jpg" class="rounded-full w-32 h-32 mx-auto pb-6" alt="">
{{-- <p class=""> تاریخ شروع همکاری:--}}
{{-- {{\Morilog\Jalali\Jalalian::fromCarbon(\Carbon\Carbon::parse($user->created_at))->format('Y/m/d')}}</p>--}}
@@ -144,9 +145,7 @@
}
</script>
</div>
</div>
</div>
+50 -66
View File
@@ -3,14 +3,12 @@
use Morilog\Jalali\Jalalian;
use Carbon\Carbon;
//dd($data);
$monthlyTotals = [];
foreach ($data as $month => $attendances) {
$tsec = 0;
foreach ($attendances as $attendance) {
// اگر duration موجود باشه استفاده می‌کنیم
if (!empty($attendance->duration)) {
[$h, $m, $s] = explode(':', $attendance->duration);
$tsec += (int)$h * 3600 + (int)$m * 60 + (int)$s;
@@ -19,88 +17,74 @@ foreach ($data as $month => $attendances) {
$hours = floor($tsec / 3600);
$minutes = floor(($tsec % 3600) / 60);
$seconds = $tsec % 60;
// ذخیره در آرایه جدید با کل زمان ماه
$monthlyTotals[$month] = sprintf('%02d:%02d', $hours, $minutes);
}
//dd($monthlyTotals);
?>
@extends('app')
@section('content')
<div class="container inline-flex flex-col items-center justify-center ">
@section('content')
<div class="ui-page">
@if(empty($data))
<div class="mt-80 text-gray-500 text-xl font-bold italic">
<div class="ui-empty">
<h1>اطلاعاتی موجود نمیباشد</h1>
</div>
@else
<div class="tab rounded-t-2xl max-w-[350px] mx-auto flex"
style="margin-top: 80px; max-width: 350px">
@foreach(array_keys($data) as $month)
<button class="tablinks active rounded-t-2xl cursor-pointer" onclick="openCity(event, '{{$month}}')">{{$month}}</button>
@endforeach
</div>
@foreach($data as $month => $records)
<div id="{{$month}}" class="tabcontent w-[350px] bg-white rounded-b-xl rounded-tl-2xl cursor-default leading-7" style="">
<table class="" style="width:100%; border-collapse: collapse;text-align: center">
<thead>
<tr class="" style="border-bottom: 1px solid #ddd;">
<th style="border-right: 1px solid #ddd;">تاریخ</th>
<th style="border-right: 1px solid #ddd;">روز</th>
<th style="border-right: 1px solid #ddd;">ورود</th>
<th style="border-right: 1px solid #ddd;">خروج</th>
<th style="border-right: 1px solid #ddd;">مدت</th>
</tr>
</thead>
<tbody >
@foreach($records as $row)
<tr class="hover:bg-cyan-500" style="border-bottom: 1px solid #ddd;">
<td style="border-right: 1px solid #ddd;">{{ Jalalian::fromCarbon(\Carbon\Carbon::parse($row->entered_at))->format('m/d') ?? '-' }}</td>
<td style="border-right: 1px solid #ddd;">{{ Jalalian::fromCarbon(\Carbon\Carbon::parse($row->entered_at))->format('%A') ?? '-' }}</td>
<td style="border-right: 1px solid #ddd;">{{ \Carbon\Carbon::parse($row->entered_at)->format('H:i') ?? '-' }}</td>
<td style="border-right: 1px solid #ddd;">{{ $row->exited_at ? \Carbon\Carbon::parse($row->exited_at)->format('H:i') : '-' }}</td>
<td style="border-right: 1px solid #ddd;">{{ $row->exited_at ? \Carbon\Carbon::parse($row->duration)->format('H:i') : '-' }}</td>
</tr>
@endforeach
<tr>
<td colspan="5" class="hover:bg-cyan-500 rounded-b-xl" style="border-top: 1px solid #ddd; text-align: center; ">
مجموع کارکرد این ماه: {{$monthlyTotals[$month]}}
</td>
</tr>
</tbody>
</table>
<div class="ui-month-tabs" style="margin-top: 5rem;">
@foreach(array_keys($data) as $month)
<button type="button" class="tablinks" onclick="openCity(event, '{{ $month }}')">{{ $month }}</button>
@endforeach
</div>
@endforeach
@foreach($data as $month => $records)
<div id="{{ $month }}" class="tabcontent ui-attendance-card">
<table class="ui-attendance-table">
<thead>
<tr>
<th>تاریخ</th>
<th>روز</th>
<th>ورود</th>
<th>خروج</th>
<th>مدت</th>
</tr>
</thead>
<tbody>
@foreach($records as $row)
<tr>
<td>{{ Jalalian::fromCarbon(Carbon::parse($row->entered_at))->format('m/d') ?? '-' }}</td>
<td>{{ Jalalian::fromCarbon(Carbon::parse($row->entered_at))->format('%A') ?? '-' }}</td>
<td>{{ Carbon::parse($row->entered_at)->format('H:i') ?? '-' }}</td>
<td>{{ $row->exited_at ? Carbon::parse($row->exited_at)->format('H:i') : '-' }}</td>
<td>{{ $row->exited_at ? Carbon::parse($row->duration)->format('H:i') : '-' }}</td>
</tr>
@endforeach
<tr>
<td colspan="5" class="table-summary">
مجموع کارکرد این ماه: {{ $monthlyTotals[$month] }}
</td>
</tr>
</tbody>
</table>
</div>
@endforeach
@endif
</div>
<script>
function openCity(evt, Month) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(Month).style.display = "block";
evt.currentTarget.className += " active";
document.querySelectorAll('.tabcontent').forEach(el => el.style.display = 'none');
document.querySelectorAll('.tablinks').forEach(el => el.classList.remove('active'));
const panel = document.getElementById(Month);
if (panel) panel.style.display = 'block';
if (evt.currentTarget) evt.currentTarget.classList.add('active');
}
// باز کردن اولین تب به صورت پیش‌فرض
document.addEventListener("DOMContentLoaded", function () {
let firstTab = document.querySelector(".tablinks"); // اولین دکمه
if (firstTab) {
firstTab.click(); // همون اول کلیک مجازی بخوره
}
document.addEventListener('DOMContentLoaded', function () {
const firstTab = document.querySelector('.tablinks');
if (firstTab) firstTab.click();
});
</script>
@endsection
+6 -7
View File
@@ -19,11 +19,10 @@ if (isset($task)) {
?>
@extends('app')
@section('content')
<div class="w-full mt-[70px] justify-center container flex ">
<div class="bg-white w-[350px] rounded-xl ">
<h1 class="w-[350px] bg-cyan-500 rounded-t-xl text-center p-1.5 font-bold text-slate-50">{{isset($task) ? "ویرایش وظیفه" : "ایجاد وظیفه جدید"}}
</h1>
<form action="{{isset($task) ? Route('task.edit') : Route('task.create')}}" method="POST" class=" p-6 grid-cols-2 gap-4 items-center">
<div class="w-full mt-[70px] justify-center container flex">
<div class="ui-card">
<div class="ui-card-header">{{isset($task) ? "ویرایش وظیفه" : "ایجاد وظیفه جدید"}}</div>
<form action="{{isset($task) ? Route('task.edit') : Route('task.create')}}" method="POST" class="ui-card-body grid grid-cols-2 gap-4 items-center">
@csrf
@if(isset($task))
<input type="hidden" id="id" name="id" value="{{$task->id}}">
@@ -87,7 +86,7 @@ if (isset($task)) {
<div class="relative" id="dropdown-container">
{{-- دکمه --}}
<button type="button" id="dropdown-btn"
class="w-full bg-white border border-gray-300 rounded-lg px-4 py-2 text-right flex justify-between items-center">
class="ui-input text-right flex justify-between items-center">
<span id="selected-count">انتخاب کاربران</span>
<svg class="w-4 h-4 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -96,7 +95,7 @@ if (isset($task)) {
</button>
<div id="dropdown-list"
class="hidden absolute z-20 w-full bg-white border border-gray-300 rounded-lg shadow-lg">
class="hidden absolute z-20 w-full ui-panel border border-gray-300 rounded-lg shadow-lg">
<div class="p-2 border-b">
<input type="text" id="search-input"
+499 -160
View File
@@ -64,22 +64,341 @@ $state = [
?>
@extends('app')
@section('content')
<div class="container w-full flex justify-center items-center flex-col ">
{{-- {{dd(count($tasks))}}--}}
@section('style')
<style>
.tasks-page {
width: 100%;
padding: 0 0.5rem 0;
}
.tasks-card {
background: var(--surface-glass);
backdrop-filter: blur(16px) saturate(160%);
-webkit-backdrop-filter: blur(16px) saturate(160%);
border: 1px solid var(--border-glass);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-soft);
overflow: hidden;
}
.tasks-filter {
margin-bottom: 0.75rem;
padding: 0.875rem 1rem;
background: var(--surface-glass);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border: 1px solid var(--border-glass);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-soft);
}
.tasks-filter select {
border-radius: 0.875rem;
border: 1px solid var(--border-input);
background: var(--surface-input);
color: var(--text-primary);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.tasks-filter select:focus {
outline: none;
border-color: color-mix(in srgb, var(--primary-color) 55%, white);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 22%, transparent);
}
.tasks-tabs {
display: flex;
flex-wrap: nowrap;
gap: 0.35rem;
padding: 0.5rem;
background: var(--surface-tabs);
border-bottom: 1px solid var(--border-input);
}
.task-tab {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.25rem;
padding: 0.55rem 0.4rem;
border-radius: 0.875rem;
font-size: 0.8rem;
font-weight: 600;
color: var(--text-muted);
background: transparent;
border: none;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
}
.task-tab:hover {
color: var(--text-secondary);
background: var(--surface-tab-hover);
}
.task-tab-active {
color: color-mix(in srgb, var(--primary-color) 82%, var(--text-primary));
background: var(--surface-tab-active);
box-shadow: var(--shadow-soft);
}
.task-tab-label {
width: 100%;
font-size: 0.72rem;
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.task-tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.35rem;
padding: 0.12rem 0.45rem;
border-radius: 9999px;
font-size: 0.68rem;
font-weight: 700;
line-height: 1;
background: var(--badge-bg);
color: var(--badge-text);
}
.task-tab-active .task-tab-count {
background: color-mix(in srgb, var(--primary-color) 18%, var(--surface-tab-active));
color: color-mix(in srgb, var(--primary-color) 70%, var(--text-primary));
}
.tasks-table-wrap {
padding: 0.5rem 0.5rem 0.75rem;
}
.tasks-cards-list {
display: flex;
flex-direction: column;
gap: 0.55rem;
}
.task-card {
display: block;
width: 100%;
padding: 0.85rem 0.9rem;
background: var(--surface-card);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
color: var(--text-primary);
cursor: pointer;
text-align: right;
transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
}
.task-card:hover {
background: var(--surface-card-hover);
box-shadow: var(--row-shadow);
}
.task-card.is-active {
border-inline-start: 4px solid var(--primary-color);
padding-inline-start: calc(0.9rem - 3px);
}
.task-card-title-row {
display: flex;
align-items: flex-start;
gap: 0.5rem;
margin-bottom: 0.35rem;
}
.task-card-icon {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
margin-top: 0.1rem;
}
.task-card-title {
margin: 0;
font-size: 0.92rem;
font-weight: 800;
line-height: 1.45;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-card-desc {
margin: 0 0 0.65rem;
padding-inline-start: 1.75rem;
font-size: 0.78rem;
line-height: 1.55;
color: var(--text-muted);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.task-card-fields {
margin: 0;
padding: 0.55rem 0 0;
border-top: 1px solid var(--border-subtle);
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.task-card-field {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
font-size: 0.78rem;
}
.task-card-label {
flex-shrink: 0;
color: var(--text-muted);
font-weight: 600;
}
.task-card-value {
color: var(--text-secondary);
text-align: left;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-card-value.font-mono {
font-variant-numeric: tabular-nums;
}
.tasks-empty {
padding: 2rem 1rem;
text-align: center;
color: var(--text-faint);
font-size: 0.9rem;
}
#taskDetailsModal {
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
#taskDetailsModal > div {
background: var(--modal-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--modal-border);
box-shadow: var(--modal-shadow);
color: var(--text-primary);
}
#taskDetailsModal .modal-tab-btn-active {
background: color-mix(in srgb, var(--primary-color) 18%, var(--surface-tab-active)) !important;
color: color-mix(in srgb, var(--primary-color) 82%, var(--text-primary)) !important;
}
[data-theme="dark"] #taskDetailsModal {
background-color: rgba(2, 6, 23, 0.72) !important;
}
[data-theme="dark"] .task-card svg [stroke="#60646C"] {
stroke: #c5d0e0 !important;
}
</style>
@endsection
@section('content')
<script>
window.tasksById = @json($tasksPayload ?? []);
function switchTaskTab(tabId) {
document.querySelectorAll('.task-table-container').forEach(el => el.classList.add('hidden'));
const panel = document.getElementById('tab-' + tabId);
if (panel) panel.classList.remove('hidden');
document.querySelectorAll('.task-tab').forEach(tab => tab.classList.remove('task-tab-active'));
const activeTab = document.querySelector('.task-tab[data-tab="' + tabId + '"]');
if (activeTab) activeTab.classList.add('task-tab-active');
}
function updateTaskTabCounts() {
document.querySelectorAll('.task-tab').forEach(tab => {
const tabId = tab.dataset.tab;
const panel = document.getElementById('tab-' + tabId);
if (!panel) return;
const visibleCount = panel.querySelectorAll('.task-row:not(.hidden)').length;
const badge = tab.querySelector('.task-tab-count');
if (badge) badge.textContent = visibleCount;
});
}
function filterTasksByAssignee(assigneeId) {
document.querySelectorAll('.task-row').forEach(row => {
if (!assigneeId) {
row.classList.remove('hidden');
return;
}
let ids = [];
try {
ids = JSON.parse(row.dataset.assigneeIds || '[]');
} catch (e) {
ids = [];
}
row.classList.toggle('hidden', !ids.includes(String(assigneeId)));
});
document.querySelectorAll('.task-table-container').forEach(panel => {
const emptyMsg = panel.querySelector('.task-filter-empty');
const visibleRows = panel.querySelectorAll('.task-row:not(.hidden)').length;
const hasRows = panel.querySelectorAll('.task-row').length > 0;
if (emptyMsg) {
emptyMsg.classList.toggle('hidden', !assigneeId || !hasRows || visibleRows > 0);
}
});
updateTaskTabCounts();
}
</script>
<div class="container tasks-page w-full flex justify-center items-center flex-col">
@if($count > 0)
<div class="w-full max-w-6xl mx-auto mt-10 px-2">
<div class="flex flex-nowrap gap-1 border-b border-gray-200">
<div class="w-full max-w-6xl mx-auto mt-4">
@if(!empty($assigneeFilterOptions))
<div class="tasks-filter flex flex-wrap items-center gap-3">
<label for="assigneeFilter" class="text-sm font-medium text-gray-700 whitespace-nowrap">
فیلتر بر اساس مسئول:
</label>
<select id="assigneeFilter"
onchange="filterTasksByAssignee(this.value)"
class="flex-1 min-w-[180px] px-3 py-2 text-sm text-gray-800">
<option value="">همه مسئولین</option>
@foreach($assigneeFilterOptions as $option)
<option value="{{ $option['id'] }}">{{ $option['name'] }}</option>
@endforeach
</select>
</div>
@endif
<div class="tasks-card mt-3">
<div class="tasks-tabs">
@foreach(array_keys($tasks) as $stateKey)
<button onclick="switchTaskTab('{{ $stateKey }}')"
class="task-tab px-4 py-2 rounded-t-lg font-medium text-sm transition
{{ $loop->first ? 'bg-white text-blue-600 border-t border-x border-gray-200' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100' }}"
<button type="button" onclick="switchTaskTab('{{ $stateKey }}')"
class="task-tab {{ $loop->first ? 'task-tab-active' : '' }}"
data-tab="{{ $stateKey }}">
{{ $stateKey }}
<span class="mr-1 text-xs bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full">
{{ count($tasks[$stateKey] ?? []) }}
</span>
<span class="task-tab-label">{{ $stateKey }}</span>
<span class="task-tab-count">{{ count($tasks[$stateKey] ?? []) }}</span>
</button>
@endforeach
</div>
@@ -87,142 +406,90 @@ $state = [
{{-- جداول هر وضعیت --}}
@foreach($tasks as $stateKey => $stateTasks)
<div id="tab-{{ $stateKey }}"
class="task-table-container {{ $loop->first ? '' : 'hidden' }} overflow-x-auto bg-white rounded-b-lg shadow">
class="task-table-container tasks-table-wrap {{ $loop->first ? '' : 'hidden' }}">
@if(count($stateTasks) > 0)
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">عنوان
</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">
اولویت
</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">{{ $isAdminLevel1 ? 'نام اپ' : 'تاریخ شروع' }}
</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">مهلت
</th>
{{-- <th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">جزئیات</th>--}}
{{-- <th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">مدت زمان</th>--}}
{{-- <th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">عملیات</th>--}}
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<div class="tasks-cards-list">
@foreach($stateTasks as $task)
@php
// دریافت نام ایجادکننده (با فرض وجود رابطه 'creator' در مدل Tasks)
$creatorName = 'نامشخص';
if ($task->relationLoaded('creator') && $task->creator) {
$creatorName = $task->creator->name;
} elseif ($task->created_by) {
// اگر رابطه لود نشده، یک بار کاربر را پیدا کن (توصیه می‌شود در کنترلر eager load شود)
$user = \App\Models\User::find($task->created_by);
$creatorName = $user ? $user->name : $task->created_by;
}
$task->creator_name = $creatorName;
// آیکون وضعیت
$stateIcon = '';
switch($task->state){
case 1: $stateIcon = $backlog; break;
case 2: $stateIcon = $todo; break;
case 3: $stateIcon = $in_progress; break;
case 4: $stateIcon = $done; break;
default: $stateIcon = '';
}
// تعیین وضعیت شروع/پایان برای دکمه
$started_at = $lastDuration?->start ?? null;
$ended_at = $lastDuration?->end ?? null;
$actionType = $ended_at ? 'start' : ($started_at ? 'end' : 'start');
$p_id = $lastDuration?->id ?? '';
$stateIcon = match((int)$task->state) {
1 => $backlog,
2 => $todo,
3 => $in_progress,
4 => $done,
default => '',
};
@endphp
{{--@if($_COOKIE['user_identity'] == 'naser_me') {{dump($task)}} @endif--}}
<tr class="hover:bg-gray-50">
<td class="px-2 py-2 {{ $task->active ? 'border-s-4 border-cyan-400 ' : '' }}">
<div class="flex items-center gap-2">
<span class="inline-flex items-center justify-center w-5 h-5">{!! $stateIcon !!}</span>
<div>
<div class="font-small text-gray-900 text-sm font-bold">{{ $task->title }}</div>
@if($task->description)
<div class="text-xs text-gray-500 mt-1 line-clamp-2">{{ \Illuminate\Support\Str::limit(strip_tags($task->description), 50) }}</div>
@endif
</div>
</div>
</td>
<td class="px-2 py-2">
<span class="px-2 py-1 text-xs rounded-full {{ $priorityColors[$task->priority] ?? 'bg-gray-100 text-gray-800' }}">
{{ $priorityLabels[$task->priority] ?? $task->priority }}
</span>
</td>
<td class="px-2 py-2 text-sm text-gray-600">
{{ $isAdminLevel1 ? $task->appName->name_fa ?? null : Jalalian::fromCarbon(Carbon::parse($task->start_date))->format('Y/m/d') }}
</td>
<td class="px-2 py-2 text-sm text-gray-600">
{{ $task->target_date ? Jalalian::fromCarbon(Carbon::parse($task->target_date))->format('Y/m/d') : '-' }}
</td>
{{-- <td class="px-2 py-2 text-sm text-gray-600 font-mono">--}}
{{-- {{ $totalDuration }}--}}
{{-- </td>--}}
{{-- <td class="px-2 py-2">--}}
{{-- <button onclick="openTaskModal(--}}
{{-- '{{ $task->id }}',--}}
{{-- '{{ $p_id }}',--}}
{{-- '{{ $started_at }}',--}}
{{-- '{{ $ended_at }}',--}}
{{-- '{{ $actionType }}'--}}
{{-- )" class="text-sm bg-blue-50 text-blue-700 px-3 py-1.5 rounded-lg hover:bg-blue-100 transition flex items-center gap-1">--}}
{{-- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">--}}
{{-- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>--}}
{{-- </svg>--}}
{{-- {{ $actionType == 'start' ? 'شروع' : 'پایان' }}--}}
{{-- </button>--}}
{{-- </td>--}}
{{-- ستون جزئیات --}}
<td class="px-2 py-2">
<button onclick='openTaskDetailsModal(@json($task))'
class="text-sm bg-gray-50 text-gray-700 px-3 py-1.5 rounded-lg hover:bg-gray-100 transition flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
</button>
</td>
<article class="task-card task-row {{ $task->active ? 'is-active' : '' }}"
data-task-id="{{ $task->id }}"
data-assignee-ids='@json($task->assignee_ids ?? [])'
onclick="openTaskDetailsModal({{ $task->id }})"
role="button"
tabindex="0"
onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();openTaskDetailsModal({{ $task->id }})}">
<div class="task-card-title-row">
<span class="task-card-icon">{!! $stateIcon !!}</span>
<h3 class="task-card-title">{{ $task->title }}</h3>
</div>
{{-- ستون زمان (همان دکمه شروع/پایان قبلی) --}}
{{-- <td class="px-2 py-2">--}}
{{-- <button onclick="openTaskTimeModal(--}}
{{-- '{{ $task->id }}',--}}
{{-- '{{ $p_id }}',--}}
{{-- '{{ $started_at }}',--}}
{{-- '{{ $ended_at }}',--}}
{{-- '{{ $actionType }}'--}}
{{-- )" class="text-sm bg-blue-50 text-blue-700 px-3 py-1.5 rounded-lg hover:bg-blue-100 transition flex items-center gap-1">--}}
{{-- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">--}}
{{-- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>--}}
{{-- </svg>--}}
{{-- {{ $actionType == 'start' ? 'شروع' : 'پایان' }}--}}
{{-- </button>--}}
{{-- </td>--}}
</tr>
@if($task->description)
<p class="task-card-desc">{{ strip_tags($task->description) }}</p>
@endif
<dl class="task-card-fields">
<div class="task-card-field">
<dt class="task-card-label">اولویت</dt>
<dd class="task-card-value">
<span class="px-2 py-0.5 text-xs rounded-full {{ $priorityColors[$task->priority] ?? 'bg-gray-100 text-gray-800' }}">
{{ $priorityLabels[$task->priority] ?? $task->priority }}
</span>
</dd>
</div>
@if($isAdminLevel1)
<div class="task-card-field">
<dt class="task-card-label">نام اپ</dt>
<dd class="task-card-value">{{ $task->appName->name_fa ?? '-' }}</dd>
</div>
@endif
@if($stateKey === 'done')
<div class="task-card-field">
<dt class="task-card-label">زمان اجرا</dt>
<dd class="task-card-value font-mono">{{ $task->total_duration ?? '00:00:00' }}</dd>
</div>
@else
<div class="task-card-field">
<dt class="task-card-label">مهلت</dt>
<dd class="task-card-value">{{ $task->target_date ? Jalalian::fromCarbon(Carbon::parse($task->target_date))->format('Y/m/d') : '-' }}</dd>
</div>
@endif
@if(!empty($task->assignee_names))
<div class="task-card-field">
<dt class="task-card-label">مسئولین</dt>
<dd class="task-card-value">{{ implode('، ', $task->assignee_names) }}</dd>
</div>
@endif
</dl>
</article>
@endforeach
</tbody>
</table>
</div>
<div class="task-filter-empty hidden tasks-empty">
تسکی برای این مسئول در این وضعیت وجود ندارد.
</div>
@else
<div class="p-8 text-center text-gray-400 bg-white rounded-b-lg">
<div class="tasks-empty">
هیچ وظیفه‌ای در این وضعیت وجود ندارد.
</div>
@endif
</div>
@endforeach
</div>
</div>
@else
<div class=" mt-[200px]">
<h1 class=" text-xl font-bold" style="color: #959595">وظیفه ای ندارید</h1>
<h1 class="text-xl font-bold text-gray-500">وظیفه ای ندارید</h1>
</div>
@endif
</div>
@@ -230,12 +497,13 @@ $state = [
{{-- START TASK DETAILS MODAL --}}
<div id="taskDetailsModal" class="fixed inset-0 bg-black/40 hidden items-center justify-center z-50 p-4">
<div class="bg-white w-full max-w-md rounded-2xl shadow-xl max-h-[90vh] overflow-y-auto">
<div id="taskDetailsModal" class="fixed inset-0 bg-slate-900/35 hidden items-center justify-center z-50 p-4">
<div class="w-full max-w-md rounded-2xl max-h-[90vh] overflow-y-auto">
{{-- هدر مودال --}}
<div class="flex justify-between items-center p-4 border-b border-gray-200">
<div class="flex justify-between items-center p-4 border-b border-slate-200/70">
<h2 class="text-lg font-bold text-gray-800" id="modalTaskTitle">عنوان تسک</h2>
<button onclick="closeDetailsModal()" class="text-gray-400 hover:text-gray-600">
<button onclick="closeDetailsModal()"
class="w-9 h-9 inline-flex items-center justify-center rounded-full text-gray-400 hover:text-gray-600 hover:bg-slate-100 transition">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
@@ -245,7 +513,7 @@ $state = [
<div class="p-4 space-y-4">
{{-- Tabs --}}
<div class="flex gap-2 bg-white sticky top-0 z-10">
<div class="flex gap-2 sticky top-0 z-10" style="background: var(--modal-bg);">
<button id="modalTabBtn-info" type="button"
onclick="switchDetailsTab('info')"
class="flex-1 px-3 py-2 text-sm rounded-lg bg-gray-100 text-gray-800">
@@ -324,6 +592,14 @@ $state = [
<h3 class="text-sm font-medium text-gray-500 mb-1">ایجادکننده</h3>
<p class="text-gray-800 text-sm" id="modalTaskCreator">-</p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-500 mb-1">تاریخ ایجاد</h3>
<p class="text-gray-800 text-sm" id="modalTaskCreatedAt">-</p>
</div>
<div class="col-span-2">
<h3 class="text-sm font-medium text-gray-500 mb-1">مسئولین</h3>
<p class="text-gray-800 text-sm" id="modalTaskAssignees">-</p>
</div>
</div>
<div class="flex gap-2 pt-2">
@@ -389,7 +665,7 @@ $state = [
placeholder="مثلاً: @ali لطفاً این بخش رو بررسی کن" required></textarea>
<div id="mentionDropdown"
class="hidden absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg max-h-48 overflow-y-auto">
class="hidden absolute z-50 w-full mt-1 ui-panel border border-gray-200 rounded-lg shadow-lg max-h-48 overflow-y-auto">
</div>
</div>
@@ -409,6 +685,7 @@ $state = [
<script>
const mentionUsers = @json($mentionUsers);
const appsList = @json($appsList);
const tasksById = window.tasksById || {};
const isAdminLevel1 = @json($isAdminLevel1);
const currentUserId = @json($user_id ?? null);
let currentTask = {};
@@ -493,21 +770,61 @@ $state = [
document.getElementById(elementId).classList.toggle('hidden');
}
// تابع سوییچ تب‌های جدول
function switchTaskTab(tabId) {
// مخفی کردن همه جداول
document.querySelectorAll('.task-table-container').forEach(el => el.classList.add('hidden'));
// نمایش جدول انتخاب شده
document.getElementById('tab-' + tabId).classList.remove('hidden');
function setTaskDetailsLoading() {
const commentsList = document.getElementById('modalCommentsList');
const durationsList = document.getElementById('modalDurationsList');
const loadingHtml = '<div class="text-sm text-gray-400 text-center">در حال بارگذاری...</div>';
if (commentsList) commentsList.innerHTML = loadingHtml;
if (durationsList) durationsList.innerHTML = loadingHtml;
}
// تغییر استایل تب‌های فعال
document.querySelectorAll('.task-tab').forEach(tab => {
tab.classList.remove('bg-white', 'text-blue-600', 'border-t', 'border-x', 'border-gray-200');
tab.classList.add('text-gray-500', 'hover:text-gray-700', 'hover:bg-gray-100');
});
const activeTab = document.querySelector(`.task-tab[data-tab="${tabId}"]`);
activeTab.classList.add('bg-white', 'text-blue-600', 'border-t', 'border-x', 'border-gray-200');
activeTab.classList.remove('text-gray-500', 'hover:text-gray-700', 'hover:bg-gray-100');
function fetchTaskDetails(taskId) {
setTaskDetailsLoading();
return fetch('/task/' + taskId + '/details', {
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
})
.then(res => {
if (!res.ok) {
throw new Error('HTTP ' + res.status);
}
return res.json();
})
.then(data => {
if (data.error) {
throw new Error(data.error);
}
Object.assign(currentTaskDetails, data);
tasksById[taskId] = currentTaskDetails;
tasksById[String(taskId)] = currentTaskDetails;
window.tasksById = tasksById;
updateModalInfoFields(currentTaskDetails);
renderTaskComments(currentTaskDetails);
renderTaskDurations(currentTaskDetails);
updateModalTotalDuration(currentTaskDetails);
const durationBtn = document.getElementById('modalDurationActionBtn');
if (durationBtn) {
const actionType = currentTaskDetails.active ? 'end' : 'start';
durationBtn.textContent = actionType === 'start' ? 'شروع' : 'پایان';
durationBtn.className = 'flex-1 px-4 py-2 rounded-lg font-medium text-white '
+ (actionType === 'start' ? 'bg-blue-600 hover:bg-blue-700' : 'bg-green-600 hover:bg-green-700')
+ ' transition';
}
})
.catch(err => {
console.error(err);
const errorHtml = '<div class="text-sm text-red-500 text-center">خطا در بارگذاری جزئیات</div>';
const commentsList = document.getElementById('modalCommentsList');
const durationsList = document.getElementById('modalDurationsList');
if (commentsList) commentsList.innerHTML = errorHtml;
if (durationsList) durationsList.innerHTML = errorHtml;
});
}
// مقداردهی اولیه (در صورت نیاز)
@@ -541,7 +858,7 @@ $state = [
const created = c.created_at ? new Date(c.created_at).toLocaleString('fa-IR') : '';
const body = escapeHtml(c.body).replace(/\n/g, '<br/>');
return `
<div class="bg-white border border-gray-200 rounded-lg p-2">
<div class="ui-panel border border-gray-200 rounded-lg p-2">
<div class="text-xs text-gray-500 flex justify-between">
<span>${escapeHtml(author)}</span>
<span>${escapeHtml(created)}</span>
@@ -615,7 +932,7 @@ $state = [
const end = d.end ? new Date(d.end).toLocaleString('fa-IR') : 'در حال انجام';
const dur = d.duration || (d.end ? '-' : '...');
return `
<div class="bg-white border border-gray-200 rounded-lg p-2">
<div class="ui-panel border border-gray-200 rounded-lg p-2">
<div class="text-xs text-gray-500 flex justify-between">
<span>${escapeHtml(uname)}</span>
<span class="font-mono">${escapeHtml(dur)}</span>
@@ -636,6 +953,7 @@ $state = [
const btn = document.getElementById(`modalTabBtn-${t}`);
if (el) el.classList.toggle('hidden', t !== tab);
if (btn) {
btn.classList.toggle('modal-tab-btn-active', t === tab);
btn.classList.toggle('bg-gray-100', t === tab);
btn.classList.toggle('text-gray-800', t === tab);
btn.classList.toggle('text-gray-500', t !== tab);
@@ -738,8 +1056,30 @@ $state = [
});
}
function formatAssigneeNames(task) {
const names = Array.isArray(task?.assignee_names) ? task.assignee_names.filter(Boolean) : [];
return names.length ? names.join('، ') : '-';
}
function updateModalInfoFields(task) {
document.getElementById('modalTaskCreator').textContent = task.creator_name || '-';
document.getElementById('modalTaskCreatedAt').textContent = task.created_at
? formatDateTime(task.created_at)
: '-';
document.getElementById('modalTaskAssignees').textContent = formatAssigneeNames(task);
}
// باز کردن مودال جزئیات تسک
function openTaskDetailsModal(task) {
function openTaskDetailsModal(taskOrId) {
const task = typeof taskOrId === 'object'
? taskOrId
: tasksById[taskOrId] ?? tasksById[String(taskOrId)];
if (!task) {
alert('اطلاعات این وظیفه در دسترس نیست.');
return;
}
currentTaskDetails = task;
// پر کردن فیلدهای مودال
@@ -747,7 +1087,7 @@ $state = [
document.getElementById('modalTaskDescription').innerHTML = task.description || 'بدون توضیحات';
document.getElementById('modalTaskStartDateView').textContent = task.start_date ? formatJalaliDate(task.start_date) : '-';
document.getElementById('modalTaskTargetDateView').textContent = task.target_date ? formatJalaliDate(task.target_date) : '-';
document.getElementById('modalTaskCreator').textContent = task.creator_name || '-';
updateModalInfoFields(task);
// اپ
const appNameEl = document.getElementById('modalTaskAppName');
@@ -902,11 +1242,10 @@ $state = [
switchDetailsTab('info');
setQuickEditMode(false);
renderTaskComments(task);
renderTaskDurations(task);
updateModalTotalDuration(task);
setTaskDetailsLoading();
setupCommentForm(task);
setupMentionAutocomplete();
fetchTaskDetails(task.id);
}
function setQuickEditMode(on) {
@@ -1089,9 +1428,9 @@ $state = [
function closeDetailsModal() {
const modal = document.getElementById('taskDetailsModal');
if (!modal) return;
modal.classList.add('hidden');
modal.style.display = 'none';
window.location.reload();
}
// فرمت تاریخ شمسی (ساده)
+2
View File
@@ -66,6 +66,8 @@ Route::post('/active_task', [TaskController::class, 'activeTask'])->name('task.a
Route::get('/create_task', [TaskController::class, 'createTask'])->name('task.create');
Route::get('/task/{id}/details', [TaskController::class, 'taskDetailsJson'])->name('task.details');
Route::get('/task/{id}', [TaskController::class, 'showTask'])->name('task.show');
Route::get('/task/delete/{id}', [TaskController::class, 'deleteTask'])->name('task.show');