Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c2e6c4e
fix: enforce date range validation across all date pickers platform-wide
abdulhaseeb-qa Aug 19, 2026
2902cf5
feat: add time entry view/edit modal, inline validation, and collapse…
abdulhaseeb-qa Aug 19, 2026
3166eef
feat: add bulk select/delete for time entries and move edit button to…
abdulhaseeb-qa Aug 19, 2026
f53dd00
fix: show all time entries by default without date filter
abdulhaseeb-qa Aug 19, 2026
b5092e3
fix: hide late column and late days card from attendance listing
abdulhaseeb-qa Aug 19, 2026
1c0ae90
fix: reduce column gap in attendance table after late column removal
abdulhaseeb-qa Aug 19, 2026
aa68a12
fix: distribute attendance table columns evenly with explicit widths
abdulhaseeb-qa Aug 19, 2026
9a91ef9
feat(hr): show attendance lateness to the owner and HR only
bladehell-ai Aug 19, 2026
e855bef
feat(hr): narrow the attendance lateness gate to the owner alone
bladehell-ai Aug 19, 2026
6af9f4e
Merge pull request #76 from codeupscale/feat/attendance-late-owner-hr…
bladehell-ai Aug 19, 2026
ce3ecbe
Merge pull request #77 from codeupscale/qa_fe_ui
bladehell-ai Aug 19, 2026
4621272
fix: dashboard weekly filters, task editing, and approval workflow im…
abdulhaseeb-qa Aug 19, 2026
b391968
fix: employee role restrictions, leave/attendance UI improvements
abdulhaseeb-qa Aug 20, 2026
fd7e5b6
Merge branch 'develop' into qa_dashboard_ui_fixes
usamamushtaqcodeupscale Aug 20, 2026
db3f58e
fix(web): read the task's real field, `name`, not the phantom `title`
usamamushtaqcodeupscale Aug 20, 2026
de87675
Merge pull request #78 from codeupscale/qa_dashboard_ui_fixes
bladehell-ai Aug 20, 2026
c074e8c
Merge pull request #79 from codeupscale/qa_fe_ui_v2
bladehell-ai Aug 20, 2026
312d6e4
fix(desktop): bind screenshot capture to the server entry id, not the…
bladehell-ai Aug 20, 2026
c51af12
fix(desktop): notify with sound when a user returns to a stopped timer
bladehell-ai Aug 20, 2026
9b614e8
Merge pull request #80 from codeupscale/fix/desktop-screenshots-bound…
bladehell-ai Aug 21, 2026
6550239
Merge branch 'develop' into fix/desktop-notify-on-return-from-break
bladehell-ai Aug 21, 2026
38a183d
Merge pull request #81 from codeupscale/fix/desktop-notify-on-return-…
bladehell-ai Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions CLAUDE.md

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions backend/app/Http/Controllers/Api/V1/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,9 @@ private function employeeDashboard(User $user, Request $request): JsonResponse
: null;

