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 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() public function dashboard()
{ {
@@ -29,60 +151,44 @@ class AdminController extends Controller
$uname = $_COOKIE['token-0']; $uname = $_COOKIE['token-0'];
setcookie("user_identity", $_COOKIE['token-0'], time() + (60 * 60 * 24 * 365), '/'); setcookie("user_identity", $_COOKIE['token-0'], time() + (60 * 60 * 24 * 365), '/');
Cookie::queue(Cookie::forget('token-0')); Cookie::queue(Cookie::forget('token-0'));
} else } else {
$uname = $_COOKIE['user_identity']; $uname = $_COOKIE['user_identity'] ?? null;
}
if (!$uname) {
return redirect('/')->with('message', [
'color' => '#ff8800ff',
'text' => 'ابتدا باید وارد شوید!'
]);
}
$u = User::select('name', 'is_admin') $u = User::select('name', 'is_admin')
->where('user_name', $uname) ->where('user_name', $uname)
->first(); ->first();
if (!$u) {
return redirect('/')->with('message', [
'color' => '#ff8800ff',
'text' => 'کاربر یافت نشد!'
]);
}
if ($u->is_admin == 1) { if ($u->is_admin == 1) {
$provider = 'admin'; $provider = 'admin';
} elseif ($u->is_admin == 2) { } elseif ($u->is_admin == 2) {
$provider = 'supervisor'; $provider = 'supervisor';
} }
if ($u->is_admin == '1' || $u->is_admin == '2') { if ($u->is_admin == '1' || $u->is_admin == '2') {
setcookie('ad', '1', time() + (60 * 60 * 24 * 365 * 5), "/"); setcookie('ad', '1', time() + (60 * 60 * 24 * 365 * 5), "/");
$users = User::wherenotnull('user_name') $users = User::whereNotNull('user_name')
->with('attendances') ->select(['id', 'name', 'user_name', 'req_counts', 'active_task'])
->get(); ->get();
$records = []; $this->enrichUsersForDashboard($users);
foreach ($users as $user) { $users = $users->sortByDesc('working');
$rec = [ $records = $this->loadPendingRequests($users);
"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); // خواندن تمام آیتم های لیست
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', [ return view("admin..admin")->with(['message', [
'color' => '#ff8800ff', 'color' => '#ff8800ff',
"text" => 'شما به پنل ادمین وارد شدید✔️' "text" => 'شما به پنل ادمین وارد شدید✔️'
+42 -30
View File
@@ -26,14 +26,14 @@ class AttendanceController extends Controller
public function index() public function index()
{ {
if (isset($_COOKIE['user_identity'])) { if (isset($_COOKIE['user_identity'])) {
$username = $_COOKIE['user_identity'];
$attendanceSince = Carbon::now()->subMonths(12)->startOfMonth()->format('Y-m-d 00:00:00');
$Attendance = $username == 1370
$Attendance = isset($_COOKIE['user_identity']) ? Attendance::where('entered_at', '>=', $attendanceSince)->get()
? ($_COOKIE['user_identity'] == 1370 : Attendance::where('user_identity', $username)
? Attendance::all() ->where('entered_at', '>=', $attendanceSince)
: Attendance::where('user_identity', $_COOKIE['user_identity'])->get()) ->get();
: collect();
$jalalian_object = Jalalian::now(); $jalalian_object = Jalalian::now();
@@ -48,23 +48,27 @@ class AttendanceController extends Controller
->orderByDesc('id') ->orderByDesc('id')
->first() ->first()
?? ai::where('key', 'angizeh')->orderByDesc('id')->first(); ?? ai::where('key', 'angizeh')->orderByDesc('id')->first();
$ids = array_merge($ids, [$s->id]); if ($s) {
Cookie::queue('seen_angizeh_ids', json_encode($ids), 14400); $ids = array_merge($ids, [$s->id]);
Cookie::queue('seen_angizeh_ids', json_encode($ids), 14400);
}
//total time //total time
$now = Jalalian::fromCarbon(Carbon::now()); $now = Jalalian::fromCarbon(Carbon::now());
$tsec = 0; $tsec = 0;
//dd(->user_profile);
if (isset($_COOKIE['token01'])) { if (isset($_COOKIE['token01'])) {
$a = json_decode($_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 { } 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 ($user && !empty($user->attendances)) {
if (!empty($user->attendances)) {
foreach ($user->attendances as $att) { foreach ($user->attendances as $att) {
$date = $att->entered_at != null ? Jalalian::fromCarbon(Carbon::parse($att->entered_at)) : null; $date = $att->entered_at != null ? Jalalian::fromCarbon(Carbon::parse($att->entered_at)) : null;
$att->date = $date != null ? $date->format('Y/m/d') : 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) $formaction = (isset($_COOKIE['ad']) && $_COOKIE['ad'] == 1)
? 'admin/' ? 'admin/'
: 'attendance/'; : '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(); $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; $until_now = null;
if ($user->break == 1) { $duration = null;
$att = Attendance::whereNull('exited_at')->latest('entered_at')->first(); if ($user && $user->break == 1) {
$break = AttendanceBreak::where('attendance_id', $att->id) $att = Attendance::where('user_identity', $user->user_name)
->whereNull('break_end') ->whereNull('exited_at')
->latest('break_start') ->latest('entered_at')
->first(); ->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 = (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) if ($user) {
->orderBy('target_date', 'desc') $user->tasks = Tasks::where('created_by', $user->id)
->get(); ->orWhereJsonContains('assignees', (string)$user->id)
//dd($user->tasks); ->orderBy('target_date', 'desc')
// dd($until_now); ->limit(50)
->get();
}
return view('attendance.index', compact('user'))->with([ return view('attendance.index', compact('user'))->with([
's' => $s, 's' => $s,
'existed' => $existed, 'existed' => $existed,
'formaction' => $formaction, 'formaction' => $formaction,
'breakDu' => $until_now ?? null, 'breakDu' => $until_now ?? null,
'duration' => $duration,
]); ]);
} else { } else {
foreach (request()->cookies as $name => $value) { foreach (request()->cookies as $name => $value) {
+277 -46
View File
@@ -101,6 +101,236 @@ class TaskController extends Controller
return implode("\n", $lines); 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() // public function tasks()
// { // {
// $user = User::where('user_name', $_COOKIE['user_identity'])->first(); // $user = User::where('user_name', $_COOKIE['user_identity'])->first();
@@ -135,61 +365,27 @@ class TaskController extends Controller
public function tasks() public function tasks()
{ {
if (!isset($_COOKIE['user_identity'])) {
return redirect('/')->with('message', [
'color' => 'red',
'text' => 'ابتدا باید وارد شوید!'
]);
}
$user = User::where('user_name', $_COOKIE['user_identity'])->first(); $user = User::where('user_name', $_COOKIE['user_identity'])->first();
if ($user) { if ($user) {
if ($user->is_admin == 1) { ['tasks' => $tasks, 'tasksPayload' => $tasksPayload, 'assigneeFilterOptions' => $assigneeFilterOptions] = $this->loadTasksPageData($user);
$tasks = Tasks::with(['duration', 'comments.user', 'appName']) $count = $tasks->count();
->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);
return view('tasks.tasks', [ return view('tasks.tasks', [
'tasks' => (new Tasks())->filterByState($tasks), 'tasks' => (new Tasks())->filterByState($tasks),
'tasksPayload' => $tasksPayload,
'assigneeFilterOptions' => $assigneeFilterOptions,
'count' => $count, 'count' => $count,
'user_id' => $user->id, 'user_id' => $user->id,
'isAdminLevel1' => $user->is_admin == 1, 'isAdminLevel1' => $user->is_admin == 1,
'mentionUsers' => User::select('id', 'name', 'user_name')->get(), 'mentionUsers' => User::select('id', 'name', 'user_name')->get(),
'appsList' => App::select('id', 'name_fa')->get(), 'appsList' => App::select('id', 'name_fa', 'type')->get(),
'priorityLabels' => [ 'priorityLabels' => [
'urgent' => 'فوری', 'urgent' => 'فوری',
'high' => 'بالا', '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) public function editTask(Request $request)
{ {
// dd($request->all()); // dd($request->all());
@@ -342,6 +569,10 @@ class TaskController extends Controller
public function activeTask(Request $request) public function activeTask(Request $request)
{ {
// dd($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(); $user = User::where('user_name', $_COOKIE['user_identity'])->first();
$task = Tasks::find($request->task_id); $task = Tasks::find($request->task_id);
if (!$task) { if (!$task) {
+5
View File
@@ -62,6 +62,11 @@ class Tasks extends Model
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function appName(){ public function appName(){
return $this->belongsTo(App::class, 'app' , 'id'); return $this->belongsTo(App::class, 'app' , 'id');
} }
Generated
+32 -22
View File
@@ -2348,25 +2348,27 @@
}, },
{ {
"name": "nette/schema", "name": "nette/schema",
"version": "v1.3.2", "version": "v1.3.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nette/schema.git", "url": "https://github.com/nette/schema.git",
"reference": "da801d52f0354f70a638673c4a0f04e16529431d" "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002",
"reference": "da801d52f0354f70a638673c4a0f04e16529431d", "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"nette/utils": "^4.0", "nette/utils": "^4.0",
"php": "8.1 - 8.4" "php": "8.1 - 8.5"
}, },
"require-dev": { "require-dev": {
"nette/tester": "^2.5.2", "nette/phpstan-rules": "^1.0",
"phpstan/phpstan-nette": "^1.0", "nette/tester": "^2.6",
"phpstan/extension-installer": "^1.4@stable",
"phpstan/phpstan": "^2.1.39@stable",
"tracy/tracy": "^2.8" "tracy/tracy": "^2.8"
}, },
"type": "library", "type": "library",
@@ -2376,6 +2378,9 @@
} }
}, },
"autoload": { "autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [ "classmap": [
"src/" "src/"
] ]
@@ -2404,35 +2409,37 @@
], ],
"support": { "support": {
"issues": "https://github.com/nette/schema/issues", "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", "name": "nette/utils",
"version": "v4.0.7", "version": "v4.1.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nette/utils.git", "url": "https://github.com/nette/utils.git",
"reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2" "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2", "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7",
"reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2", "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "8.0 - 8.4" "php": "8.2 - 8.5"
}, },
"conflict": { "conflict": {
"nette/finder": "<3", "nette/finder": "<3",
"nette/schema": "<1.2.2" "nette/schema": "<1.2.2"
}, },
"require-dev": { "require-dev": {
"jetbrains/phpstorm-attributes": "dev-master", "jetbrains/phpstorm-attributes": "^1.2",
"nette/phpstan-rules": "^1.0",
"nette/tester": "^2.5", "nette/tester": "^2.5",
"phpstan/phpstan": "^1.0", "phpstan/extension-installer": "^1.4@stable",
"phpstan/phpstan": "^2.1@stable",
"tracy/tracy": "^2.9" "tracy/tracy": "^2.9"
}, },
"suggest": { "suggest": {
@@ -2446,10 +2453,13 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "4.0-dev" "dev-master": "4.1-dev"
} }
}, },
"autoload": { "autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [ "classmap": [
"src/" "src/"
] ]
@@ -2490,9 +2500,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/nette/utils/issues", "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", "name": "nikic/php-parser",
@@ -8288,12 +8298,12 @@
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": [], "stability-flags": {},
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": "^8.2" "php": "^8.2"
}, },
"platform-dev": [], "platform-dev": {},
"plugin-api-version": "2.6.0" "plugin-api-version": "2.9.0"
} }
+8
View File
@@ -40,6 +40,14 @@ return [
'driver' => 'session', 'driver' => 'session',
'provider' => 'users', 'provider' => 'users',
], ],
'user' => [
'driver' => 'session',
'provider' => 'users',
],
'customer' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [ 'admin' => [
'driver' => 'jwt', 'driver' => 'jwt',
'provider' => 'admins', 'provider' => 'admins',
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "a.montask", "name": "a_montask",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
+37 -111
View File
@@ -1,63 +1,9 @@
<?php <?php
use App\Models\Attendance;
use App\Models\AttendanceBreak;
use Morilog\Jalali\Jalalian; use Morilog\Jalali\Jalalian;
use Carbon\Carbon; 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); $usersChunked = $users->chunk(3);
//dd($users);
?> ?>
@@ -66,43 +12,35 @@ $usersChunked = $users->chunk(3);
<div class="container w-full flex flex-col items-center"> <div class="container w-full flex flex-col items-center">
@if(!collect($reqs)->pluck('attendances')->flatten()->isEmpty()) @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"> <div class="ui-requests-card flex flex-col items-center">
<p style="min-font-size: 30; text-align: center; margin-bottom: 10px">درخواست ها</p> <p>درخواست ها</p>
{{-- {{dd($reqs)}}--}}
@foreach ($reqs as $u) @foreach ($reqs as $u)
@if(!empty($u['attendances'])) @if(!empty($u['attendances']))
<button type="button" <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 " class="ui-chip-btn"
onclick="togglerequests('attendance-{{ $loop->index }}')" onclick="togglerequests('attendance-{{ $loop->index }}')">
style="margin: 10px 0; text-align: center; background-color: #00bcd4">
{{ $u['name'] }} {{ $u['name'] }}
</button> </button>
{{-- @else--}}
{{-- <p class="text-stone-400" style="text-align: center">درخواستی ثبت نشده</p>--}}
{{-- @break--}}
@endif @endif
<div id="attendance-{{ $loop->index }}" class="hidden-content pt-4"> <div id="attendance-{{ $loop->index }}" class="hidden-content pt-4 w-full">
<table style="max-width: 350px; margin: auto; text-align: center" class="w-[350px] table-auto"> <div class="ui-table-wrap">
<table class="w-full table-auto">
<thead> <thead>
<tr class="text-sm" style="background-color: #00a3ba; color: white; text-align: center;"> <tr>
<th class="px-2 py-2 w-1/6">درخواست</th>
<th class="px-2 py-2 border w-1/6">درخواست</th> <th class="px-2 py-2 w-1/6">تاریخ</th>
<th class="px-2 py-2 border w-1/6">تاریخ</th> <th class="px-2 py-2 w-1/6">ورود</th>
<th class="px-2 py-2 border w-1/6">ورود</th> <th class="px-2 py-2 w-1/6">خروج</th>
<th class="px-2 py-2 border w-1/6">خروج</th> <th class="px-2 py-2 w-1/6">تعیین وضعیت</th>
{{-- @if($userType == 'admin')--}}
<th class="px-2 py-2 border w-1/6">تعیین وضعیت</th>
{{-- @endif--}}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{-- {{dd($u['attendances'])}}--}} {{-- {{dd($u['attendances'])}}--}}
@forelse($u['attendances'] as $rec) @forelse($u['attendances'] as $rec)
<tr style="background-color: {{$rec->id=='new' ? 'lightblue' : 'white'}}"> <tr class="{{ $rec->id=='new' ? 'is-new' : '' }}">
{{--{{dd($rec)}}--}} {{--{{dd($rec)}}--}}
<td class="px-4 py-2 border">{{ match ($rec->function) { <td class="px-4 py-2 border">{{ match ($rec->function) {
'edit' => "تغییر ساعت", 'edit' => "تغییر ساعت",
@@ -138,8 +76,7 @@ $usersChunked = $users->chunk(3);
<input type="hidden" name="update" id="update" value="approve"> <input type="hidden" name="update" id="update" value="approve">
<input type="hidden" name="data" id="data" <input type="hidden" name="data" id="data"
value="{{json_encode($rec)}}"> value="{{json_encode($rec)}}">
<button class="bg-white rounded w-full" <button type="submit" class="ui-btn-approve">
style="background-color: #81ff65; margin-bottom: 5px">
تایید تایید
</button> </button>
</form> </form>
@@ -148,8 +85,7 @@ $usersChunked = $users->chunk(3);
<input type="hidden" name="update" id="update" value="reject"> <input type="hidden" name="update" id="update" value="reject">
<input type="hidden" name="data" id="data" <input type="hidden" name="data" id="data"
value="{{json_encode($rec)}}"> value="{{json_encode($rec)}}">
<button class="bg-white rounded w-full" <button type="submit" class="ui-btn-reject">
style="background-color: #ff6565">
رد رد
</button> </button>
</form> </form>
@@ -163,7 +99,8 @@ $usersChunked = $users->chunk(3);
@endforelse @endforelse
</tbody> </tbody>
</table> </table>
</div> {{-- پایان hidden-content --}} </div>
</div>
@endforeach @endforeach
{{-- @if(collect($reqs)->pluck('attendances')->flatten()->isEmpty())--}} {{-- @if(collect($reqs)->pluck('attendances')->flatten()->isEmpty())--}}
{{-- <p class="text-stone-400" style="text-align: center">درخواستی ثبت نشده</p>--}} {{-- <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 "> <div class="grid grid-cols-2 grid-rows-none ">
@foreach ($usersChunked as $chunk) @foreach ($usersChunked as $chunk)
@foreach ($chunk as $u) @foreach ($chunk as $u)
<div class="form m-3 justify-evenly rounded-lg shadow-md " @php
style="align-items: center; text-align: center; border: 2px solid $cardClass = 'ui-user-card m-3';
{{ $u->active_task ? '#00ff15' : ($u->working == 1 ? '#16c8ce' : ($u->working == 2 ? 'orange' : 'white' ))}}; padding: 10px; "> if ($u->active_task) {
@php $cardClass .= ' is-active';
@endphp } elseif ($u->working == 1) {
<a href="/admin/{{ $u->user_name }}" $cardClass .= ' is-working';
class=" rounded-xl" } elseif ($u->working == 2) {
style="text-decoration: none; color: inherit;"> $cardClass .= ' is-break';
<p style="font-weight: bold ; color:#333;"> }
{{ $u->name ?? $u->user_name }} @endphp
</p> <div class="{{ $cardClass }}">
<a href="/admin/{{ $u->user_name }}">
<p class="ui-user-name">{{ $u->name ?? $u->user_name }}</p>
@if($u->active_task) @if($u->active_task)
<p>وظیفه: {{ $u->active_task }}</p> <p>وظیفه: {{ $u->active_task }}</p>
@endif @endif
<p> <p>کارکرد ماه جاری: {{ $u->totaltime }}</p>
کارکرد ماه جاری: <p>تعداد درخواست ها: {{ $u->req_counts }}</p>
{{ $u->totaltime }}
</p>
<p>
تعداد درخواست ها:
{{ $u->req_counts }}
</p>
</a> </a>
@if($userType == 'admin') @if($userType == 'admin')
<button type="button" <button type="button"
class="rounded-lg bg-cyan-500 hover:bg-cyan-600" class="ui-chip-btn mt-2 !min-w-0 !w-auto !text-sm"
onclick="window.location.href='{{ url('attendance/profile').'?username='.$u->user_name }}'" 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; ">
ویرایش ویرایش
</button> </button>
@endif @endif
@@ -214,10 +142,8 @@ $usersChunked = $users->chunk(3);
@endforeach @endforeach
@endforeach @endforeach
<a class="form m-3 justify-evenly rounded-lg shadow-md " href="{{route('admin.new')}}" <a class="ui-user-card m-3 flex items-center justify-center min-h-[140px]" href="{{route('admin.new')}}">
style="align-items: center; text-align: center; "> <i class="fa-regular fa-plus text-5xl text-gray-400"></i>
<i class="fa-regular fa-plus "
style="font-size: 80px; width: 90%; height: 90%;"></i>
</a> </a>
</div> </div>
+12 -14
View File
@@ -224,7 +224,7 @@ SVG;
@else @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 "> <img src="{{$user->picture ?? $gravatarUrl}}" alt="Profile Picture" class="w-16 h-16 rounded-full ">
<label> <label>
@@ -361,7 +361,7 @@ SVG;
<div class="w-full flex justify-center mt-4" style=""> <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"> <div class=" border-b border-gray-300 text-center w-full">
@@ -369,7 +369,7 @@ SVG;
<div class="w-[350px]"> <div class="w-[350px]">
@if(isset($_COOKIE['user_identity'])) @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" <thead class=" text-black sticky top-0 z-10"
style="background: rgba(244,244,244,0.82);border-bottom: 1px solid;border-color: #e1e1e1 "> style="background: rgba(244,244,244,0.82);border-bottom: 1px solid;border-color: #e1e1e1 ">
<tr> <tr>
@@ -456,10 +456,10 @@ SVG;
<!-- ATTENDANCE Modal --> <!-- ATTENDANCE Modal -->
<div id="modal-container" <div id="modal-container"
class="fixed inset-0 z-50 hidden bg-black bg-opacity-60 flex items-center justify-center p-4"> class="ui-modal-backdrop centered hidden">
<div id="modal-content" class=" bg-white p-4 sm:p-6 rounded-lg shadow-lg max-w-sm w-full relative"> <div id="modal-content" class="ui-modal-panel max-w-sm w-full">
<h2 id="modal-title" class="text-xl sm:text-2xl font-semibold mb-4 text-center"></h2> <h2 id="modal-title" class="ui-title"></h2>
<p id="modal-body" class="text-gray-700 mb-4 text-center"></p> <p id="modal-body" class="ui-subtitle"></p>
<div class="flex flex-col gap-4" style="align-items: center"> <div class="flex flex-col gap-4" style="align-items: center">
<div class="flex justify-center gap-2"> <div class="flex justify-center gap-2">
@@ -559,16 +559,14 @@ SVG;
{{-- START TASK MODAL --}} {{-- START TASK MODAL --}}
<div id="taskModal" <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">
<div class="relative bg-white w-[350px] rounded-2xl shadow-lg p-5 text-center"> <div class="ui-modal-panel">
<button type="button" onclick="closeTaskModal()" class="ui-modal-close" aria-label="بستن">
<button onclick="closeTaskModal()"
class="absolute top-3 right-3 text-gray-400 hover:text-gray-600 transition">
&times; &times;
</button> </button>
<h2 class="text-lg font-bold mb-3" id="modalTitle">انتخاب وظیفه</h2> <h2 class="ui-title" id="modalTitle">انتخاب وظیفه</h2>
<p class="text-gray-600 mb-4" id="modalDesc">یکی از وظایف زیر را انتخاب کنید:</p> <p class="ui-subtitle" id="modalDesc">یکی از وظایف زیر را انتخاب کنید:</p>
<div class="grid flex-col gap-2 grid-cols-2 pb-3 items-center justify-items-end"> <div class="grid flex-col gap-2 grid-cols-2 pb-3 items-center justify-items-end">
@if(!empty($user->tasks)) @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" <div class=" flex flex-col items-center justify-center p-4 " dir="rtl"
style="padding-top: 50px"> 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"> <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> </div>
<form id="attendanceForm" action="{{route('admin.new')}}" method="post" class="space-y-4"> <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 Morilog\Jalali\Jalalian;
use Carbon\Carbon; use Carbon\Carbon;
// پیدا کردن نام ماه قبل به تاریخ شمسی (مثلا اگر الان اسفند است، خروجی "بهمن" خواهد بود)
$previousMonthName = Jalalian::now()->subMonths(1)->format('%B'); $previousMonthName = Jalalian::now()->subMonths(1)->format('%B');
// محاسبه مجموع کارکرد برای هر کارمند به تفکیک ماه
$employeeTotals = []; $employeeTotals = [];
foreach ($data as $employee) { foreach ($data as $employee) {
// تبدیل آبجکت به آرایه در صورت نیاز برای حلقه
$recordsGrouped = is_array($employee->records) ? $employee->records : (array)$employee->records; $recordsGrouped = is_array($employee->records) ? $employee->records : (array)$employee->records;
$monthlyTotals = []; $monthlyTotals = [];
foreach ($recordsGrouped as $month => $attendances) { foreach ($recordsGrouped as $month => $attendances) {
$tsec = 0; $tsec = 0;
foreach ($attendances as $attendance) { foreach ($attendances as $attendance) {
// اگر duration موجود باشه استفاده می‌کنیم
if (!empty($attendance->duration)) { if (!empty($attendance->duration)) {
[$h, $m, $s] = explode(':', $attendance->duration); [$h, $m, $s] = explode(':', $attendance->duration);
$tsec += (int)$h * 3600 + (int)$m * 60 + (int)$s; $tsec += (int)$h * 3600 + (int)$m * 60 + (int)$s;
@@ -26,29 +22,239 @@ foreach ($data as $employee) {
$hours = floor($tsec / 3600); $hours = floor($tsec / 3600);
$minutes = floor(($tsec % 3600) / 60); $minutes = floor(($tsec % 3600) / 60);
// ذخیره زمان برای این ماه
$monthlyTotals[$month] = sprintf('%02d:%02d', $hours, $minutes); $monthlyTotals[$month] = sprintf('%02d:%02d', $hours, $minutes);
} }
// ذخیره کل ماه‌ها برای این کارمند
$employeeTotals[$employee->id] = $monthlyTotals; $employeeTotals[$employee->id] = $monthlyTotals;
} }
?> ?>
@extends('app') @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) @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> <h1>اطلاعاتی موجود نمیباشد</h1>
</div> </div>
@else @else
<div class="employee-picker">
<div class="flex flex-wrap justify-center gap-2 mt-10 mb-6 max-w-3xl">
@foreach($data as $employee) @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 }}')"> onclick="openEmployee(event, 'emp_{{ $employee->id }}', '{{ $employee->id }}')">
{{ $employee->name }} {{ $employee->name }}
</button> </button>
@@ -56,15 +262,13 @@ foreach ($data as $employee) {
</div> </div>
@foreach($data as $employee) @foreach($data as $employee)
<div id="emp_{{ $employee->id }}" class="employee-content hidden flex-col items-center justify-center w-full"> <div id="emp_{{ $employee->id }}" class="employee-panel">
<div class="employee-header">
<div class="flex justify-between items-center mb-4 w-[350px]"> <h2>{{ $employee->name }}</h2>
<h2 class="text-xl font-bold text-cyan-700">{{ $employee->name }}</h2> <div class="employee-stat">
<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">
<span>کارکرد ماه قبل ({{ $previousMonthName }}):</span> <span>کارکرد ماه قبل ({{ $previousMonthName }}):</span>
<strong class="text-sm font-black">{{ $employeeTotals[$employee->id][$previousMonthName] ?? '00:00' }}</strong> <strong>{{ $employeeTotals[$employee->id][$previousMonthName] ?? '00:00' }}</strong>
</span> </div>
</div> </div>
@php @php
@@ -72,13 +276,14 @@ foreach ($data as $employee) {
@endphp @endphp
@if(empty($recordsGrouped)) @if(empty($recordsGrouped))
<div class="my-10 text-gray-500 text-lg font-bold italic"> <div class="panel-empty">
رکورد ترددی برای این شخص ثبت نشده است. رکورد ترددی برای این شخص ثبت نشده است.
</div> </div>
@else @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) @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 }}')"> onclick="openMonth(event, 'month_{{ $employee->id }}_{{ $month }}', '{{ $employee->id }}')">
{{ $month }} {{ $month }}
</button> </button>
@@ -86,43 +291,37 @@ foreach ($data as $employee) {
</div> </div>
@foreach($recordsGrouped as $month => $records) @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"> <div id="month_{{ $employee->id }}_{{ $month }}"
<table style="width:100%; border-collapse: collapse; text-align: center"> class="attendance-card tabcontent-{{ $employee->id }}"
style="display: none;">
<table class="attendance-table">
<thead> <thead>
<tr style="border-bottom: 1px solid #ddd; background-color: #f8f9fa;"> <tr>
<th style="border-right: 1px solid #ddd; padding: 8px 0;">تاریخ</th> <th>تاریخ</th>
<th style="border-right: 1px solid #ddd;">روز</th> <th>روز</th>
<th style="border-right: 1px solid #ddd;">ورود</th> <th>ورود</th>
<th style="border-right: 1px solid #ddd;">خروج</th> <th>خروج</th>
<th style="border-right: 1px solid #ddd;">مدت</th> <th>مدت</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach($records as $row) @foreach($records as $row)
<tr class="hover:bg-cyan-100 transition-colors" style="border-bottom: 1px solid #ddd;"> <tr>
<td style="border-right: 1px solid #ddd; padding: 4px 0;"> <td>{{ Jalalian::fromCarbon(Carbon::parse($row->entered_at))->format('m/d') ?? '-' }}</td>
{{ Jalalian::fromCarbon(\Carbon\Carbon::parse($row->entered_at))->format('m/d') ?? '-' }} <td>{{ Jalalian::fromCarbon(Carbon::parse($row->entered_at))->format('%A') ?? '-' }}</td>
</td> <td>{{ Carbon::parse($row->entered_at)->format('H:i') ?? '-' }}</td>
<td style="border-right: 1px solid #ddd;"> <td>{{ $row->exited_at ? Carbon::parse($row->exited_at)->format('H:i') : '-' }}</td>
{{ Jalalian::fromCarbon(\Carbon\Carbon::parse($row->entered_at))->format('%A') ?? '-' }} <td>{{ $row->duration ? Carbon::parse($row->duration)->format('H:i') : '-' }}</td>
</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> </tr>
@endforeach @endforeach
</tbody>
<tfoot>
<tr> <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' }} مجموع کارکرد این ماه: {{ $employeeTotals[$employee->id][$month] ?? '00:00' }}
</td> </td>
</tr> </tr>
</tbody> </tfoot>
</table> </table>
</div> </div>
@endforeach @endforeach
@@ -132,58 +331,46 @@ foreach ($data as $employee) {
@endif @endif
</div> </div>
<style>
/* مخفی کردن اسکرول‌بار برای تب‌های ماه‌ها اگر تعدادشان زیاد شد */
.hide-scrollbar::-webkit-scrollbar { display: none; }
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
</style>
<script> <script>
function openEmployee(evt, empContainerId, empId) { function openEmployee(evt, empContainerId, empId) {
let employeeContents = document.getElementsByClassName("employee-content"); document.querySelectorAll('.employee-panel').forEach(el => el.classList.remove('is-visible'));
for (let i = 0; i < employeeContents.length; i++) { document.querySelectorAll('.emp-btn').forEach(btn => btn.classList.remove('emp-btn-active'));
employeeContents[i].style.display = "none";
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"); const firstMonthTab = document.querySelector('#' + empContainerId + ' .tablinks-' + empId);
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);
if (firstMonthTab) { if (firstMonthTab) {
firstMonthTab.click(); firstMonthTab.click();
} }
} }
function openMonth(evt, monthId, empId) { function openMonth(evt, monthId, empId) {
let tabcontents = document.getElementsByClassName("tabcontent-" + empId); document.querySelectorAll('.tabcontent-' + empId).forEach(el => {
for (let i = 0; i < tabcontents.length; i++) { el.style.display = 'none';
tabcontents[i].style.display = "none"; });
}
let tablinks = document.getElementsByClassName("tablinks-" + empId); document.querySelectorAll('.tablinks-' + empId).forEach(btn => {
for (let i = 0; i < tablinks.length; i++) { btn.classList.remove('month-tab-active');
tablinks[i].classList.remove("bg-white", "border-cyan-500"); });
tablinks[i].classList.add("bg-gray-100", "border-transparent");
}
document.getElementById(monthId).style.display = "block"; const monthPanel = document.getElementById(monthId);
evt.currentTarget.classList.remove("bg-gray-100", "border-transparent"); if (monthPanel) monthPanel.style.display = 'block';
evt.currentTarget.classList.add("bg-white", "border-cyan-500");
if (evt.currentTarget) {
evt.currentTarget.classList.add('month-tab-active');
}
} }
document.addEventListener("DOMContentLoaded", function () { document.addEventListener('DOMContentLoaded', function () {
let firstEmployeeBtn = document.querySelector(".emp-btn"); const firstEmployeeBtn = document.querySelector('.emp-btn');
if (firstEmployeeBtn) { if (firstEmployeeBtn) {
firstEmployeeBtn.click(); firstEmployeeBtn.click();
} }
}); });
</script> </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); return array_search($task->state, $state_order);
}); });
} }
//dd(count($break = $user->attendanceBreaks)); // $duration for break timer is passed from AttendanceController
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);
?> ?>
@extends('app') @extends('app')
@@ -123,7 +113,7 @@ if (count($user->attendanceBreaks) > 0) {
@else @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"> <label class="text-xl font-bold pb-2">
@@ -169,33 +159,27 @@ if (count($user->attendanceBreaks) > 0) {
</div> </div>
@else @else
{{-- دکمه‌های تب (نام ماه‌ها) --}} {{-- دکمه‌های تب (نام ماه‌ها) --}}
<div class="tab rounded-t-2xl max-w-[350px] mx-auto flex overflow-x-auto" <div class="ui-month-tabs">
style="margin-top: 0px; max-width: 350px">
@foreach($groupedData->keys() as $month) @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}}')"> onclick="openCity(event, '{{$month}}')">
{{$month}} {{$month}}
</button> </button>
@endforeach @endforeach
</div> </div>
{{-- محتوای تب‌ها (جدول‌ها) --}}
@foreach($groupedData as $month => $records) @foreach($groupedData as $month => $records)
<div id="{{$month}}" <div id="{{$month}}" class="tabcontent ui-attendance-card">
class="tabcontent w-[350px] bg-white rounded-b-xl rounded-tl-2xl cursor-default leading-7 shadow-md"> <table class="ui-attendance-table">
<table class="table-auto"
style="width:100%; border-collapse: collapse; text-align: center">
<thead> <thead>
<tr style="border-bottom: 1px solid #ddd; background: rgba(244,244,244,0.82);" <tr>
class="border-1 border-slate-300">
@if($_COOKIE['user_identity'] == 1370) @if($_COOKIE['user_identity'] == 1370)
<th style="border-right: 1px solid #ddd; padding: 8px;">نام</th> <th>نام</th>
@endif @endif
<th style="border-right: 1px solid #ddd; padding: 4px;">تاریخ</th> <th>تاریخ</th>
<th style="border-right: 1px solid #ddd; padding: 4px;">ورود</th> <th>ورود</th>
<th style="border-right: 1px solid #ddd; padding: 4px;">خروج</th> <th>خروج</th>
<th style="border-right: 1px solid #ddd; padding: 4px;">مدت</th> <th>مدت</th>
</tr> </tr>
</thead> </thead>
@@ -240,8 +224,7 @@ if (count($user->attendanceBreaks) > 0) {
{{-- ردیف جمع کل (اختیاری - چون متغیر آن موجود نبود کامنت کردم که خطا ندهد) --}} {{-- ردیف جمع کل (اختیاری - چون متغیر آن موجود نبود کامنت کردم که خطا ندهد) --}}
<tr> <tr>
<td colspan="5" class="rounded-b-xl bg-gray-50 font-bold" <td colspan="5" class="table-summary">
style="border-top: 1px solid #ddd; text-align: center; padding: 10px;">
تعداد رکورد: {{ count($records) }} تعداد رکورد: {{ count($records) }}
</td> </td>
</tr> </tr>
@@ -265,10 +248,10 @@ if (count($user->attendanceBreaks) > 0) {
<!-- ATTENDANCE Modal --> <!-- ATTENDANCE Modal -->
<div id="modal-container" <div id="modal-container"
class="fixed inset-0 z-50 hidden bg-black bg-opacity-60 flex items-center justify-center p-4"> class="ui-modal-backdrop centered hidden">
<div id="modal-content" class=" bg-white p-4 sm:p-6 rounded-lg shadow-lg max-w-sm w-full relative"> <div id="modal-content" class="ui-modal-panel max-w-sm w-full">
<h2 id="modal-title" class="text-xl sm:text-2xl font-semibold mb-4 text-center"></h2> <h2 id="modal-title" class="ui-title"></h2>
<p id="modal-body" class="text-gray-700 mb-4 text-center"></p> <p id="modal-body" class="ui-subtitle"></p>
<div class="flex flex-col gap-4" style="align-items: center"> <div class="flex flex-col gap-4" style="align-items: center">
<div class="flex justify-center gap-2"> <div class="flex justify-center gap-2">
@@ -376,23 +359,18 @@ if (count($user->attendanceBreaks) > 0) {
{{-- START TASK MODAL --}} {{-- START TASK MODAL --}}
<div id="taskModal" <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)"> onclick="closeOnOutsideClick(event)">
<div class="relative bg-white w-[350px] rounded-2xl shadow-lg p-5 text-center"> <div class="ui-modal-panel">
<button onclick="closeTaskModal()" <button type="button" onclick="closeTaskModal()" class="ui-modal-close" aria-label="بستن">
class="absolute top-3 right-3 text-gray-400 hover:text-gray-600 transition">
<i class="fa-solid fa-xmark"></i> <i class="fa-solid fa-xmark"></i>
</button> </button>
<h2 class="text-lg font-bold mb-3" id="modalTitle">انتخاب وظیفه</h2> <h2 class="ui-title" id="modalTitle">انتخاب وظیفه</h2>
<p class="ui-subtitle" id="modalDesc">یکی از وظایف زیر را انتخاب کنید:</p>
<p class="text-gray-600 mb-4" id="modalDesc">یکی از وظایف زیر را انتخاب کنید:</p> <input class="ui-input mb-3"
<input class="h-12 w-full bg-transparent pl-11 pr-4 text-sm text-custom-text-100 placeholder="جست و جو..." type="text" value=""
placeholder:text-custom-text-400 focus:ring-0 rounded-3xl border-2 border-gray-300 py-2" id="taskSearchInput">
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">
{{-- <div class="grid flex-col gap-2 grid-cols-2 pb-3 items-center justify-items-end">--}} {{-- <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) @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 <form action="{{url("active_task")}}" method="POST" class=" ">@csrf
{{-- <input type="hidden" name="pid" value="{{ $user->tasks->pivot->id }}" class="">--}} {{-- <input type="hidden" name="pid" value="{{ $user->tasks->pivot->id }}" class="">--}}
<input type="hidden" name="uid" value="{{ $user->id }}" class=""> <input type="hidden" name="uid" value="{{ $user->id }}" class="">
<div class=" overflow-y-scroll" <div class="ui-task-list">
style="max-height: calc(100vh - 300px);">
@foreach($user->tasks as $item) @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" <label class="ui-task-item">
style="display: flex">
@if($user->active_task) @if($user->active_task)
@if( $user->active_task == $item->title ) @if( $user->active_task == $item->title )
<input type="radio" name="task_id" checked="true" <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> <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: <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> {!! array_key_exists($item->state,$state) ? $state[$item->state] : "" !!};"></span>
</label>
</div>
@endforeach @endforeach
</div> </div>
@@ -446,21 +421,17 @@ if (count($user->attendanceBreaks) > 0) {
</form> </form>
@else @else
<div class="inline-flex justify-center items-center" <div class="flex justify-center items-center py-10">
style="height: calc(100vh - 400px);">
<p class="font-bold text-gray-400"> <p class="font-bold text-gray-400">
وظیفه ای وجود ندارد وظیفه ای وجود ندارد
</p> </p>
</div> </div>
@endif @endif
{{-- افزودن تسک--}} {{-- افزودن تسک--}}
<form> <form class="mt-3 flex items-center justify-center gap-2 flex-wrap">
<input class="h-12 w-[200px] bg-transparent pl-11 pr-4 text-sm text-custom-text-100 <input class="ui-input !w-[200px]"
placeholder:text-custom-text-400 focus:ring-0 rounded-3xl border-2 border-gray-300 py-2" placeholder="عنوان وظیفه" type="text" value=""
placeholder="عنوان وظیفه" tabindex="1" id="taskTitle" role="combobox" id="taskTitle">
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">
<button id="createTask" <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"> 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> </button>
@@ -468,10 +439,13 @@ if (count($user->attendanceBreaks) > 0) {
</form> </form>
</div> </div>
</div>
@endsection </div>
@section('script') @endsection
@section('script')
<script> <script>
const existed = {{ $existed ? 'true' : 'false' }}; const existed = {{ $existed ? 'true' : 'false' }};
@@ -735,23 +709,21 @@ if (count($user->attendanceBreaks) > 0) {
} }
//SEARCH BAR IN TASK MODAL //SEARCH BAR IN TASK MODAL
const searchInput = document.getElementById("headlessui-combobox-input-:r13m:"); const searchInput = document.getElementById("taskSearchInput");
const listItems = document.querySelectorAll('#taskModal .inline-flex.items-center'); const listItems = document.querySelectorAll('#taskModal .ui-task-item');
searchInput.addEventListener("input", function () { if (searchInput && listItems.length) {
const filter = searchInput.value.toLowerCase(); searchInput.addEventListener("input", function () {
listItems.forEach(item => { const filter = searchInput.value.toLowerCase();
const nameDiv = item.querySelector('div'); listItems.forEach(item => {
if (nameDiv) { const nameDiv = item.querySelector('div');
const name = nameDiv.textContent.toLowerCase(); if (nameDiv) {
if (name.includes(filter)) { const name = nameDiv.textContent.toLowerCase();
item.style.display = 'flex'; item.style.display = name.includes(filter) ? 'flex' : 'none';
} else {
item.style.display = 'none';
} }
} });
}); });
}); }
const taskTitle = document.getElementById('taskTitle'); const taskTitle = document.getElementById('taskTitle');
+3 -4
View File
@@ -2,7 +2,8 @@
@section('content') @section('content')
{{-- {{dd($user)}}--}} {{-- {{dd($user)}}--}}
<div class="w-full mt-[90px] justify-center container flex"> <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=""> <img src="/assets/img/bg/bg-shape.jpg" class="rounded-full w-32 h-32 mx-auto pb-6" alt="">
{{-- <p class=""> تاریخ شروع همکاری:--}} {{-- <p class=""> تاریخ شروع همکاری:--}}
{{-- {{\Morilog\Jalali\Jalalian::fromCarbon(\Carbon\Carbon::parse($user->created_at))->format('Y/m/d')}}</p>--}} {{-- {{\Morilog\Jalali\Jalalian::fromCarbon(\Carbon\Carbon::parse($user->created_at))->format('Y/m/d')}}</p>--}}
@@ -144,9 +145,7 @@
} }
</script> </script>
</div>
</div> </div>
</div> </div>
+50 -66
View File
@@ -3,14 +3,12 @@
use Morilog\Jalali\Jalalian; use Morilog\Jalali\Jalalian;
use Carbon\Carbon; use Carbon\Carbon;
//dd($data);
$monthlyTotals = []; $monthlyTotals = [];
foreach ($data as $month => $attendances) { foreach ($data as $month => $attendances) {
$tsec = 0; $tsec = 0;
foreach ($attendances as $attendance) { foreach ($attendances as $attendance) {
// اگر duration موجود باشه استفاده می‌کنیم
if (!empty($attendance->duration)) { if (!empty($attendance->duration)) {
[$h, $m, $s] = explode(':', $attendance->duration); [$h, $m, $s] = explode(':', $attendance->duration);
$tsec += (int)$h * 3600 + (int)$m * 60 + (int)$s; $tsec += (int)$h * 3600 + (int)$m * 60 + (int)$s;
@@ -19,88 +17,74 @@ foreach ($data as $month => $attendances) {
$hours = floor($tsec / 3600); $hours = floor($tsec / 3600);
$minutes = floor(($tsec % 3600) / 60); $minutes = floor(($tsec % 3600) / 60);
$seconds = $tsec % 60;
// ذخیره در آرایه جدید با کل زمان ماه
$monthlyTotals[$month] = sprintf('%02d:%02d', $hours, $minutes); $monthlyTotals[$month] = sprintf('%02d:%02d', $hours, $minutes);
} }
//dd($monthlyTotals);
?> ?>
@extends('app') @extends('app')
@section('content')
<div class="container inline-flex flex-col items-center justify-center "> @section('content')
<div class="ui-page">
@if(empty($data)) @if(empty($data))
<div class="mt-80 text-gray-500 text-xl font-bold italic"> <div class="ui-empty">
<h1>اطلاعاتی موجود نمیباشد</h1> <h1>اطلاعاتی موجود نمیباشد</h1>
</div> </div>
@else @else
<div class="tab rounded-t-2xl max-w-[350px] mx-auto flex" <div class="ui-month-tabs" style="margin-top: 5rem;">
style="margin-top: 80px; max-width: 350px"> @foreach(array_keys($data) as $month)
@foreach(array_keys($data) as $month) <button type="button" class="tablinks" onclick="openCity(event, '{{ $month }}')">{{ $month }}</button>
<button class="tablinks active rounded-t-2xl cursor-pointer" onclick="openCity(event, '{{$month}}')">{{$month}}</button> @endforeach
@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> </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 @endif
</div> </div>
<script> <script>
function openCity(evt, Month) { function openCity(evt, Month) {
var i, tabcontent, tablinks; document.querySelectorAll('.tabcontent').forEach(el => el.style.display = 'none');
tabcontent = document.getElementsByClassName("tabcontent"); document.querySelectorAll('.tablinks').forEach(el => el.classList.remove('active'));
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none"; const panel = document.getElementById(Month);
} if (panel) panel.style.display = 'block';
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) { if (evt.currentTarget) evt.currentTarget.classList.add('active');
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(Month).style.display = "block";
evt.currentTarget.className += " active";
} }
// باز کردن اولین تب به صورت پیش‌فرض document.addEventListener('DOMContentLoaded', function () {
document.addEventListener("DOMContentLoaded", function () { const firstTab = document.querySelector('.tablinks');
let firstTab = document.querySelector(".tablinks"); // اولین دکمه if (firstTab) firstTab.click();
if (firstTab) {
firstTab.click(); // همون اول کلیک مجازی بخوره
}
}); });
</script> </script>
@endsection @endsection
+6 -7
View File
@@ -19,11 +19,10 @@ if (isset($task)) {
?> ?>
@extends('app') @extends('app')
@section('content') @section('content')
<div class="w-full mt-[70px] justify-center container flex "> <div class="w-full mt-[70px] justify-center container flex">
<div class="bg-white w-[350px] rounded-xl "> <div class="ui-card">
<h1 class="w-[350px] bg-cyan-500 rounded-t-xl text-center p-1.5 font-bold text-slate-50">{{isset($task) ? "ویرایش وظیفه" : "ایجاد وظیفه جدید"}} <div class="ui-card-header">{{isset($task) ? "ویرایش وظیفه" : "ایجاد وظیفه جدید"}}</div>
</h1> <form action="{{isset($task) ? Route('task.edit') : Route('task.create')}}" method="POST" class="ui-card-body grid grid-cols-2 gap-4 items-center">
<form action="{{isset($task) ? Route('task.edit') : Route('task.create')}}" method="POST" class=" p-6 grid-cols-2 gap-4 items-center">
@csrf @csrf
@if(isset($task)) @if(isset($task))
<input type="hidden" id="id" name="id" value="{{$task->id}}"> <input type="hidden" id="id" name="id" value="{{$task->id}}">
@@ -87,7 +86,7 @@ if (isset($task)) {
<div class="relative" id="dropdown-container"> <div class="relative" id="dropdown-container">
{{-- دکمه --}} {{-- دکمه --}}
<button type="button" id="dropdown-btn" <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> <span id="selected-count">انتخاب کاربران</span>
<svg class="w-4 h-4 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -96,7 +95,7 @@ if (isset($task)) {
</button> </button>
<div id="dropdown-list" <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"> <div class="p-2 border-b">
<input type="text" id="search-input" <input type="text" id="search-input"
+499 -160
View File
@@ -64,22 +64,341 @@ $state = [
?> ?>
@extends('app') @extends('app')
@section('content')
<div class="container w-full flex justify-center items-center flex-col "> @section('style')
{{-- {{dd(count($tasks))}}--}} <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) @if($count > 0)
<div class="w-full max-w-6xl mx-auto mt-10 px-2"> <div class="w-full max-w-6xl mx-auto mt-4">
<div class="flex flex-nowrap gap-1 border-b border-gray-200"> @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) @foreach(array_keys($tasks) as $stateKey)
<button onclick="switchTaskTab('{{ $stateKey }}')" <button type="button" onclick="switchTaskTab('{{ $stateKey }}')"
class="task-tab px-4 py-2 rounded-t-lg font-medium text-sm transition class="task-tab {{ $loop->first ? 'task-tab-active' : '' }}"
{{ $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' }}"
data-tab="{{ $stateKey }}"> data-tab="{{ $stateKey }}">
{{ $stateKey }} <span class="task-tab-label">{{ $stateKey }}</span>
<span class="mr-1 text-xs bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full"> <span class="task-tab-count">{{ count($tasks[$stateKey] ?? []) }}</span>
{{ count($tasks[$stateKey] ?? []) }}
</span>
</button> </button>
@endforeach @endforeach
</div> </div>
@@ -87,142 +406,90 @@ $state = [
{{-- جداول هر وضعیت --}} {{-- جداول هر وضعیت --}}
@foreach($tasks as $stateKey => $stateTasks) @foreach($tasks as $stateKey => $stateTasks)
<div id="tab-{{ $stateKey }}" <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) @if(count($stateTasks) > 0)
<table class="min-w-full divide-y divide-gray-200"> <div class="tasks-cards-list">
<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">
@foreach($stateTasks as $task) @foreach($stateTasks as $task)
@php @php
// دریافت نام ایجادکننده (با فرض وجود رابطه 'creator' در مدل Tasks) $stateIcon = match((int)$task->state) {
$creatorName = 'نامشخص'; 1 => $backlog,
if ($task->relationLoaded('creator') && $task->creator) { 2 => $todo,
$creatorName = $task->creator->name; 3 => $in_progress,
} elseif ($task->created_by) { 4 => $done,
// اگر رابطه لود نشده، یک بار کاربر را پیدا کن (توصیه می‌شود در کنترلر eager load شود) default => '',
$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 ?? '';
@endphp @endphp
{{--@if($_COOKIE['user_identity'] == 'naser_me') {{dump($task)}} @endif--}} <article class="task-card task-row {{ $task->active ? 'is-active' : '' }}"
<tr class="hover:bg-gray-50"> data-task-id="{{ $task->id }}"
<td class="px-2 py-2 {{ $task->active ? 'border-s-4 border-cyan-400 ' : '' }}"> data-assignee-ids='@json($task->assignee_ids ?? [])'
<div class="flex items-center gap-2"> onclick="openTaskDetailsModal({{ $task->id }})"
<span class="inline-flex items-center justify-center w-5 h-5">{!! $stateIcon !!}</span> role="button"
<div> tabindex="0"
<div class="font-small text-gray-900 text-sm font-bold">{{ $task->title }}</div> onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();openTaskDetailsModal({{ $task->id }})}">
@if($task->description) <div class="task-card-title-row">
<div class="text-xs text-gray-500 mt-1 line-clamp-2">{{ \Illuminate\Support\Str::limit(strip_tags($task->description), 50) }}</div> <span class="task-card-icon">{!! $stateIcon !!}</span>
@endif <h3 class="task-card-title">{{ $task->title }}</h3>
</div> </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>
{{-- ستون زمان (همان دکمه شروع/پایان قبلی) --}} @if($task->description)
{{-- <td class="px-2 py-2">--}} <p class="task-card-desc">{{ strip_tags($task->description) }}</p>
{{-- <button onclick="openTaskTimeModal(--}} @endif
{{-- '{{ $task->id }}',--}}
{{-- '{{ $p_id }}',--}} <dl class="task-card-fields">
{{-- '{{ $started_at }}',--}} <div class="task-card-field">
{{-- '{{ $ended_at }}',--}} <dt class="task-card-label">اولویت</dt>
{{-- '{{ $actionType }}'--}} <dd class="task-card-value">
{{-- )" 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">--}} <span class="px-2 py-0.5 text-xs rounded-full {{ $priorityColors[$task->priority] ?? 'bg-gray-100 text-gray-800' }}">
{{-- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">--}} {{ $priorityLabels[$task->priority] ?? $task->priority }}
{{-- <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>--}} </span>
{{-- </svg>--}} </dd>
{{-- {{ $actionType == 'start' ? 'شروع' : 'پایان' }}--}} </div>
{{-- </button>--}}
{{-- </td>--}} @if($isAdminLevel1)
</tr> <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 @endforeach
</tbody> </div>
</table> <div class="task-filter-empty hidden tasks-empty">
تسکی برای این مسئول در این وضعیت وجود ندارد.
</div>
@else @else
<div class="p-8 text-center text-gray-400 bg-white rounded-b-lg"> <div class="tasks-empty">
هیچ وظیفه‌ای در این وضعیت وجود ندارد. هیچ وظیفه‌ای در این وضعیت وجود ندارد.
</div> </div>
@endif @endif
</div> </div>
@endforeach @endforeach
</div>
</div> </div>
@else @else
<div class=" mt-[200px]"> <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> </div>
@endif @endif
</div> </div>
@@ -230,12 +497,13 @@ $state = [
{{-- START TASK DETAILS MODAL --}} {{-- START TASK DETAILS MODAL --}}
<div id="taskDetailsModal" class="fixed inset-0 bg-black/40 hidden items-center justify-center z-50 p-4"> <div id="taskDetailsModal" class="fixed inset-0 bg-slate-900/35 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 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> <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"> <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" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path> d="M6 18L18 6M6 6l12 12"></path>
@@ -245,7 +513,7 @@ $state = [
<div class="p-4 space-y-4"> <div class="p-4 space-y-4">
{{-- Tabs --}} {{-- 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" <button id="modalTabBtn-info" type="button"
onclick="switchDetailsTab('info')" onclick="switchDetailsTab('info')"
class="flex-1 px-3 py-2 text-sm rounded-lg bg-gray-100 text-gray-800"> 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> <h3 class="text-sm font-medium text-gray-500 mb-1">ایجادکننده</h3>
<p class="text-gray-800 text-sm" id="modalTaskCreator">-</p> <p class="text-gray-800 text-sm" id="modalTaskCreator">-</p>
</div> </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>
<div class="flex gap-2 pt-2"> <div class="flex gap-2 pt-2">
@@ -389,7 +665,7 @@ $state = [
placeholder="مثلاً: @ali لطفاً این بخش رو بررسی کن" required></textarea> placeholder="مثلاً: @ali لطفاً این بخش رو بررسی کن" required></textarea>
<div id="mentionDropdown" <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>
</div> </div>
@@ -409,6 +685,7 @@ $state = [
<script> <script>
const mentionUsers = @json($mentionUsers); const mentionUsers = @json($mentionUsers);
const appsList = @json($appsList); const appsList = @json($appsList);
const tasksById = window.tasksById || {};
const isAdminLevel1 = @json($isAdminLevel1); const isAdminLevel1 = @json($isAdminLevel1);
const currentUserId = @json($user_id ?? null); const currentUserId = @json($user_id ?? null);
let currentTask = {}; let currentTask = {};
@@ -493,21 +770,61 @@ $state = [
document.getElementById(elementId).classList.toggle('hidden'); document.getElementById(elementId).classList.toggle('hidden');
} }
// تابع سوییچ تب‌های جدول function setTaskDetailsLoading() {
function switchTaskTab(tabId) { const commentsList = document.getElementById('modalCommentsList');
// مخفی کردن همه جداول const durationsList = document.getElementById('modalDurationsList');
document.querySelectorAll('.task-table-container').forEach(el => el.classList.add('hidden')); const loadingHtml = '<div class="text-sm text-gray-400 text-center">در حال بارگذاری...</div>';
// نمایش جدول انتخاب شده if (commentsList) commentsList.innerHTML = loadingHtml;
document.getElementById('tab-' + tabId).classList.remove('hidden'); if (durationsList) durationsList.innerHTML = loadingHtml;
}
// تغییر استایل تب‌های فعال function fetchTaskDetails(taskId) {
document.querySelectorAll('.task-tab').forEach(tab => { setTaskDetailsLoading();
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'); return fetch('/task/' + taskId + '/details', {
}); headers: {
const activeTab = document.querySelector(`.task-tab[data-tab="${tabId}"]`); 'Accept': 'application/json',
activeTab.classList.add('bg-white', 'text-blue-600', 'border-t', 'border-x', 'border-gray-200'); 'X-Requested-With': 'XMLHttpRequest',
activeTab.classList.remove('text-gray-500', 'hover:text-gray-700', 'hover:bg-gray-100'); },
})
.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 created = c.created_at ? new Date(c.created_at).toLocaleString('fa-IR') : '';
const body = escapeHtml(c.body).replace(/\n/g, '<br/>'); const body = escapeHtml(c.body).replace(/\n/g, '<br/>');
return ` 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"> <div class="text-xs text-gray-500 flex justify-between">
<span>${escapeHtml(author)}</span> <span>${escapeHtml(author)}</span>
<span>${escapeHtml(created)}</span> <span>${escapeHtml(created)}</span>
@@ -615,7 +932,7 @@ $state = [
const end = d.end ? new Date(d.end).toLocaleString('fa-IR') : 'در حال انجام'; const end = d.end ? new Date(d.end).toLocaleString('fa-IR') : 'در حال انجام';
const dur = d.duration || (d.end ? '-' : '...'); const dur = d.duration || (d.end ? '-' : '...');
return ` 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"> <div class="text-xs text-gray-500 flex justify-between">
<span>${escapeHtml(uname)}</span> <span>${escapeHtml(uname)}</span>
<span class="font-mono">${escapeHtml(dur)}</span> <span class="font-mono">${escapeHtml(dur)}</span>
@@ -636,6 +953,7 @@ $state = [
const btn = document.getElementById(`modalTabBtn-${t}`); const btn = document.getElementById(`modalTabBtn-${t}`);
if (el) el.classList.toggle('hidden', t !== tab); if (el) el.classList.toggle('hidden', t !== tab);
if (btn) { if (btn) {
btn.classList.toggle('modal-tab-btn-active', t === tab);
btn.classList.toggle('bg-gray-100', t === tab); btn.classList.toggle('bg-gray-100', t === tab);
btn.classList.toggle('text-gray-800', t === tab); btn.classList.toggle('text-gray-800', t === tab);
btn.classList.toggle('text-gray-500', 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; currentTaskDetails = task;
// پر کردن فیلدهای مودال // پر کردن فیلدهای مودال
@@ -747,7 +1087,7 @@ $state = [
document.getElementById('modalTaskDescription').innerHTML = task.description || 'بدون توضیحات'; document.getElementById('modalTaskDescription').innerHTML = task.description || 'بدون توضیحات';
document.getElementById('modalTaskStartDateView').textContent = task.start_date ? formatJalaliDate(task.start_date) : '-'; document.getElementById('modalTaskStartDateView').textContent = task.start_date ? formatJalaliDate(task.start_date) : '-';
document.getElementById('modalTaskTargetDateView').textContent = task.target_date ? formatJalaliDate(task.target_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'); const appNameEl = document.getElementById('modalTaskAppName');
@@ -902,11 +1242,10 @@ $state = [
switchDetailsTab('info'); switchDetailsTab('info');
setQuickEditMode(false); setQuickEditMode(false);
renderTaskComments(task); setTaskDetailsLoading();
renderTaskDurations(task);
updateModalTotalDuration(task);
setupCommentForm(task); setupCommentForm(task);
setupMentionAutocomplete(); setupMentionAutocomplete();
fetchTaskDetails(task.id);
} }
function setQuickEditMode(on) { function setQuickEditMode(on) {
@@ -1089,9 +1428,9 @@ $state = [
function closeDetailsModal() { function closeDetailsModal() {
const modal = document.getElementById('taskDetailsModal'); const modal = document.getElementById('taskDetailsModal');
if (!modal) return;
modal.classList.add('hidden'); modal.classList.add('hidden');
modal.style.display = 'none'; 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('/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/{id}', [TaskController::class, 'showTask'])->name('task.show');
Route::get('/task/delete/{id}', [TaskController::class, 'deleteTask'])->name('task.show'); Route::get('/task/delete/{id}', [TaskController::class, 'deleteTask'])->name('task.show');