Some checks failed
Deploy Nyla Frontend to Integration / deploy (push) Failing after 56s
400 lines
11 KiB
TypeScript
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,
|
|
});
|
|
}
|