This commit is contained in:
michael committed 2026-09-15 09:51:30 +00:00
1 parent b7f7d7e793
commit c01d747352
14 files changed
+987 -294

No files matched your search

+138 -6
View File
@@ -47,6 +47,16 @@ import {
} from "../api";
import { currentDay, currentMonth, formatDate, formatMinutes } from "../format";
import { ticketPattern } from "../timers";
import {
matchesZammadPriorityFilter,
matchesZammadStateFilter,
ZammadPriorityBadge,
zammadPriorityFilterOptions,
zammadPrioritySortRank,
ZammadStateBadge,
zammadStateFilterOptions,
zammadStateSortRank
} from "../zammad-meta";
import type {
DashboardFavorite,
DashboardPreference,
@@ -161,7 +171,27 @@ function widgetAllowedFilters(type: DashboardWidgetType) {
}
if (type === "projectBudgets") {
return ["budgeted", "withoutBudget", "critical90", "overBudget", "open", "closed", "openTodos", "overdueTodos", "fixedBilling", "manualBilling"];
return [
"budgeted",
"withoutBudget",
"critical90",
"overBudget",
"open",
"closed",
"zammadState:2",
"zammadState:3",
"zammadState:4",
"zammadState:7",
"zammadState:none",
"zammadPriority:1",
"zammadPriority:2",
"zammadPriority:3",
"zammadPriority:none",
"openTodos",
"overdueTodos",
"fixedBilling",
"manualBilling"
];
}
if (type === "openRatings") {
@@ -169,7 +199,23 @@ function widgetAllowedFilters(type: DashboardWidgetType) {
}
if (type === "favorites") {
return ["projects", "openTodos", "overdueTodos", "missingMeta", "fixedBilling", "manualBilling"];
return [
"projects",
"openTodos",
"overdueTodos",
"zammadState:2",
"zammadState:3",
"zammadState:4",
"zammadState:7",
"zammadState:none",
"zammadPriority:1",
"zammadPriority:2",
"zammadPriority:3",
"zammadPriority:none",
"missingMeta",
"fixedBilling",
"manualBilling"
];
}
return [];
@@ -498,6 +544,14 @@ function projectFilterGroup(filter: string) {
return "status";
}
if (filter.startsWith("zammadState:")) {
return "zammadState";
}
if (filter.startsWith("zammadPriority:")) {
return "zammadPriority";
}
if (filter === "openTodos" || filter === "overdueTodos") {
return "todos";
}
@@ -547,6 +601,14 @@ function matchesSingleProjectFilter(project: ProjectSummary, filterBy: string, f
return Boolean(project.closed_at);
}
if (filterBy.startsWith("zammadState:")) {
return matchesZammadStateFilter(project.zammad_state_id, filterBy.slice("zammadState:".length) as Parameters<typeof matchesZammadStateFilter>[1]);
}
if (filterBy.startsWith("zammadPriority:")) {
return matchesZammadPriorityFilter(project.zammad_priority_id, filterBy.slice("zammadPriority:".length) as Parameters<typeof matchesZammadPriorityFilter>[1]);
}
if (filterBy === "openTodos") {
return Number(project.open_todos ?? 0) > 0;
}
@@ -608,11 +670,19 @@ function favoriteFilterGroup(filter: string) {
return "todos";
}
if (filter.startsWith("zammadState:")) {
return "zammadState";
}
if (filter.startsWith("zammadPriority:")) {
return "zammadPriority";
}
return filter;
}
function sortedFavorites(favorites: DashboardFavorite[], metas: Record<string, TicketMeta | null>, config: Record<string, unknown>) {
const sortBy = config.sortBy === "ticket" || config.sortBy === "title" || config.sortBy === "recent" ? config.sortBy : "custom";
const sortBy = config.sortBy === "ticket" || config.sortBy === "title" || config.sortBy === "recent" || config.sortBy === "zammadStatus" || config.sortBy === "zammadPriority" ? config.sortBy : "custom";
const direction: DashboardSortDirection = config.sortDirection === "desc" ? "desc" : "asc";
const factor = directionFactor(direction);
@@ -629,6 +699,14 @@ function sortedFavorites(favorites: DashboardFavorite[], metas: Record<string, T
return compareOptionalTimestamp(left.addedAt, right.addedAt, direction) || compareText(left.ticketNumber, right.ticketNumber);
}
if (sortBy === "zammadStatus") {
return (compareNumber(zammadStateSortRank(metas[left.ticketNumber]?.zammad_state_id), zammadStateSortRank(metas[right.ticketNumber]?.zammad_state_id)) * factor) || compareText(left.ticketNumber, right.ticketNumber);
}
if (sortBy === "zammadPriority") {
return (compareNumber(zammadPrioritySortRank(metas[left.ticketNumber]?.zammad_priority_id), zammadPrioritySortRank(metas[right.ticketNumber]?.zammad_priority_id)) * factor) || compareText(left.ticketNumber, right.ticketNumber);
}
return (compareNumber(left.sortOrder, right.sortOrder) * factor) || compareText(left.ticketNumber, right.ticketNumber);
});
}
@@ -1568,12 +1646,34 @@ function WidgetSettings({
function ProjectBudgetWidget({ widget, editing, projects, stats, onNavigate, onUpdateConfig }: WidgetContentProps) {
const filterValues = readFilterValues(
widget.config,
["budgeted", "withoutBudget", "critical90", "overBudget", "open", "closed", "openTodos", "overdueTodos", "fixedBilling", "manualBilling"],
[
"budgeted",
"withoutBudget",
"critical90",
"overBudget",
"open",
"closed",
"zammadState:2",
"zammadState:3",
"zammadState:4",
"zammadState:7",
"zammadState:none",
"zammadPriority:1",
"zammadPriority:2",
"zammadPriority:3",
"zammadPriority:none",
"openTodos",
"overdueTodos",
"fixedBilling",
"manualBilling"
],
["budgeted"]
);
const sortBy =
widget.config?.sortBy === "consumption" ||
widget.config?.sortBy === "consumptionPercent" ||
widget.config?.sortBy === "zammadStatus" ||
widget.config?.sortBy === "zammadPriority" ||
widget.config?.sortBy === "title" ||
widget.config?.sortBy === "ticket" ||
widget.config?.sortBy === "organization" ||
@@ -1598,6 +1698,14 @@ function ProjectBudgetWidget({ widget, editing, projects, stats, onNavigate, onU
return (compareText(left.organization_name || left.customer_name, right.organization_name || right.customer_name) * factor) || compareText(left.ticket_number, right.ticket_number);
}
if (sortBy === "zammadStatus") {
return (compareNumber(zammadStateSortRank(left.zammad_state_id), zammadStateSortRank(right.zammad_state_id)) * factor) || compareText(left.ticket_number, right.ticket_number);
}
if (sortBy === "zammadPriority") {
return (compareNumber(zammadPrioritySortRank(left.zammad_priority_id), zammadPrioritySortRank(right.zammad_priority_id)) * factor) || compareText(left.ticket_number, right.ticket_number);
}
if (sortBy === "consumption") {
return (compareNumber(left.consumption_minutes ?? 0, right.consumption_minutes ?? 0) * factor) || compareText(left.ticket_number, right.ticket_number);
}
@@ -1640,6 +1748,8 @@ function ProjectBudgetWidget({ widget, editing, projects, stats, onNavigate, onU
{ value: "withoutBudget", label: "Ohne Kontingent" },
{ value: "open", label: "Offene Projekte" },
{ value: "closed", label: "Geschlossene Projekte" },
...zammadStateFilterOptions.filter((option) => option.value !== "all").map((option) => ({ value: `zammadState:${option.value}`, label: `Status: ${option.label}` })),
...zammadPriorityFilterOptions.filter((option) => option.value !== "all").map((option) => ({ value: `zammadPriority:${option.value}`, label: `Prio: ${option.label}` })),
{ value: "openTodos", label: "Mit offenen ToDos" },
{ value: "overdueTodos", label: "Mit überfälligen ToDos" },
{ value: "fixedBilling", label: "Fixe Abrechnungen" },
@@ -1655,7 +1765,9 @@ function ProjectBudgetWidget({ widget, editing, projects, stats, onNavigate, onU
{ value: "todos", label: "ToDos" },
{ value: "title", label: "Titel" },
{ value: "ticket", label: "Ticket" },
{ value: "organization", label: "Organisation" }
{ value: "organization", label: "Organisation" },
{ value: "zammadStatus", label: "Zammad-Status" },
{ value: "zammadPriority", label: "Zammad-Prio" }
]}
onChange={onUpdateConfig}
/>
@@ -1677,6 +1789,10 @@ function ProjectBudgetWidget({ widget, editing, projects, stats, onNavigate, onU
<span className="block truncate text-[11px] text-muted-foreground">
{project.ticket_number} · {hasMultipleTeamspaceEntries ? `${entries.length} Teamspace · ` : ""}{budgetMinutes > 0 ? `${decimalHours(consumptionMinutes)} / ${decimalHours(budgetMinutes)} · ${percent ?? 0}%` : "ohne Kontingent"}
</span>
<span className="mt-1 flex flex-wrap gap-1">
<ZammadStateBadge stateId={project.zammad_state_id} />
<ZammadPriorityBadge priorityId={project.zammad_priority_id} />
</span>
{percent !== null ? (
<span className="mt-1 block h-1.5 overflow-hidden rounded-full bg-muted" title={`${percent}% Verbrauch`} aria-label={`${percent}% Verbrauch`}>
<span className={cn("block h-full rounded-full", consumptionBarClass(percent))} style={{ width: `${Math.min(100, percent)}%` }} />
@@ -2196,6 +2312,14 @@ function FavoritesWidget({
return todos.some((todo) => todo.ticket_number === favorite.ticketNumber && todo.status === "open" && todo.ends_on && todo.ends_on < today);
}
if (filter.startsWith("zammadState:")) {
return matchesZammadStateFilter(meta?.zammad_state_id, filter.slice("zammadState:".length) as Parameters<typeof matchesZammadStateFilter>[1]);
}
if (filter.startsWith("zammadPriority:")) {
return matchesZammadPriorityFilter(meta?.zammad_priority_id, filter.slice("zammadPriority:".length) as Parameters<typeof matchesZammadPriorityFilter>[1]);
}
if (filter === "missingMeta") {
return favoriteMetas[favorite.ticketNumber] === null;
}
@@ -2233,6 +2357,8 @@ function FavoritesWidget({
{ value: "projects", label: "Nur Projekte" },
{ value: "openTodos", label: "Mit offenen ToDos" },
{ value: "overdueTodos", label: "Mit überfälligen ToDos" },
...zammadStateFilterOptions.filter((option) => option.value !== "all").map((option) => ({ value: `zammadState:${option.value}`, label: `Status: ${option.label}` })),
...zammadPriorityFilterOptions.filter((option) => option.value !== "all").map((option) => ({ value: `zammadPriority:${option.value}`, label: `Prio: ${option.label}` })),
{ value: "missingMeta", label: "Nicht gefunden" },
{ value: "fixedBilling", label: "Fixe Abrechnungen" },
{ value: "manualBilling", label: "Ohne fixe Abrechnung" }
@@ -2243,7 +2369,9 @@ function FavoritesWidget({
{ value: "custom", label: "Frei" },
{ value: "ticket", label: "Ticket" },
{ value: "title", label: "Titel" },
{ value: "recent", label: "Neu" }
{ value: "recent", label: "Neu" },
{ value: "zammadStatus", label: "Zammad-Status" },
{ value: "zammadPriority", label: "Zammad-Prio" }
]}
onChange={onUpdateConfig}
/>
@@ -2261,6 +2389,10 @@ function FavoritesWidget({
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => meta?.id && onNavigate(`/tickets/${meta.id}?month=${currentMonth()}`)}>
<span className="block truncate text-xs font-medium">{favorite.ticketNumber}</span>
<span className="block truncate text-[11px] text-muted-foreground">{meta?.title || meta?.organization_name || "Favorit"}</span>
<span className="mt-1 flex flex-wrap gap-1">
<ZammadStateBadge stateId={meta?.zammad_state_id} />
<ZammadPriorityBadge priorityId={meta?.zammad_priority_id} />
</span>
</button>
{editing && sortBy === "custom" ? (
<>