// Week range uses the user's timezone so the boundaries align with their calendar week
$weekStart = Carbon::now($tz)->startOfWeek(); // Monday 00:00 local
$weekEnd = Carbon::now($tz)->endOfWeek(); // Sunday 23:59 local
$weekOffset = (int) $request->input('week_offset', 0);
$weekStart = Carbon::now($tz)->startOfWeek()->addWeeks($weekOffset);
$weekEnd = Carbon::now($tz)->endOfWeek()->addWeeks($weekOffset);
[$weekStartUtc, $weekEndUtc] = TimezoneAwareDateRange::toUtcBounds(
$weekStart->toDateString(),
$weekEnd->toDateString(),
Expand Down Expand Up @@ -334,6 +335,9 @@ private function employeeDashboard(User $user, Request $request): JsonResponse
'weekly_hours_target' => $weeklyTarget,
'daily_breakdown' => $dailyBreakdown,
'activity_percentage' => $activityPercentage,
'week_offset' => $weekOffset,
'week_start' => $weekStart->toDateString(),
'week_end' => $weekEnd->toDateString(),
'date_from' => $responseDateFrom,
'date_to' => $responseDateTo,
]);
Expand Down
23 changes: 23 additions & 0 deletions backend/app/Http/Controllers/Api/V1/Hr/DepartmentController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ public function index(Request $request): JsonResponse
{
$query = Department::where('organization_id', $request->user()->organization_id);

if ($request->user()->isEmployee()) {
$deptId = $request->user()->employeeProfile?->department_id;
if ($deptId) {
$query->where('id', $deptId);
} else {
$query->whereRaw('1 = 0');
}
}

if ($request->has('is_active')) {
$query->where('is_active', filter_var($request->input('is_active'), FILTER_VALIDATE_BOOLEAN));
}
Expand Down Expand Up @@ -82,6 +91,20 @@ public function destroy(Request $request, string $id): JsonResponse

public function tree(Request $request): JsonResponse
{
if ($request->user()->isEmployee()) {
$deptId = $request->user()->employeeProfile?->department_id;
if ($deptId) {
$dept = Department::where('organization_id', $request->user()->organization_id)
->with('positions')
->find($deptId);
$tree = $dept ? [$dept->toArray()] : [];
} else {
$tree = [];
}

return response()->json(['tree' => $tree]);
}

$tree = $this->service->getOrgTree($request->user()->organization);

return response()->json(['tree' => $tree]);
Expand Down
48 changes: 38 additions & 10 deletions backend/app/Http/Controllers/Api/V1/TimeEntryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ public function update(Request $request, string $id): JsonResponse
$request->validate([
'project_id' => 'nullable|uuid',
'task_id' => 'nullable|uuid',
'task_name' => 'nullable|string|max:255',
'started_at' => 'sometimes|date',
'ended_at' => 'nullable|date|after:started_at',
'notes' => 'nullable|string|max:1000',
Expand All @@ -129,7 +130,33 @@ public function update(Request $request, string $id): JsonResponse
$request->user()->organization->tasks()->findOrFail($request->task_id);
}

$data = $request->only(['project_id', 'task_id', 'started_at', 'ended_at', 'notes']);
// Auto-create task from free-text name if no task_id provided
$taskId = $request->task_id;
if (! $taskId && ! empty($request->task_name)) {
$projectId = $request->project_id ?? $entry->project_id;
if ($projectId) {
$org = $request->user()->organization;
$task = \App\Models\Task::firstOrCreate(
['organization_id' => $org->id, 'project_id' => $projectId, 'name' => trim($request->task_name)],
['created_by' => $request->user()->id]
);
$taskId = $task->id;
}
}

$data = $request->only(['project_id', 'started_at', 'ended_at', 'notes']);
if ($request->has('task_id') || $request->has('task_name')) {
$data['task_id'] = $taskId;
}

// Check if time fields actually changed before we apply the update.
$timeChanged = false;
if ($request->has('started_at') && $entry->started_at->toISOString() !== \Carbon\Carbon::parse($request->started_at)->toISOString()) {
$timeChanged = true;
}
if ($request->has('ended_at') && ($entry->ended_at?->toISOString() ?? null) !== ($request->ended_at ? \Carbon\Carbon::parse($request->ended_at)->toISOString() : null)) {
$timeChanged = true;
}

// Capture pre-edit approval state so we know whether cached report totals
// (which only include approved entries) need busting after the mutation.
Expand All @@ -144,15 +171,12 @@ public function update(Request $request, string $id): JsonResponse
]);
}

// Approval integrity (HIGH): a self-edit of a manual entry must not keep a
// stale 'approved'/'rejected' status when the editor lacks approve
// authority — the (possibly inflated) hours must not reach billable/payroll
// totals without a fresh re-approval. mustResetOnEdit() preserves the
// create() auto-approve-self rule so an org/project-scoped approver editing
// their OWN entry is NOT trapped in a queue only they could clear.
// Tracked/idle entries are not part of the approval workflow — leave them.
// Only reset approval when TIME fields changed — task/notes edits update
// immediately without re-approval.
$approvalReset = false;
if (
$entry->type === 'manual'
$timeChanged
&& $entry->type === 'manual'
&& in_array($entry->approval_status, ['approved', 'rejected'], true)
&& $this->manualTimeEntries->mustResetOnEdit($request->user(), $entry)
) {
Expand All @@ -163,6 +187,7 @@ public function update(Request $request, string $id): JsonResponse
'approved_at' => null,
'rejection_reason' => null,
]);
$approvalReset = true;
}

