Fix task listing and filters, add project colors/icons
- Remove user membership filter from task query — show all org tasks, not just user's accessible ones - Add color and icon fields to projects with DB migration and entity updates - Fetch org members and projects upfront for filters, not just from task results - Convert project and status filters to chips like categories, with color/icon display - Add icon and color picker to project creation form Backend changes: - TaskRepositoryImpl: remove (pu.email = ?2 or tu.email = ?2) from WHERE clause - TaskRepository interface: remove email param from findAccessibleTasksFiltered - ListAllTasksUseCase: adjust call without email - CreateProjectUseCase: accept color and icon params - ProjectController: update request/response records, map new fields - ProjectEntityMapper: map color/icon in both directions - Add V3 migration for color/icon columns Frontend changes: - TaskListPage: fetch org members and projects on org change - TaskFilterBar: replace dropdowns with chips for projects and status - ProjectsPage: add color picker and icon picker to form - Project type: add optional color/icon fields - TaskAssignee: allow photo to be nullable Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
814bc51b86
commit
3f67d34306
14 changed files with 150 additions and 42 deletions
|
|
@ -8,6 +8,8 @@ import net.beetechgroup.beetask.entities.organization.Organization;
|
||||||
public class Project {
|
public class Project {
|
||||||
private Long id;
|
private Long id;
|
||||||
private String name;
|
private String name;
|
||||||
|
private String color;
|
||||||
|
private String icon;
|
||||||
private Organization organization;
|
private Organization organization;
|
||||||
private List<User> users;
|
private List<User> users;
|
||||||
|
|
||||||
|
|
@ -27,6 +29,22 @@ public class Project {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getColor() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColor(String color) {
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIcon() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIcon(String icon) {
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
|
||||||
public Organization getOrganization() {
|
public Organization getOrganization() {
|
||||||
return organization;
|
return organization;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ public class ProjectEntity {
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
private String name;
|
private String name;
|
||||||
|
private String color;
|
||||||
|
private String icon;
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "organization_id")
|
@JoinColumn(name = "organization_id")
|
||||||
private OrganizationEntity organization;
|
private OrganizationEntity organization;
|
||||||
|
|
@ -43,6 +45,21 @@ public class ProjectEntity {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getColor() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColor(String color) {
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIcon() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIcon(String icon) {
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
|
||||||
public OrganizationEntity getOrganization() {
|
public OrganizationEntity getOrganization() {
|
||||||
return organization;
|
return organization;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ public class ProjectEntityMapper {
|
||||||
ProjectEntity entity = new ProjectEntity();
|
ProjectEntity entity = new ProjectEntity();
|
||||||
entity.setId(project.getId());
|
entity.setId(project.getId());
|
||||||
entity.setName(project.getName());
|
entity.setName(project.getName());
|
||||||
|
entity.setColor(project.getColor());
|
||||||
|
entity.setIcon(project.getIcon());
|
||||||
entity.setOrganization(OrganizationEntityMapper.toEntity(project.getOrganization()));
|
entity.setOrganization(OrganizationEntityMapper.toEntity(project.getOrganization()));
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +23,8 @@ public class ProjectEntityMapper {
|
||||||
Project project = new Project();
|
Project project = new Project();
|
||||||
project.setId(entity.getId());
|
project.setId(entity.getId());
|
||||||
project.setName(entity.getName());
|
project.setName(entity.getName());
|
||||||
|
project.setColor(entity.getColor());
|
||||||
|
project.setIcon(entity.getIcon());
|
||||||
project.setOrganization(OrganizationEntityMapper.toDomain(entity.getOrganization()));
|
project.setOrganization(OrganizationEntityMapper.toDomain(entity.getOrganization()));
|
||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,17 +53,16 @@ public class TaskRepositoryImpl implements TaskRepository, PanacheRepository<Tas
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Task> findAccessibleTasksFiltered(String email, Long organizationId, String text, List<Long> projectIds,
|
public List<Task> findAccessibleTasksFiltered(Long organizationId, String text, List<Long> projectIds,
|
||||||
List<TaskStatus> statuses, List<Long> categoryIds, List<Long> userIds) {
|
List<TaskStatus> statuses, List<Long> categoryIds, List<Long> userIds) {
|
||||||
StringBuilder query = new StringBuilder(
|
StringBuilder query = new StringBuilder(
|
||||||
"select distinct t from TaskEntity t join t.project p left join p.users pu left join t.user tu " +
|
"select distinct t from TaskEntity t join t.project p " +
|
||||||
"where p.organization.id = ?1 and (pu.email = ?2 or tu.email = ?2)"
|
"where p.organization.id = ?1"
|
||||||
);
|
);
|
||||||
List<Object> params = new ArrayList<>();
|
List<Object> params = new ArrayList<>();
|
||||||
params.add(organizationId);
|
params.add(organizationId);
|
||||||
params.add(email);
|
|
||||||
|
|
||||||
int idx = 3;
|
int idx = 2;
|
||||||
if (Objects.nonNull(text) && !text.isBlank()) {
|
if (Objects.nonNull(text) && !text.isBlank()) {
|
||||||
query.append(" and (lower(t.title) like ?").append(idx).append(" or lower(t.description) like ?").append(idx + 1).append(")");
|
query.append(" and (lower(t.title) like ?").append(idx).append(" or lower(t.description) like ?").append(idx + 1).append(")");
|
||||||
String pattern = "%" + text.toLowerCase() + "%";
|
String pattern = "%" + text.toLowerCase() + "%";
|
||||||
|
|
@ -92,8 +91,8 @@ public class TaskRepositoryImpl implements TaskRepository, PanacheRepository<Tas
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Task> tasks = find(query.toString(), params.toArray()).list().stream().map(TaskEntityMapper::toDomain).toList();
|
List<Task> tasks = find(query.toString(), params.toArray()).list().stream().map(TaskEntityMapper::toDomain).toList();
|
||||||
LOGGER.infof("Loaded %d accessible tasks for user %s in organization %d with filters (text=%s, projectIds=%s, statuses=%s, categoryIds=%s, userIds=%s)",
|
LOGGER.infof("Loaded %d accessible tasks in organization %d with filters (text=%s, projectIds=%s, statuses=%s, categoryIds=%s, userIds=%s)",
|
||||||
tasks.size(), email, organizationId, text, projectIds, statuses, categoryIds, userIds);
|
tasks.size(), organizationId, text, projectIds, statuses, categoryIds, userIds);
|
||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ public class ProjectController {
|
||||||
@Authenticated
|
@Authenticated
|
||||||
public List<ProjectResponse> listProjects(@PathParam("orgId") Long orgId) {
|
public List<ProjectResponse> listProjects(@PathParam("orgId") Long orgId) {
|
||||||
List<ProjectResponse> projects = listProjectsUseCase.execute(orgId).stream()
|
List<ProjectResponse> projects = listProjectsUseCase.execute(orgId).stream()
|
||||||
.map(p -> new ProjectResponse(p.getId(), p.getName()))
|
.map(p -> new ProjectResponse(p.getId(), p.getName(), p.getColor(), p.getIcon()))
|
||||||
.toList();
|
.toList();
|
||||||
LOGGER.infof("Listed %d projects for organization %d", projects.size(), orgId);
|
LOGGER.infof("Listed %d projects for organization %d", projects.size(), orgId);
|
||||||
return projects;
|
return projects;
|
||||||
|
|
@ -42,14 +42,14 @@ public class ProjectController {
|
||||||
@Authenticated
|
@Authenticated
|
||||||
public ProjectResponse createProject(@PathParam("orgId") Long orgId, CreateProjectRequest request) {
|
public ProjectResponse createProject(@PathParam("orgId") Long orgId, CreateProjectRequest request) {
|
||||||
LOGGER.infof("Project creation requested for organization %d with name '%s'", orgId, request.name());
|
LOGGER.infof("Project creation requested for organization %d with name '%s'", orgId, request.name());
|
||||||
Project project = createProjectUseCase.execute(orgId, request.name());
|
Project project = createProjectUseCase.execute(orgId, request.name(), request.color(), request.icon());
|
||||||
LOGGER.infof("Project %d created successfully for organization %d", project.getId(), orgId);
|
LOGGER.infof("Project %d created successfully for organization %d", project.getId(), orgId);
|
||||||
return new ProjectResponse(project.getId(), project.getName());
|
return new ProjectResponse(project.getId(), project.getName(), project.getColor(), project.getIcon());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO must be a independent file
|
// TODO must be a independent file
|
||||||
public record CreateProjectRequest(String name) {}
|
public record CreateProjectRequest(String name, String color, String icon) {}
|
||||||
|
|
||||||
// TODO must be a independent file
|
// TODO must be a independent file
|
||||||
public record ProjectResponse(Long id, String name) {}
|
public record ProjectResponse(Long id, String name, String color, String icon) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ public class CreateProjectUseCase {
|
||||||
this.organizationRepository = organizationRepository;
|
this.organizationRepository = organizationRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Project execute(Long organizationId, String projectName) {
|
public Project execute(Long organizationId, String projectName, String color, String icon) {
|
||||||
Organization organization = organizationRepository.findOrganizationById(organizationId)
|
Organization organization = organizationRepository.findOrganizationById(organizationId)
|
||||||
.orElseThrow(() -> {
|
.orElseThrow(() -> {
|
||||||
LOGGER.warnf("Project creation failed because organization %d was not found", organizationId);
|
LOGGER.warnf("Project creation failed because organization %d was not found", organizationId);
|
||||||
|
|
@ -26,6 +26,8 @@ public class CreateProjectUseCase {
|
||||||
LOGGER.infof("Creating project '%s' in organization %d", projectName, organizationId);
|
LOGGER.infof("Creating project '%s' in organization %d", projectName, organizationId);
|
||||||
Project project = new Project();
|
Project project = new Project();
|
||||||
project.setName(projectName);
|
project.setName(projectName);
|
||||||
|
project.setColor(color);
|
||||||
|
project.setIcon(icon);
|
||||||
project.setOrganization(organization);
|
project.setOrganization(organization);
|
||||||
|
|
||||||
Project savedProject = projectRepository.saveProject(project);
|
Project savedProject = projectRepository.saveProject(project);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ public interface TaskRepository {
|
||||||
Task saveTask(Task task);
|
Task saveTask(Task task);
|
||||||
Task findTaskById(Long id);
|
Task findTaskById(Long id);
|
||||||
List<Task> findAllTasks();
|
List<Task> findAllTasks();
|
||||||
List<Task> findAccessibleTasksFiltered(String email, Long organizationId, String text, List<Long> projectIds,
|
List<Task> findAccessibleTasksFiltered(Long organizationId, String text, List<Long> projectIds,
|
||||||
List<TaskStatus> statuses, List<Long> categoryIds, List<Long> userIds);
|
List<TaskStatus> statuses, List<Long> categoryIds, List<Long> userIds);
|
||||||
List<Task> findTasksWorkedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end);
|
List<Task> findTasksWorkedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end);
|
||||||
List<Task> findTasksFinishedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end);
|
List<Task> findTasksFinishedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end);
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ public class ListAllTasksUseCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<CreateTaskOutput> tasks = taskRepository.findAccessibleTasksFiltered(
|
List<CreateTaskOutput> tasks = taskRepository.findAccessibleTasksFiltered(
|
||||||
input.userEmail(),
|
|
||||||
input.organizationId(),
|
input.organizationId(),
|
||||||
input.text(),
|
input.text(),
|
||||||
input.projectIds(),
|
input.projectIds(),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE projects ADD COLUMN color VARCHAR(9);
|
||||||
|
ALTER TABLE projects ADD COLUMN icon VARCHAR(64);
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Filter, RefreshCw, Search } from 'lucide-react'
|
import { Filter, RefreshCw, Search } from 'lucide-react'
|
||||||
import { TASK_STATUS_LABELS, TASK_STATUS_OPTIONS } from '../types/task'
|
import { TASK_STATUS_LABELS, TASK_STATUS_OPTIONS, type TaskStatus } from '../types/task'
|
||||||
import type { Project } from '../services/projectService'
|
import type { Project } from '../services/projectService'
|
||||||
import type { Category } from '../types/category'
|
import type { Category } from '../types/category'
|
||||||
import { MultiSelectChips } from './MultiSelectChips'
|
import { MultiSelectChips } from './MultiSelectChips'
|
||||||
|
|
@ -9,6 +9,13 @@ import type { TaskFilters } from './taskFilters'
|
||||||
import type { TaskAssignee } from '../types/task'
|
import type { TaskAssignee } from '../types/task'
|
||||||
import { UserAvatar } from './UserAvatar'
|
import { UserAvatar } from './UserAvatar'
|
||||||
|
|
||||||
|
const STATUS_CHIP_COLORS: Record<TaskStatus, string> = {
|
||||||
|
NOT_STARTED: '#94a3b8',
|
||||||
|
IN_PROGRESS: '#f59e0b',
|
||||||
|
COMPLETED: '#10b981',
|
||||||
|
CANCELED: '#ef4444',
|
||||||
|
}
|
||||||
|
|
||||||
interface TaskFilterBarProps {
|
interface TaskFilterBarProps {
|
||||||
filters: TaskFilters
|
filters: TaskFilters
|
||||||
onFiltersChange: (filters: TaskFilters) => void
|
onFiltersChange: (filters: TaskFilters) => void
|
||||||
|
|
@ -59,26 +66,31 @@ export function TaskFilterBar({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
{projects.length > 0 && (
|
||||||
{projects.length > 0 && (
|
<div className="flex items-center gap-2">
|
||||||
<MultiSelectDropdown
|
<Filter size={16} className="shrink-0 text-text-muted" />
|
||||||
label="Projetos"
|
<span className="text-[10px] font-black uppercase tracking-widest text-text-muted whitespace-nowrap">Projetos</span>
|
||||||
allLabel="Todos os projetos"
|
<MultiSelectChips
|
||||||
options={projects.map((project) => ({
|
options={projects.map((p) => ({
|
||||||
value: project.id,
|
value: p.id,
|
||||||
label: project.name,
|
label: p.name,
|
||||||
|
color: p.color ?? undefined,
|
||||||
|
icon: p.icon ? <CategoryIcon iconName={p.icon} size={14} /> : undefined,
|
||||||
}))}
|
}))}
|
||||||
selected={filters.projectIds}
|
selected={filters.projectIds}
|
||||||
onChange={(projectIds) => update({ projectIds })}
|
onChange={(projectIds) => update({ projectIds })}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<MultiSelectDropdown
|
<div className="flex items-center gap-2">
|
||||||
label="Status"
|
<Filter size={16} className="shrink-0 text-text-muted" />
|
||||||
allLabel="Todos os status"
|
<span className="text-[10px] font-black uppercase tracking-widest text-text-muted whitespace-nowrap">Status</span>
|
||||||
options={TASK_STATUS_OPTIONS.map((status) => ({
|
<MultiSelectChips
|
||||||
value: status,
|
options={TASK_STATUS_OPTIONS.map((s) => ({
|
||||||
label: TASK_STATUS_LABELS[status],
|
value: s,
|
||||||
|
label: TASK_STATUS_LABELS[s],
|
||||||
|
color: STATUS_CHIP_COLORS[s],
|
||||||
}))}
|
}))}
|
||||||
selected={filters.statuses}
|
selected={filters.statuses}
|
||||||
onChange={(statuses) => update({ statuses })}
|
onChange={(statuses) => update({ statuses })}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { projectService, type Project } from "../services/projectService";
|
import { projectService, type Project } from "../services/projectService";
|
||||||
|
import { IconPicker } from "../components/IconPicker";
|
||||||
|
|
||||||
export const ProjectsPage: React.FC = () => {
|
export const ProjectsPage: React.FC = () => {
|
||||||
const { activeOrg } = useAuth();
|
const { activeOrg } = useAuth();
|
||||||
|
|
@ -21,6 +22,8 @@ export const ProjectsPage: React.FC = () => {
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||||
const [newProjectName, setNewProjectName] = useState("");
|
const [newProjectName, setNewProjectName] = useState("");
|
||||||
|
const [newProjectColor, setNewProjectColor] = useState("#6366f1");
|
||||||
|
const [newProjectIcon, setNewProjectIcon] = useState("Folder");
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -49,12 +52,15 @@ export const ProjectsPage: React.FC = () => {
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const newProject = await projectService.create(
|
const newProject = await projectService.create(activeOrg.id, {
|
||||||
activeOrg.id,
|
name: newProjectName,
|
||||||
newProjectName,
|
color: newProjectColor,
|
||||||
);
|
icon: newProjectIcon,
|
||||||
|
});
|
||||||
setProjects((prev) => [...prev, newProject]);
|
setProjects((prev) => [...prev, newProject]);
|
||||||
setNewProjectName("");
|
setNewProjectName("");
|
||||||
|
setNewProjectColor("#6366f1");
|
||||||
|
setNewProjectIcon("Folder");
|
||||||
setShowCreateForm(false);
|
setShowCreateForm(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to create project", err);
|
console.error("Failed to create project", err);
|
||||||
|
|
@ -311,6 +317,34 @@ export const ProjectsPage: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-bold text-text-main ml-1">
|
||||||
|
Cor
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={newProjectColor}
|
||||||
|
onChange={(e) => setNewProjectColor(e.target.value)}
|
||||||
|
className="h-14 w-14 cursor-pointer rounded-xl border border-border-soft"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newProjectColor}
|
||||||
|
onChange={(e) => setNewProjectColor(e.target.value)}
|
||||||
|
className="flex-1 px-4 py-3 bg-surface-muted/50 border border-border-soft rounded-xl text-text-main font-mono text-sm focus:outline-none focus:ring-4 focus:ring-brand/10 focus:border-brand transition-all"
|
||||||
|
placeholder="#6366f1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-bold text-text-main ml-1">
|
||||||
|
Ícone
|
||||||
|
</label>
|
||||||
|
<IconPicker value={newProjectIcon} onChange={setNewProjectIcon} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="p-4 bg-rose-500/10 border border-rose-500/20 rounded-2xl text-rose-500 text-xs font-bold flex items-center gap-3">
|
<div className="p-4 bg-rose-500/10 border border-rose-500/20 rounded-2xl text-rose-500 text-xs font-bold flex items-center gap-3">
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
|
|
@ -320,7 +354,7 @@ export const ProjectsPage: React.FC = () => {
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isCreating || !newProjectName.trim()}
|
disabled={isCreating || !newProjectName.trim() || !newProjectColor || !newProjectIcon}
|
||||||
className="w-full flex items-center justify-center gap-3 bg-brand hover:bg-brand-dark disabled:bg-brand/50 disabled:cursor-not-allowed text-white font-black py-4 px-6 rounded-2xl transition-all shadow-lg shadow-brand/20 active:scale-[0.98]"
|
className="w-full flex items-center justify-center gap-3 bg-brand hover:bg-brand-dark disabled:bg-brand/50 disabled:cursor-not-allowed text-white font-black py-4 px-6 rounded-2xl transition-all shadow-lg shadow-brand/20 active:scale-[0.98]"
|
||||||
>
|
>
|
||||||
{isCreating ? (
|
{isCreating ? (
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ import { Link, useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
import { taskService } from '../services/taskService'
|
import { taskService } from '../services/taskService'
|
||||||
import type { Project } from '../services/projectService'
|
import type { Project } from '../services/projectService'
|
||||||
|
import { projectService } from '../services/projectService'
|
||||||
import { categoryService } from '../services/categoryService'
|
import { categoryService } from '../services/categoryService'
|
||||||
|
import { organizationService } from '../services/organizationService'
|
||||||
import type { Category } from '../types/category'
|
import type { Category } from '../types/category'
|
||||||
import { cn } from '../lib/utils'
|
import { cn } from '../lib/utils'
|
||||||
import type { TaskAssignee, TaskResponse } from '../types/task'
|
import type { TaskAssignee, TaskResponse } from '../types/task'
|
||||||
|
|
@ -61,7 +63,12 @@ function mergeProjects(existing: Project[], tasks: TaskResponse[]) {
|
||||||
|
|
||||||
tasks.forEach((task) => {
|
tasks.forEach((task) => {
|
||||||
if (task.project) {
|
if (task.project) {
|
||||||
byId.set(task.project.id, { id: task.project.id, name: task.project.name })
|
byId.set(task.project.id, {
|
||||||
|
id: task.project.id,
|
||||||
|
name: task.project.name,
|
||||||
|
color: task.project.color,
|
||||||
|
icon: task.project.icon,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -120,11 +127,21 @@ export function TaskListPage() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeOrg) return
|
if (!activeOrg) return
|
||||||
setFilters(DEFAULT_TASK_FILTERS)
|
setFilters(DEFAULT_TASK_FILTERS)
|
||||||
setProjects([])
|
|
||||||
setAssignees([])
|
|
||||||
setCategories([])
|
setCategories([])
|
||||||
void loadTasks(DEFAULT_TASK_FILTERS)
|
void loadTasks(DEFAULT_TASK_FILTERS)
|
||||||
|
|
||||||
categoryService.listByOrganization(activeOrg.id).then(setCategories).catch(() => {})
|
categoryService.listByOrganization(activeOrg.id).then(setCategories).catch(() => {})
|
||||||
|
|
||||||
|
projectService.listByOrganization(activeOrg.id).then(setProjects).catch(() => {})
|
||||||
|
|
||||||
|
organizationService.listMembers(activeOrg.id).then((members) => {
|
||||||
|
setAssignees(members.map((m) => ({
|
||||||
|
id: m.userId,
|
||||||
|
name: m.userName,
|
||||||
|
email: m.userEmail,
|
||||||
|
photo: m.userPhoto ?? null,
|
||||||
|
})))
|
||||||
|
}).catch(() => {})
|
||||||
}, [activeOrg])
|
}, [activeOrg])
|
||||||
|
|
||||||
const handleFiltersChange = (newFilters: TaskFilters) => {
|
const handleFiltersChange = (newFilters: TaskFilters) => {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { apiFetch } from '../lib/api'
|
||||||
export interface Project {
|
export interface Project {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
|
color?: string | null
|
||||||
|
icon?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const projectService = {
|
export const projectService = {
|
||||||
|
|
@ -10,10 +12,10 @@ export const projectService = {
|
||||||
return apiFetch<Project[]>(`/organizations/${orgId}/projects`)
|
return apiFetch<Project[]>(`/organizations/${orgId}/projects`)
|
||||||
},
|
},
|
||||||
|
|
||||||
create: async (orgId: number, name: string): Promise<Project> => {
|
create: async (orgId: number, payload: { name: string; color: string; icon: string }): Promise<Project> => {
|
||||||
return apiFetch<Project>(`/organizations/${orgId}/projects`, {
|
return apiFetch<Project>(`/organizations/${orgId}/projects`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { name },
|
body: payload,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ export type TaskHistoryItem = {
|
||||||
export type TaskProject = {
|
export type TaskProject = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
|
color?: string | null
|
||||||
|
icon?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TaskCategory = {
|
export type TaskCategory = {
|
||||||
|
|
@ -46,7 +48,7 @@ export type TaskAssignee = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
photo: string
|
photo: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TaskResponse = {
|
export type TaskResponse = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue