Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ This project was bootstrapped from the official [Laravel Vue Starter Kit](https:
- Assignee (filtered by specific team members or unassigned tasks).
- Tags (multi-select filter).
- Due Dates (shortcuts like Today, This Week, Overdue, or custom exact dates and date ranges).
- **Task Details & Collaboration:** Task editing with rich description styling (Tiptap editor), comments with threaded replies, and a detailed activity log timeline.
- **Workload Dashboard:** Team workspace overview featuring task metrics, urgent "To Handle Now" tasks, column breakdowns, and a recent activity feed (with direct links back to tasks).
- **Task Details & Collaboration:**
- Task editing with rich description styling (Tiptap editor), comments with threaded replies, and a detailed activity log timeline.
- Discussion threads with nested replies.
- Detailed activity log timeline for full auditability.
- **Document Uploads:** Attach files directly to tasks (currently supporting Images and PDFs).
- **Smart Link Previews:** Automatic, rich previews for URLs embedded in task descriptions and comments.
- **Workload Dashboard:** Team workspace overview featuring task metrics, urgent "To Handle Now" tasks, column breakdowns, and a recent activity feed.

## Tech Stack

- **Backend:** Laravel 12 (PHP >= 8.2), Inertia.js V3
- **Backend:** Laravel 12 (PHP >= 8.4), Inertia.js V3
- **Frontend:** Vue 3
- **Styling:** Tailwind CSS & Shadcn
- **CI/CD:** GitHub Actions workflow for automated testing, linting and deployment.
Expand All @@ -28,7 +33,7 @@ This project was bootstrapped from the official [Laravel Vue Starter Kit](https:
### Prerequisites

Make sure you have the following installed on your local system:
- **PHP** >= 8.2
- **PHP** >= 8.4
- **Composer**
- **Node.js** >= 20
- **NPM**
Expand Down
2 changes: 2 additions & 0 deletions app/Http/Controllers/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ public function __invoke(Request $request): Response
$totalTasks = (clone $baseTasksQuery)->count();

$overdueQuery = (clone $baseTasksQuery)
->whereHas('column', fn($query) => $query->where('type', '!=', 'done'))
->whereNotNull('due_date')
->whereDate('due_date', '<', today());

$dueTodayQuery = (clone $baseTasksQuery)
->whereHas('column', fn($query) => $query->where('type', '!=', 'done'))
->whereNotNull('due_date')
->whereDate('due_date', today());

Expand Down
6 changes: 6 additions & 0 deletions app/Http/Requests/TaskCreateRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public function rules(): array
'title' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string', 'max:1000'],
'due_date' => ['nullable', 'date'],
'created_by' => [
'nullable',
Rule::exists('users', 'id')->where(
fn($query) => $query->where('team_id', $this->user()?->team_id)
),
],
'assigned_to' => [
'nullable',
Rule::exists('users', 'id')->where(
Expand Down
7 changes: 7 additions & 0 deletions app/Http/Requests/TaskUpdateRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ public function rules(): array
'title' => ['sometimes', 'string', 'max:255'],
'description' => ['nullable', 'string', 'max:1000'],
'due_date' => ['sometimes', 'nullable', 'date'],
'created_by' => [
'sometimes',
'nullable',
Rule::exists('users', 'id')->where(
fn($query) => $query->where('team_id', $this->user()?->team_id)
),
],
'assigned_to' => [
'sometimes',
'nullable',
Expand Down
2 changes: 1 addition & 1 deletion app/Services/TaskService.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function createTask(array $data, User $user): Task
return DB::transaction(function () use ($data, $user, $columnId, $order, $columnName, $tagIds, $newFiles) {
$task = Task::create(array_merge($data, [
'team_id' => $user->team_id,
'created_by' => $user->id,
'created_by' => $data['created_by'] ?? $user->id,
'column_id' => $columnId,
'order' => $order,
'column_updated_at' => now(), // Initialize column timing
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
],
"license": "MIT",
"require": {
"php": "^8.2",
"php": "^8.4",
"inertiajs/inertia-laravel": "^3.0",
"laravel/fortify": "^1.30",
"laravel/framework": "^12.0",
Expand Down
2 changes: 2 additions & 0 deletions resources/js/components/tasks/TaskAssigneeSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { computed } from 'vue';
const props = defineProps<{
modelValue: number | null;
teamMembers: TeamMember[];
allowUnassigned?: boolean;
}>();

const emit = defineEmits<{
Expand Down Expand Up @@ -72,6 +73,7 @@ const selectMember = (memberId: number | null) => {
<DropdownMenuSeparator />

<DropdownMenuItem
v-if="allowUnassigned !== false"
class="cursor-pointer"
@click="selectMember(null)"
>
Expand Down
25 changes: 20 additions & 5 deletions resources/js/components/tasks/TaskCreateDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const form = useForm<TaskForm>({
description: '',
due_date: '',
assigned_to: currentUserId.value,
created_by: currentUserId.value,
tag_ids: [],
attachments: [],
});
Expand All @@ -54,6 +55,7 @@ const submit = () => {
onSuccess: () => {
form.reset();
form.assigned_to = currentUserId.value;
form.created_by = currentUserId.value;
form.tag_ids = [];
selectedTags.value = [];
isOpen.value = false;
Expand Down Expand Up @@ -128,18 +130,31 @@ const submit = () => {
<div class="grid gap-2">
<Label>Assigned to</Label>
<TaskAssigneeSelect
:allow-unassigned="true"
v-model="form.assigned_to"
:team-members="teamMembers"
/>
<InputError :message="form.errors.assigned_to" />
</div>
</div>

<TagSelector
:selected="selectedTags"
:available-tags="availableTags"
@update:selected="onTagsUpdated"
/>
<div class="grid gap-4 sm:grid-cols-2">
<div class="grid gap-2">
<Label>Reporter</Label>
<TaskAssigneeSelect
v-model="form.created_by"
:team-members="teamMembers"
:allow-unassigned="false"
/>
<InputError :message="form.errors.created_by" />
</div>

<TagSelector
:selected="selectedTags"
:available-tags="availableTags"
@update:selected="onTagsUpdated"
/>
</div>

<DialogFooter>
<Button
Expand Down
3 changes: 3 additions & 0 deletions resources/js/components/tasks/TaskEditDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const hydrateFormsFromTask = (task: Task) => {
? task.due_date.slice(0, 16)
: '';
form.assigned_to = task.assigned_to ?? null;
form.created_by = task.created_by;
form.tag_ids = task.tags?.map((t) => t.id) ?? [];
form.attachments = [];
form.removed_attachment_ids = [];
Expand Down Expand Up @@ -99,6 +100,7 @@ const loadTaskDetails = async (taskId: number, showLoader = true) => {
? task.due_date.slice(0, 16)
: '';
form.assigned_to = task.assigned_to ?? null;
form.created_by = task.created_by;
form.attachments = [];
form.removed_attachment_ids = [];
commentsList.value = [...(task.comments ?? [])];
Expand Down Expand Up @@ -290,6 +292,7 @@ watch([() => props.task, isOpen], ([task, open], [oldTask, oldOpen]) => {
@update:description="form.description = $event"
@update:due-date="form.due_date = $event"
@update:assigned-to="form.assigned_to = $event"
@update:created-by="form.created_by = $event"
@update:tag-ids="form.tag_ids = $event"
@update:attachments="form.attachments = $event"
@update:removed-attachment-ids="form.removed_attachment_ids = $event"
Expand Down
27 changes: 22 additions & 5 deletions resources/js/components/tasks/TaskEditFormPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const emit = defineEmits<{
'update:description': [value: string];
'update:due-date': [value: string];
'update:assigned-to': [value: number | null];
'update:created-by': [value: number | undefined];
'update:tag-ids': [value: number[]];
'update:attachments': [value: File[]];
'update:removed-attachment-ids': [value: string[]];
Expand Down Expand Up @@ -103,6 +104,7 @@ const onTagsUpdated = (tags: Tag[]) => {
<TaskAssigneeSelect
:model-value="form.assigned_to"
:team-members="teamMembers"
:allow-unassigned="true"
@update:model-value="
$emit('update:assigned-to', $event)
"
Expand All @@ -111,11 +113,26 @@ const onTagsUpdated = (tags: Tag[]) => {
</div>
</div>

<TagSelector
:selected="selectedTags"
:available-tags="availableTags"
@update:selected="onTagsUpdated"
/>
<div class="grid gap-4 sm:grid-cols-2">
<div class="grid gap-2">
<Label>Reporter</Label>
<TaskAssigneeSelect
:model-value="form.created_by ?? null"
:team-members="teamMembers"
:allow-unassigned="false"
@update:model-value="
$emit('update:created-by', $event ?? undefined)
"
/>
<InputError :message="form.errors.created_by" />
</div>

<TagSelector
:selected="selectedTags"
:available-tags="availableTags"
@update:selected="onTagsUpdated"
/>
</div>
</div>
</div>

Expand Down
2 changes: 2 additions & 0 deletions resources/js/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export interface TaskForm {
description: string;
due_date: string;
assigned_to: number | null;
created_by?: number;
tag_ids: number[];
attachments: File[];
}
Expand Down Expand Up @@ -177,6 +178,7 @@ export interface TaskEditFormState {
description: string;
due_date: string;
assigned_to: number | null;
created_by?: number;
tag_ids: number[];
attachments: File[];
removed_attachment_ids: string[];
Expand Down
Loading