// Cache staleness (LOW 1): editing a manual entry, or any entry that was
Expand All @@ -172,7 +197,10 @@ public function update(Request $request, string $id): JsonResponse
app(ReportService::class)->flushForOrg($entry->organization_id);
}

return response()->json(['entry' => $entry->fresh()->load(['project', 'task'])]);
return response()->json([
'entry' => $entry->fresh()->load(['project', 'task']),
'approval_reset' => $approvalReset,
]);
}

// TIME-07: Delete entry
Expand Down
2 changes: 1 addition & 1 deletion backend/app/Http/Requests/Hr/StoreLeaveRequestRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function rules(): array
'uuid',
Rule::exists('leave_types', 'id')->where('organization_id', $orgId),
],
'start_date' => ['required', 'date', 'after_or_equal:today'],
'start_date' => ['required', 'date', 'after_or_equal:'.now()->subDays(7)->toDateString()],
'end_date' => ['required', 'date', 'after_or_equal:start_date'],
'reason' => ['required', 'string', 'max:1000'],
'half_day' => ['sometimes', 'boolean'],
Expand Down
1 change: 1 addition & 0 deletions backend/app/Http/Requests/StoreTimeEntryRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function rules(): array
],
'project_id' => ['nullable', 'uuid'],
'task_id' => ['nullable', 'uuid'],
'task_name' => ['nullable', 'string', 'max:255'],
'started_at' => ['required', 'date', 'before_or_equal:now'],
'ended_at' => ['required', 'date', 'after:started_at'],
'notes' => ['nullable', 'string', 'max:1000'],
Expand Down
10 changes: 8 additions & 2 deletions backend/app/Policies/TimeEntryPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,15 @@ public function delete(User $user, TimeEntry $entry): bool
return false;
}

// Own entry — only if user has delete permission at own scope
// Own entry — allow deleting own manual entries that are pending or rejected
// (even without time_entries.delete permission)
if ($user->id === $entry->user_id) {
return app(PermissionService::class)->hasPermission($user, 'time_entries.delete');
if (app(PermissionService::class)->hasPermission($user, 'time_entries.delete')) {
return true;
}

return $entry->type === 'manual'
&& in_array($entry->approval_status, ['pending', 'rejected'], true);
}

$service = app(PermissionService::class);
Expand Down
15 changes: 13 additions & 2 deletions backend/app/Services/ManualTimeEntryService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Services;

use App\Models\Task;
use App\Models\TimeEntry;
use App\Models\User;
use Carbon\Carbon;
Expand Down Expand Up @@ -84,15 +85,25 @@ public function create(User $actor, array $data): TimeEntry
$org->tasks()->findOrFail($data['task_id']);
}

// Auto-create task from free-text name if no task_id provided
$taskId = $data['task_id'] ?? null;
if (! $taskId && ! empty($data['task_name']) && ! empty($data['project_id'])) {
$task = Task::firstOrCreate(
['organization_id' => $org->id, 'project_id' => $data['project_id'], 'name' => trim($data['task_name'])],
['created_by' => $actor->id]
);
$taskId = $task->id;
}

$startedAt = Carbon::parse($data['started_at']);
$endedAt = Carbon::parse($data['ended_at']);

return DB::transaction(function () use ($actor, $targetUser, $org, $data, $startedAt, $endedAt, $approved) {
return DB::transaction(function () use ($actor, $targetUser, $org, $data, $startedAt, $endedAt, $approved, $taskId) {
$entry = TimeEntry::create([
'organization_id' => $org->id,
'user_id' => $targetUser->id,
'project_id' => $data['project_id'] ?? null,
'task_id' => $data['task_id'] ?? null,
'task_id' => $taskId,
'started_at' => $startedAt,
'ended_at' => $endedAt,
'duration_seconds' => (int) abs($endedAt->diffInSeconds($startedAt)),
Expand Down
Loading
Loading