ui-nyla/src/api/redmineApi.ts
ValueOn AG 7eb305f910
Some checks failed
Deploy Nyla Frontend to Integration / deploy (push) Failing after 56s
cp adapted to 2026 poweron
2026-06-09 09:53:38 +02:00

400 lines
11 KiB
TypeScript

// Copyright (c) 2026 PowerOn AG
// All rights reserved.
/**
* Redmine API
*
* Frontend client for the Redmine feature backend.
* URL pattern: /api/redmine/{instanceId}/...
*/
import { ApiRequestOptions } from '../hooks/useApi';
// ============================================================================
// Types -- mirror gateway/modules/features/redmine/datamodelRedmine.py
// ============================================================================
export interface RedmineConfigDto {
id?: string;
featureInstanceId: string;
mandateId?: string | null;
baseUrl: string;
projectId: string;
hasApiKey: boolean;
rootTrackerName: string;
defaultPeriodValue?: Record<string, any> | null;
schemaCacheTtlSeconds: number;
schemaCachedAt?: number | null;
isActive: boolean;
lastConnectedAt?: number | null;
lastSyncAt?: number | null;
lastFullSyncAt?: number | null;
lastSyncTicketCount?: number | null;
lastSyncErrorMessage?: string | null;
}
export interface RedmineConfigUpdateRequest {
baseUrl?: string;
projectId?: string;
apiKey?: string;
rootTrackerName?: string;
defaultPeriodValue?: Record<string, any> | null;
schemaCacheTtlSeconds?: number;
isActive?: boolean;
}
export interface RedmineFieldChoice {
id: number;
name: string;
isClosed?: boolean | null;
}
export interface RedmineCustomFieldSchema {
id: number;
name: string;
fieldFormat: string;
isRequired: boolean;
possibleValues: string[];
multiple: boolean;
defaultValue?: string | null;
}
export interface RedmineFieldSchema {
projectId: string;
projectName: string;
trackers: RedmineFieldChoice[];
statuses: RedmineFieldChoice[];
priorities: RedmineFieldChoice[];
users: RedmineFieldChoice[];
categories: RedmineFieldChoice[];
customFields: RedmineCustomFieldSchema[];
rootTrackerName: string;
rootTrackerId: number | null;
}
export interface RedmineRelation {
id: number;
issueId: number;
issueToId: number;
relationType: string;
delay?: number | null;
}
export interface RedmineCustomFieldValue {
id: number;
name: string;
value: any;
}
export interface RedmineTicket {
id: number;
subject: string;
description: string;
trackerId?: number | null;
trackerName?: string | null;
statusId?: number | null;
statusName?: string | null;
isClosed: boolean;
priorityId?: number | null;
priorityName?: string | null;
assignedToId?: number | null;
assignedToName?: string | null;
authorId?: number | null;
authorName?: string | null;
parentId?: number | null;
fixedVersionId?: number | null;
fixedVersionName?: string | null;
categoryId?: number | null;
categoryName?: string | null;
createdOn?: string | null;
updatedOn?: string | null;
customFields: RedmineCustomFieldValue[];
relations: RedmineRelation[];
}
export interface RedmineSyncResult {
instanceId: string;
full: boolean;
ticketsUpserted: number;
relationsUpserted: number;
durationMs: number;
lastSyncAt: number;
error?: string | null;
}
export interface RedmineSyncStatus {
instanceId: string;
lastSyncAt?: number | null;
lastFullSyncAt?: number | null;
lastSyncDurationMs?: number | null;
lastSyncTicketCount?: number | null;
lastSyncErrorAt?: number | null;
lastSyncErrorMessage?: string | null;
mirroredTicketCount: number;
mirroredRelationCount: number;
}
export interface RedmineConnectionTestResult {
ok: boolean;
reason?: string;
message?: string;
status?: number;
user?: { id: number; name: string };
project?: { id: number; name: string };
}
export interface RedmineStats {
instanceId: string;
dateFrom?: string | null;
dateTo?: string | null;
bucket: string;
trackerIds: number[];
categoryIds: number[];
statusFilter: string;
kpis: {
total: number;
open: number;
closed: number;
closedInPeriod: number;
createdInPeriod: number;
orphans: number;
};
statusByTracker: Array<{
trackerId?: number | null;
trackerName: string;
countsByStatus: Record<string, number>;
total: number;
}>;
throughput: Array<{
bucketKey: string;
label: string;
created: number;
closed: number;
cumTotal: number;
cumOpen: number;
}>;
topAssignees: Array<{
assignedToId?: number | null;
name: string;
open: number;
}>;
relationDistribution: Array<{ relationType: string; count: number }>;
backlogAging: Array<{
bucketKey: string;
label: string;
minDays: number;
maxDays?: number | null;
count: number;
}>;
}
export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<any>;
const _baseUrl = (instanceId: string): string => `/api/redmine/${instanceId}`;
// ============================================================================
// Config
// ============================================================================
export async function getRedmineConfigApi(
request: ApiRequestFunction,
instanceId: string,
): Promise<RedmineConfigDto> {
return await request({ url: `${_baseUrl(instanceId)}/config`, method: 'get' });
}
export async function updateRedmineConfigApi(
request: ApiRequestFunction,
instanceId: string,
body: RedmineConfigUpdateRequest,
): Promise<RedmineConfigDto> {
return await request({ url: `${_baseUrl(instanceId)}/config`, method: 'put', data: body });
}
export async function deleteRedmineConfigApi(
request: ApiRequestFunction,
instanceId: string,
): Promise<{ deleted: boolean }> {
return await request({ url: `${_baseUrl(instanceId)}/config`, method: 'delete' });
}
export async function testRedmineConnectionApi(
request: ApiRequestFunction,
instanceId: string,
): Promise<RedmineConnectionTestResult> {
return await request({ url: `${_baseUrl(instanceId)}/config/test`, method: 'post' });
}
// ============================================================================
// Schema
// ============================================================================
export async function getRedmineSchemaApi(
request: ApiRequestFunction,
instanceId: string,
forceRefresh = false,
): Promise<RedmineFieldSchema> {
return await request({
url: `${_baseUrl(instanceId)}/schema`,
method: 'get',
params: forceRefresh ? { forceRefresh: true } : undefined,
});
}
// ============================================================================
// Sync
// ============================================================================
export async function runRedmineSyncApi(
request: ApiRequestFunction,
instanceId: string,
force = false,
): Promise<RedmineSyncResult> {
return await request({
url: `${_baseUrl(instanceId)}/sync`,
method: 'post',
params: force ? { force: true } : undefined,
});
}
export async function getRedmineSyncStatusApi(
request: ApiRequestFunction,
instanceId: string,
): Promise<RedmineSyncStatus> {
return await request({ url: `${_baseUrl(instanceId)}/sync/status`, method: 'get' });
}
// ============================================================================
// Tickets
// ============================================================================
export interface ListTicketsParams {
trackerIds?: number[];
status?: 'open' | 'closed' | '*';
dateFrom?: string;
dateTo?: string;
assignedToId?: number;
}
export async function listRedmineTicketsApi(
request: ApiRequestFunction,
instanceId: string,
params: ListTicketsParams = {},
): Promise<RedmineTicket[]> {
const queryParams: Record<string, any> = {};
if (params.status) queryParams.status = params.status;
if (params.dateFrom) queryParams.dateFrom = params.dateFrom;
if (params.dateTo) queryParams.dateTo = params.dateTo;
if (params.assignedToId !== undefined) queryParams.assignedToId = params.assignedToId;
if (params.trackerIds && params.trackerIds.length > 0) queryParams.trackerIds = params.trackerIds;
return await request({
url: `${_baseUrl(instanceId)}/tickets`,
method: 'get',
params: queryParams,
});
}
export async function getRedmineTicketApi(
request: ApiRequestFunction,
instanceId: string,
issueId: number,
): Promise<RedmineTicket> {
return await request({
url: `${_baseUrl(instanceId)}/tickets/${issueId}`,
method: 'get',
});
}
export interface RedmineTicketUpdateBody {
subject?: string;
description?: string;
trackerId?: number;
statusId?: number;
priorityId?: number;
assignedToId?: number;
parentIssueId?: number;
fixedVersionId?: number;
notes?: string;
customFields?: Record<number, any>;
}
export async function updateRedmineTicketApi(
request: ApiRequestFunction,
instanceId: string,
issueId: number,
body: RedmineTicketUpdateBody,
): Promise<RedmineTicket> {
return await request({
url: `${_baseUrl(instanceId)}/tickets/${issueId}`,
method: 'put',
data: body,
});
}
export interface RedmineTicketCreateBody {
subject: string;
trackerId: number;
description?: string;
statusId?: number;
priorityId?: number;
assignedToId?: number;
parentIssueId?: number;
fixedVersionId?: number;
customFields?: Record<number, any>;
}
export async function createRedmineTicketApi(
request: ApiRequestFunction,
instanceId: string,
body: RedmineTicketCreateBody,
): Promise<RedmineTicket> {
return await request({
url: `${_baseUrl(instanceId)}/tickets`,
method: 'post',
data: body,
});
}
export async function deleteRedmineTicketApi(
request: ApiRequestFunction,
instanceId: string,
issueId: number,
fallbackStatusId?: number,
): Promise<{ deleted: boolean; archived: boolean; statusId: number | null }> {
return await request({
url: `${_baseUrl(instanceId)}/tickets/${issueId}`,
method: 'delete',
params: fallbackStatusId !== undefined ? { fallbackStatusId } : undefined,
});
}
// ============================================================================
// Stats
// ============================================================================
export interface RedmineStatsParams {
dateFrom?: string;
dateTo?: string;
bucket?: 'day' | 'week' | 'month';
trackerIds?: number[];
categoryIds?: number[];
statusFilter?: '*' | 'open' | 'closed';
}
export async function getRedmineStatsApi(
request: ApiRequestFunction,
instanceId: string,
params: RedmineStatsParams = {},
): Promise<RedmineStats> {
const queryParams: Record<string, any> = {};
if (params.dateFrom) queryParams.dateFrom = params.dateFrom;
if (params.dateTo) queryParams.dateTo = params.dateTo;
if (params.bucket) queryParams.bucket = params.bucket;
if (params.trackerIds && params.trackerIds.length > 0) queryParams.trackerIds = params.trackerIds;
if (params.categoryIds && params.categoryIds.length > 0) queryParams.categoryIds = params.categoryIds;
if (params.statusFilter && params.statusFilter !== '*') queryParams.statusFilter = params.statusFilter;
return await request({
url: `${_baseUrl(instanceId)}/stats`,
method: 'get',
params: queryParams,
});
}