feat: team assignment on tickets + My team's tickets view
Backend: - team_id column on tickets table - team_id filter in GET /tickets (resolves team members) - team_id in UpdateTicketSchema + PATCH handler - SetTeam transaction type Frontend: - Team selector in ticket detail properties sidebar - My team's tickets in sidebar (when user belongs to a team) - team_id passed through to API from ticket list page Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -198,12 +198,14 @@ function TicketWorkbenchContent() {
|
||||
}
|
||||
}
|
||||
|
||||
const routeTeamId = searchParams.get("team_id") ?? "";
|
||||
const [ticketsRes, queuesRes, usersRes, fieldsRes, lifecycleRes] = await Promise.all([
|
||||
getTickets({
|
||||
q: searchQuery.trim() || undefined,
|
||||
status: apiStatus || undefined,
|
||||
queue_id: activeQueue || apiQueue || undefined,
|
||||
owner_id: apiOwner || undefined,
|
||||
team_id: routeTeamId || undefined,
|
||||
custom_fields: Object.keys(customFieldFilters).length > 0 ? customFieldFilters : undefined,
|
||||
}),
|
||||
getQueues(),
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
getQueues,
|
||||
getLifecycles,
|
||||
getUsers,
|
||||
getTeams,
|
||||
getQueueCustomFields,
|
||||
previewTicket,
|
||||
updateTicket,
|
||||
@@ -37,6 +38,7 @@ import type {
|
||||
Transaction,
|
||||
Queue,
|
||||
Lifecycle,
|
||||
Team,
|
||||
User,
|
||||
QueueCustomField,
|
||||
PreviewResult,
|
||||
@@ -212,6 +214,7 @@ export default function TicketDetailPage({
|
||||
const [queue, setQueue] = useState<Queue | null>(null);
|
||||
const [lifecycles, setLifecycles] = useState<Lifecycle[]>([]);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [teams, setTeams] = useState<Team[]>([]);
|
||||
const [queueFields, setQueueFields] = useState<QueueCustomField[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -230,7 +233,7 @@ export default function TicketDetailPage({
|
||||
const [scripResults, setScripResults] = useState<UpdateResult["scrip_results"] | null>(null);
|
||||
const [editingSubject, setEditingSubject] = useState(false);
|
||||
const [subjectDraft, setSubjectDraft] = useState("");
|
||||
const [fieldSaving, setFieldSaving] = useState<"subject" | "owner" | null>(null);
|
||||
const [fieldSaving, setFieldSaving] = useState<"subject" | "owner" | "team" | null>(null);
|
||||
const [customFieldDrafts, setCustomFieldDrafts] = useState<Record<string, string>>({});
|
||||
const [customFieldSaving, setCustomFieldSaving] = useState<string | null>(null);
|
||||
const [editingFieldId, setEditingFieldId] = useState<string | null>(null);
|
||||
@@ -239,12 +242,13 @@ export default function TicketDetailPage({
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const [ticketRes, txRes, queuesRes, lifecycleRes, usersRes] = await Promise.all([
|
||||
const [ticketRes, txRes, queuesRes, lifecycleRes, usersRes, teamsRes] = await Promise.all([
|
||||
getTicket(id),
|
||||
getTicketTransactions(id),
|
||||
getQueues(),
|
||||
getLifecycles(),
|
||||
getUsers(),
|
||||
getTeams(),
|
||||
]);
|
||||
|
||||
if (ticketRes.error) {
|
||||
@@ -290,6 +294,7 @@ export default function TicketDetailPage({
|
||||
setError((prev) => prev || usersRes.error);
|
||||
} else {
|
||||
setUsers(usersRes.data ?? []);
|
||||
setTeams(teamsRes.data ?? []);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
@@ -379,6 +384,29 @@ export default function TicketDetailPage({
|
||||
}
|
||||
};
|
||||
|
||||
const handleTeamChange = async (teamId: string) => {
|
||||
if (!ticket || fieldSaving) return;
|
||||
const nextTeamId = teamId || null;
|
||||
if (nextTeamId === ticket.team_id) return;
|
||||
|
||||
setFieldSaving("team");
|
||||
setFieldError(null);
|
||||
|
||||
const { data, error } = await updateTicket(id, { team_id: nextTeamId });
|
||||
|
||||
setFieldSaving(null);
|
||||
|
||||
if (error) {
|
||||
setFieldError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
setTicket(data.ticket);
|
||||
await refreshTransactions();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOwnerChange = async (ownerId: string) => {
|
||||
if (!ticket || fieldSaving) return;
|
||||
const nextOwnerId = ownerId || null;
|
||||
@@ -909,6 +937,25 @@ export default function TicketDetailPage({
|
||||
</select>
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[92px_minmax(0,1fr)] gap-3 border-b border-border px-3 py-2.5">
|
||||
<dt className="text-[11px] font-semibold uppercase text-muted-foreground">Team</dt>
|
||||
<dd className="min-w-0">
|
||||
<select
|
||||
value={ticket.team_id ?? ""}
|
||||
onChange={(event) => void handleTeamChange(event.target.value)}
|
||||
disabled={fieldSaving === "team"}
|
||||
className="h-8 w-full rounded-md border border-input bg-card px-2 text-sm text-foreground outline-none transition-colors focus:border-ring disabled:opacity-60"
|
||||
aria-label="Team"
|
||||
>
|
||||
<option value="">No team</option>
|
||||
{teams.map((team) => (
|
||||
<option key={team.id} value={team.id}>
|
||||
{team.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</dd>
|
||||
</div>
|
||||
<PropertyRow label="Priority" value="Not set" />
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user