diff --git a/web-v2/src/lib/api.ts b/web-v2/src/lib/api.ts new file mode 100644 index 0000000..d4885d2 --- /dev/null +++ b/web-v2/src/lib/api.ts @@ -0,0 +1,3131 @@ +const API_BASE = '/api/v3'; + +export class ApiError extends Error { + constructor( + public readonly status: number, + public readonly code: string, + message: string, + public readonly requestId?: string, + ) { + super(message); + this.name = 'ApiError'; + } +} + +export interface AuthUser { + id: string; + username: string; + firstName: string; + lastName: string; + email: string | null; + mustChangePassword: boolean; + roles: string[]; + permissions: string[]; +} + +export interface AuthResponse { + user: AuthUser; + csrfToken: string; + accessExpiresInSeconds: number; +} + +export interface HealthResponse { + status: string; + service: string; + api: string; + version: string; + phase: string; + database: string; + time: string; + latencyMs: number; +} + +export interface DashboardSummary { + counts: { + totalAssets: number; + assetsNeedValidation: number; + assetsWithoutGeometry: number; + plannedInspections: number; + activeUsers: number; + inactiveUsers: number; + activeSessions: number; + openFindings: number; + awaitingCompanyResponse: number; + overdueCompanyResponses: number; + companyResponsesDueNext7Days: number; + awaitingVerificationSchedule: number; + overdueControls: number; + controlsNext30Days: number; + }; + recentAudit: Array<{ + id: string; + occurredAt: string; + actorUsername: string | null; + action: string; + entityType: string | null; + entityId: string | null; + }>; + upcomingControls: Array<{ + id: string; + code: string; + title: string; + nextControlOn: string; + actId: string; + actCode: string; + visitId: string; + visitCode: string; + assetId: string; + assetCode: string; + assetName: string; + }>; + generatedAt: string; +} + +export interface RoleSummary { id: string; code: string; name: string } + +export interface AdministrativeUser { + id: string; + username: string; + email: string | null; + firstName: string; + lastName: string; + status: 'ACTIVE' | 'INACTIVE'; + mustChangePassword: boolean; + failedLoginAttempts: number; + lockedUntil: string | null; + lastLoginAt: string | null; + passwordChangedAt: string | null; + createdAt: string; + updatedAt: string; + roles: RoleSummary[]; +} + +export interface Permission { id: string; code: string; description: string } + +export interface AdministrativeRole { + id: string; + code: string; + name: string; + description: string; + isSystem: boolean; + createdAt: string; + updatedAt: string; + userCount: number; + permissions: Permission[]; +} + +export interface AuditEventSummary { + id: string; + occurredAt: string; + actorUserId: string | null; + actorUsername: string | null; + action: string; + entityType: string | null; + entityId: string | null; + requestId: string | null; + source: 'WEB' | 'ANDROID' | 'SYSTEM' | 'IMPORT'; + ip: string | null; + hasDetails: boolean; +} + +export interface AuditEventDetail extends AuditEventSummary { + userAgent: string | null; + beforeData: Record | null; + afterData: Record | null; + metadata: Record | null; +} + +export interface PageMeta { page: number; pageSize: number; total: number; totalPages: number } +interface Page { data: T[]; meta: PageMeta } + +export type AssetInformationStatus = + | 'DRAFT' | 'PENDING_SURVEY' | 'SURVEYED' | 'VALIDATED' + | 'OBSERVED' | 'OUTDATED' | 'INACTIVE'; + +export type AssetOperationalStatus = + | 'UNKNOWN' | 'IN_SERVICE' | 'TEMPORARILY_OUT_OF_SERVICE' + | 'OUT_OF_SERVICE' | 'DECOMMISSIONED' | 'ABANDONED'; + +export type AssetDataOrigin = + | 'MANUAL' | 'FIELD_SURVEY' | 'PROVIDED_DOCUMENT' | 'IMPORT' | 'SYSTEM'; + +export type AssetTypeOperationalRole = 'GENERIC' | 'AREA' | 'COMPANY'; + +export interface AssetProvenance { + assetId: string; + origin: AssetDataOrigin; + sourceName: string | null; + sourceReference: string | null; + observedAt: string | null; + notes: string | null; + verifiedAt: string | null; + verifiedBy: string | null; + verifiedByUsername: string | null; + updatedAt: string; + updatedBy: string | null; + updatedByUsername: string | null; +} + +export type AssetAttributeDataType = + | 'TEXT' | 'NUMBER' | 'BOOLEAN' | 'DATE' | 'DATETIME' | 'SELECT'; + +export interface AssetAttributeDefinition { + id: string; + code: string; + name: string; + dataType: AssetAttributeDataType; + isRequired: boolean; + isActive: boolean; + unit: string | null; + options: string[] | null; + sortOrder: number; +} + +export interface AssetTypeSummary { + id: string; + code: string; + name: string; + isActive: boolean; + operationalRole: AssetTypeOperationalRole; +} + +export interface AssetType extends AssetTypeSummary { + description: string; + canBeRoot: boolean; + createdAt: string; + updatedAt: string; + assetCount: number; + allowedParentTypes: AssetTypeSummary[]; + attributes: AssetAttributeDefinition[]; +} + +export interface MasterBootstrapStatus { + presetCode: string; + presetName: string; + typeCount: number; + canApply: boolean; + reason: string | null; + types: Array<{ + code: string; + name: string; + operationalRole: AssetTypeOperationalRole; + parentCodes: string[]; + attributeCount: number; + }>; +} + +export interface MasterBootstrapResult { + presetCode: string; + presetName: string; + createdTypeCount: number; + createdAttributeCount: number; + createdParentRuleCount: number; + data: AssetType[]; +} + +export interface MasterEnrichmentStatus { + presetCode: string; + presetName: string; + typeCount: number; + canApply: boolean; + complete: boolean; + reason: string | null; + missingTypeCodes: string[]; + missingAttributeCount: number; + missingParentRuleCount: number; +} + +export interface AssetListItem { + id: string; + code: string; + name: string; + commonName: string | null; + type: { id: string; code: string; name: string }; + parent: { id: string; code: string; name: string } | null; + operationalArea: { id: string; code: string; name: string } | null; + operatorCompany: { id: string; code: string; name: string } | null; + informationStatus: AssetInformationStatus; + operationalStatus: AssetOperationalStatus; + childrenCount: number; + hasGeometry: boolean; + geometryType: AssetGeometryType | null; + mediaCount: number; + dataOrigin: AssetDataOrigin; + provenanceVerified: boolean; + currentVersion: number; + updatedAt: string; +} + +export interface AssetLineageItem { + id: string; + code: string; + name: string; + commonName: string | null; + type: { id: string; code: string; name: string }; +} + +export interface AssetDetail extends AssetListItem { + description: string | null; + createdAt: string; + createdBy: string | null; + updatedBy: string | null; + attributes: Array<{ + definitionId: string; + code: string; + name: string; + dataType: AssetAttributeDataType; + isRequired: boolean; + unit: string | null; + options: string[] | null; + sortOrder: number; + value: unknown; + }>; +} + +export type AssetDossierTimelineKind = + | 'INVENTORY_CHANGE' | 'INSPECTION' | 'ACT' | 'FINDING' | 'FINDING_CLOSED' + | 'COMMUNICATION' | 'PHOTO' | 'DOCUMENT' | 'REPORT' | 'SOURCE_DOCUMENT' | 'VERIFICATION'; + +export interface AssetDossierTimelineEvent { + id: string; + kind: AssetDossierTimelineKind; + occurredAt: string; + title: string; + description: string | null; + href?: string; + meta?: Record; +} + +export interface AssetDossier { + asset: { id: string; code: string; name: string; commonName: string | null }; + counters: { + inspections: number; acts: number; findings: number; openFindings: number; closedFindings: number; + evidence: number; documents: number; photos: number; reports: number; verifications: number; + }; + visits: Array<{ id: string; code: string; title: string; status: InspectionVisitStatus; plannedStartAt: string | null; actualStartedAt: string | null; actualClosedAt: string | null; createdAt: string }>; + acts: Array<{ id: string; visitId: string; code: string; status: InspectionActStatus; occurredAt: string; title: string; summary: string; closedAt: string | null; currentVersion: number; visitCode: string; visitTitle: string }>; + findings: Array<{ id: string; actId: string; code: string; status: InspectionFindingStatus; title: string; description: string; correctionDueOn: string | null; companyResponseReceivedOn: string | null; nextControlOn: string | null; closedAt: string | null; closureNotes: string | null; createdAt: string; updatedAt: string; actCode: string; actOccurredAt: string; visitId: string; visitCode: string; visitTitle: string }>; + evidence: Array<{ id: string; findingId: string; communicationId: string | null; kind: InspectionEvidenceKind; purpose: InspectionEvidencePurpose; originalName: string; title: string | null; description: string | null; capturedAt: string | null; createdAt: string; findingCode: string; findingTitle: string }>; + communications: Array<{ id: string; findingId: string; direction: InspectionFindingCommunication['direction']; channel: InspectionFindingCommunication['channel']; type: InspectionFindingCommunication['type']; occurredAt: string; subject: string; details: string | null; contactName: string | null; createdAt: string; findingCode: string; findingTitle: string }>; + verificationResults: Array<{ id: string; findingId: string; findingCode: string; findingTitle: string; visitId: string; visitCode: string; visitTitle: string; visitStatus: InspectionVisitStatus; targetControlOn: string | null; outcome: InspectionVerificationOutcome | null; resultNotes: string | null; verifiedAt: string | null; resultRecordedAt: string | null; rescheduledControlOn: string | null; evidenceCount: number }>; + documents: Array<{ id: string; documentType: SourceDocumentType; documentNumber: string | null; title: string; issuer: string | null; documentDate: string | null; externalReference: string | null; relationType: AssetSourceDocumentRelationType; notes: string | null; linkedAt: string }>; + inspectionReports: Array<{ id: string; code: string; status: InspectionReportStatus; pdfStatus: InspectionReportPdfStatus; title: string; generatedAt: string; frozenSha256: string; actId: string; actCode: string; visitId: string; visitCode: string }>; + reports: Array<{ id: string; documentType: SourceDocumentType; documentNumber: string | null; title: string; issuer: string | null; documentDate: string | null; externalReference: string | null; relationType: AssetSourceDocumentRelationType; notes: string | null; linkedAt: string }>; + media: Array<{ id: string; kind: AssetMediaKind; originalName: string; title: string | null; description: string | null; capturedAt: string | null; createdAt: string; source: 'WEB' | 'ANDROID' | 'IMPORT' }>; + versions: Array<{ id: string; versionNumber: number; changeType: AssetVersionChangeType; changedFields: string[]; occurredAt: string; actorUsername: string | null; source: string }>; + timeline: AssetDossierTimelineEvent[]; +} + +export interface AssetContextHistoryItem { + id: string; + assetId: string; + validFrom: string; + validUntil: string | null; + changeReason: string; + endReason: string | null; + assetVersionNumber: number; + source: string; + requestId: string | null; + createdAt: string; + parent: { id: string; code: string; name: string } | null; + operationalArea: { id: string; code: string; name: string } | null; + operatorCompany: { id: string; code: string; name: string } | null; + creator: { id: string; username: string; firstName: string; lastName: string } | null; + isCurrent: boolean; +} + +export type FieldDiscoveryStatus = 'PENDING' | 'APPROVED' | 'MATCHED' | 'REJECTED'; + +export interface FieldDiscovery { + id: string; + status: FieldDiscoveryStatus; + discoveryNotes: string | null; + observedAt: string; + reviewedAt: string | null; + reviewNotes: string | null; + createdAt: string; + asset: { + id: string; code: string; name: string; commonName: string | null; informationStatus: AssetInformationStatus; + typeId: string; typeName: string; parentId: string | null; operationalAreaId: string | null; operatorCompanyId: string | null; + }; + visit: { id: string; code: string; title: string; status: InspectionVisitStatus }; + creator: { id: string; username: string; firstName: string; lastName: string }; + reviewer: { id: string; username: string; firstName: string; lastName: string } | null; + matchedAsset: { id: string; code: string; name: string; commonName: string | null } | null; +} + +export interface OperationalAssetSummary { + id: string; + code: string; + name: string; + commonName: string | null; + typeName: string; +} + +export type AreaOrganizationRole = 'OPERATOR' | 'TECHNICAL_OPERATOR' | 'CONCESSIONAIRE' | 'PERMIT_HOLDER' | 'PARTICIPANT' | 'OTHER'; + +export interface AreaCompanyRelation { + id: string; + area: OperationalAssetSummary; + company: OperationalAssetSummary; + relationRole: AreaOrganizationRole; + participationPercent: number | null; + legalInstrument: string | null; + sourceDocumentId: string | null; + validFrom: string; + validUntil: string | null; + startReason: string; + endReason: string | null; + createdBy: { id: string; username: string } | null; + endedBy: { id: string; username: string } | null; + createdAt: string; + updatedAt: string; + active: boolean; + assignedAssetCount: number; +} + + +export type OrganizationKind = 'COMPANY' | 'UTE' | 'PUBLIC_ENTITY' | 'OTHER'; +export type SourceDocumentType = 'NOTE' | 'TECHNICAL_REPORT' | 'INSPECTION_ACT' | 'INVENTORY' | 'RESOLUTION' | 'DECREE' | 'CONTRACT' | 'SPREADSHEET' | 'OTHER'; +export type AssetSourceDocumentRelationType = 'SOURCE' | 'MENTIONS' | 'VALIDATES' | 'SUPERSEDES' | 'OTHER'; +export type OrganizationMembershipRole = 'MEMBER' | 'LEAD_MEMBER' | 'OTHER'; +export type AreaLegalRightType = 'EXPLOITATION_CONCESSION' | 'EXPLORATION_PERMIT' | 'TRANSPORT_CONCESSION' | 'OTHER'; +export type AreaLegalRightStatus = 'ACTIVE' | 'EXPIRED' | 'REVOKED' | 'PENDING'; +export type AreaLegalRightOrganizationRole = 'HOLDER' | 'PARTICIPANT' | 'OPERATOR' | 'OTHER'; + +export interface SourceDocumentSummary { + id: string; documentType: SourceDocumentType; documentNumber: string | null; title: string; + issuer: string | null; documentDate: string | null; externalReference: string | null; notes: string | null; + createdAt?: string; updatedAt?: string; +} +export interface AssetRegistry { + organizationProfile: null | { assetId: string; organizationKind: OrganizationKind; legalName: string | null; taxId: string | null; notificationEmail: string | null; notes: string | null; createdAt: string; updatedAt: string }; + organizationMemberships: Array<{ id: string; parent: { id: string; code: string; name: string }; member: { id: string; code: string; name: string }; role: OrganizationMembershipRole; participationPercent: number | null; validFrom: string; validUntil: string | null; sourceDocumentId: string | null; notes: string | null; endReason: string | null }>; + externalIdentifiers: Array<{ id: string; namespace: string; value: string; validFrom: string; validUntil: string | null; sourceDocumentId: string | null; notes: string | null; endReason: string | null }>; + sourceDocuments: Array; + legalRights: Array<{ id: string; rightType: AreaLegalRightType; name: string; instrumentNumber: string | null; validFrom: string | null; validUntil: string | null; status: AreaLegalRightStatus; sourceDocumentId: string | null; notes: string | null; organizations: Array<{ id: string; organizationId: string; organizationName: string; role: AreaLegalRightOrganizationRole; participationPercent: number | null; validFrom: string; validUntil: string | null; notes: string | null; endReason: string | null }> }>; +} + + +export type AssetImportBatchStatus = 'ANALYZED' | 'REVIEW_REQUIRED' | 'CANCELLED' | 'FAILED'; +export type AssetImportRowStatus = 'READY' | 'WARNING' | 'CONFLICT' | 'IGNORED'; +export type AssetImportProfileCode = 'MENDOZA_INVENTORY_V1' | 'MENDOZA_YACIMIENTOS_V1' | 'UNKNOWN'; + +export interface AssetImportBatch { + id: string; + originalName: string; + mimeType: string; + sizeBytes: number; + sha256: string; + profileCode: AssetImportProfileCode; + profileConfidence: number; + worksheetName: string | null; + headerRow: number | null; + totalRows: number; + readyRows: number; + warningRows: number; + conflictRows: number; + ignoredRows: number; + status: AssetImportBatchStatus; + sourceDocumentId: string | null; + sourceLabel: string | null; + notes: string | null; + analysis: Record; + uploadedBy: string | null; + uploadedByUsername: string | null; + analyzedAt: string; + createdAt: string; + updatedAt: string; +} + +export interface AssetImportBatchDetail extends AssetImportBatch { + topIssues: Array<{ code: string; count: number }>; + duplicateFiles: Array<{ id: string; originalName: string; createdAt: string; status: AssetImportBatchStatus }>; +} + +export interface AssetImportRow { + id: string; + worksheetName: string; + rowNumber: number; + status: AssetImportRowStatus; + suggestedAction: 'CREATE' | 'MATCH' | 'REVIEW' | 'IGNORE'; + rawData: Record; + normalizedData: Record; + issues: string[]; + fingerprint: string; + matchedAssetId: string | null; + importedAssetId: string | null; + createdAt: string; + updatedAt: string; +} + +export type AssetImportPlanStatus = 'REVIEW_REQUIRED' | 'READY' | 'APPLIED' | 'ROLLED_BACK' | 'SUPERSEDED' | 'FAILED'; +export type AssetImportPlanEntityKind = 'DEPARTMENT' | 'ORGANIZATION' | 'AREA' | 'AREA_DEPARTMENT_RELATION' | 'FIELD' | 'OPERATOR_RELATION' | 'LEGAL_RIGHT' | 'LEGAL_RIGHT_ORGANIZATION' | 'INSTALLATION' | 'LOCAL_STRUCTURE' | 'TECHNICAL_ASSET'; +export type AssetImportPlanAction = 'CREATE' | 'MATCH' | 'REVIEW' | 'IGNORE'; +export type AssetImportPlanItemStatus = 'PLANNED' | 'MATCHED' | 'REVIEW' | 'IGNORED' | 'APPLIED' | 'ROLLED_BACK' | 'FAILED'; + +export interface AssetImportPlanItem { + id: string; + planId: string; + itemOrder: number; + entityKey: string; + entityKind: AssetImportPlanEntityKind; + action: AssetImportPlanAction; + status: AssetImportPlanItemStatus; + assetTypeCode: string | null; + displayName: string; + generatedCode: string | null; + parentEntityKey: string | null; + matchedAssetId: string | null; + appliedAssetId: string | null; + appliedObjectId: string | null; + payload: Record; + sourceRowNumbers: number[]; + reviewCodes: string[]; + resolutionNote: string | null; + resolvedBy: string | null; + resolvedAt: string | null; +} + +export interface AssetImportPlan { + id: string; + batchId: string; + revision: number; + status: AssetImportPlanStatus; + externalIdNamespace: string | null; + operatorAssetId: string | null; + summary: Record; + planHash: string; + generatedBy: string | null; + generatedAt: string; + appliedBy: string | null; + appliedAt: string | null; + applicationSummary: Record | null; + rolledBackBy: string | null; + rolledBackAt: string | null; + rollbackSummary: Record | null; + supersededAt: string | null; + createdAt: string; + updatedAt: string; + masterStateStale: boolean; + masterStateHashAvailable: boolean; + masterStateCheckedAt: string; + previewItems: AssetImportPlanItem[]; + reviewItems: AssetImportPlanItem[]; +} + +export interface AssetImportReviewItem extends AssetImportPlanItem { + batchId: string; + batchName: string; + sourceLabel: string | null; + profileCode: AssetImportProfileCode; + planRevision: number; + planStatus: AssetImportPlanStatus; + dependency: boolean; +} + +export interface AssetImportOrganizationOption { + id: string; + code: string; + name: string; + legalName: string | null; +} + +export type Position = [number, number]; +export type AssetGeometryType = 'POINT' | 'LINESTRING' | 'POLYGON'; + +export type GeoJsonGeometry = + | { type: 'POINT'; coordinates: Position } + | { type: 'LINESTRING'; coordinates: Position[] } + | { type: 'POLYGON'; coordinates: Position[][] }; + +export interface AssetGeometry { + assetId: string; + geometry: GeoJsonGeometry; + geometryType: AssetGeometryType; + source: 'WEB' | 'ANDROID' | 'IMPORT' | 'SURVEY'; + accuracyM: number | null; + capturedAt: string | null; + deviceLabel: string | null; + createdAt: string; + updatedAt: string; + updatedBy: string | null; +} + +export interface MapAssetProperties { + id: string; + code: string; + name: string; + commonName?: string | null; + typeId: string; + typeCode: string; + typeName: string; + parentId: string | null; + parentName: string | null; + informationStatus: AssetInformationStatus; + geometryType: AssetGeometryType; + accuracyM: number | null; + capturedAt: string | null; + updatedAt: string; +} + +export interface MapAssetFeature { + type: 'Feature'; + id: string; + geometry: GeoJsonGeometry; + properties: MapAssetProperties; +} + +export interface MapAssetFeatureCollection { + type: 'FeatureCollection'; + features: MapAssetFeature[]; + meta: { count: number; truncated: boolean }; +} + +export type AssetMediaKind = 'PHOTO' | 'DOCUMENT'; + +export interface AssetMedia { + id: string; + assetId: string; + kind: AssetMediaKind; + originalName: string; + mimeType: 'image/jpeg' | 'image/png' | 'image/webp' | 'application/pdf'; + sizeBytes: number; + sha256: string; + title: string | null; + description: string | null; + capturedAt: string | null; + latitude: number | null; + longitude: number | null; + accuracyM: number | null; + source: 'WEB' | 'ANDROID' | 'IMPORT'; + uploadedBy: string | null; + uploadedByUsername: string | null; + createdAt: string; + updatedAt: string; +} + +export type AssetVersionChangeType = + | 'BASELINE' + | 'CREATED' + | 'UPDATED' + | 'CONTEXT_CHANGED' + | 'STATUS_CHANGED' + | 'OPERATIONAL_STATUS_CHANGED' + | 'REGISTRY_UPDATED' + | 'GEOMETRY_UPDATED' + | 'GEOMETRY_REMOVED' + | 'MEDIA_UPLOADED' + | 'MEDIA_UPDATED' + | 'MEDIA_REMOVED' + | 'PROVENANCE_BASELINE' + | 'PROVENANCE_UPDATED' + | 'PROVENANCE_VERIFIED'; + +export interface AssetVersionSnapshot { + id: string; + code: string; + name: string; + description: string | null; + type: { id: string; code: string; name: string; operationalRole?: AssetTypeOperationalRole }; + parent: { id: string; code: string; name: string } | null; + operationalArea?: { id: string; code: string; name: string } | null; + operatorCompany?: { id: string; code: string; name: string } | null; + informationStatus: AssetInformationStatus; + operationalStatus: AssetOperationalStatus; + organizationProfile?: Record | null; + organizationMemberships?: Array>; + externalIdentifiers?: Array>; + sourceDocuments?: Array>; + legalRights?: Array>; + attributes: Array<{ + definitionId: string; + code: string; + name: string; + dataType: AssetAttributeDataType; + isRequired: boolean; + unit: string | null; + options: string[] | null; + sortOrder: number; + value: unknown; + }>; + geometry: AssetGeometry | null; + media?: AssetMedia[]; + provenance?: { + origin: AssetDataOrigin; + sourceName: string | null; + sourceReference: string | null; + observedAt: string | null; + notes: string | null; + verifiedAt: string | null; + verifiedBy: string | null; + updatedAt: string; + updatedBy: string | null; + }; + createdAt: string; + updatedAt: string; + createdBy: string | null; + updatedBy: string | null; + currentVersion: number; +} + +export interface AssetVersionSummary { + id: string; + assetId: string; + assetCode: string; + assetName: string; + typeId: string; + typeName: string; + informationStatus: AssetInformationStatus; + operationalStatus: AssetOperationalStatus; + versionNumber: number; + changeType: AssetVersionChangeType; + changedFields: string[]; + occurredAt: string; + actorUserId: string | null; + actorUsername: string | null; + source: 'WEB' | 'ANDROID' | 'SYSTEM' | 'IMPORT'; + requestId: string | null; + isCurrent: boolean; +} + +export interface AssetVersionDetail extends AssetVersionSummary { + snapshot: AssetVersionSnapshot; +} + +export interface TemporalAssetSummary extends AssetVersionSummary { + effectiveUntil: string | null; +} + +export interface TemporalAssetDetail extends TemporalAssetSummary { + snapshot: AssetVersionSnapshot; + asOf: string; +} + +export type SurveyCampaignStatus = + | 'DRAFT' | 'PLANNED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED'; + +export type SurveyTargetStatus = + | 'PENDING' | 'IN_PROGRESS' | 'SUBMITTED' | 'COMPLETED' | 'SKIPPED'; + +export interface SurveyPerson { + id: string; + username: string; + firstName: string; + lastName: string; +} + +export interface SurveyCampaignListItem { + id: string; + code: string; + name: string; + description: string | null; + status: SurveyCampaignStatus; + plannedStartAt: string | null; + plannedEndAt: string | null; + scopeAsset: { id: string; code: string; name: string } | null; + coordinator: SurveyPerson | null; + targetCount: number; + pendingCount: number; + inProgressCount: number; + submittedCount: number; + completedCount: number; + skippedCount: number; + createdAt: string; + updatedAt: string; +} + +export interface SurveyTarget { + id: string; + campaignId: string; + asset: { id: string; code: string; name: string; typeName: string }; + assignedUser: SurveyPerson | null; + status: SurveyTargetStatus; + dueAt: string | null; + instructions: string | null; + createdAt: string; + updatedAt: string; +} + +export interface SurveyCampaign extends SurveyCampaignListItem { + targets: SurveyTarget[]; +} + +export type SurveyReportOutcome = 'CONFIRMED' | 'CHANGES_RECORDED' | 'NOT_LOCATED'; +export type SurveyReportStatus = 'DRAFT' | 'SUBMITTED' | 'APPROVED' | 'REJECTED'; +export type SurveyReportVersionEvent = 'SUBMITTED' | 'APPROVED' | 'REJECTED'; + +export interface SurveyTargetReport { + id: string; + targetId: string; + outcome: SurveyReportOutcome | null; + status: SurveyReportStatus; + observedAt: string | null; + latitude: number | null; + longitude: number | null; + accuracyM: number | null; + notes: string | null; + assetVersionAtSubmission: number | null; + submittedAt: string | null; + submittedBy: SurveyPerson | null; + reviewedAt: string | null; + reviewedBy: SurveyPerson | null; + reviewNotes: string | null; + selectedMediaIds: string[]; + createdAt: string; + updatedAt: string; +} + +export interface SurveyReportMedia extends AssetMedia { + kind: 'PHOTO'; + selected: boolean; +} + +export interface SurveyReportVersion { + id: string; + versionNumber: number; + event: SurveyReportVersionEvent; + snapshot: Record; + actorUserId: string | null; + actorUsername: string | null; + createdAt: string; +} + +export interface SurveyExecution { + target: { + id: string; + status: SurveyTargetStatus; + dueAt: string | null; + instructions: string | null; + assignedUser: SurveyPerson | null; + }; + campaign: { + id: string; + code: string; + name: string; + status: SurveyCampaignStatus; + }; + asset: { + id: string; + code: string; + name: string; + typeName: string; + informationStatus: AssetInformationStatus; + currentVersion: number; + }; + report: SurveyTargetReport | null; + availableMedia: SurveyReportMedia[]; + versions: SurveyReportVersion[]; +} + +export type InspectionVisitStatus = + | 'DRAFT' | 'PLANNED' | 'IN_PROGRESS' | 'CLOSED' | 'CANCELLED'; + +export interface InspectionPerson { + id: string; + username: string; + firstName: string; + lastName: string; +} + +export interface InspectionAssetSummary { + id: string; + code: string; + name: string; + commonName?: string | null; + typeName: string; + operatorCompany?: { id: string; code: string; name: string } | null; + operationalArea?: { id: string; code: string; name: string } | null; +} + +export interface InspectionPlanningContextAsset { + id: string; + code: string; + name: string; +} + +export type InspectionVisitAssetPlanningSource = 'LEGACY' | 'AUTOMATIC' | 'PREVENTIVE' | 'VERIFICATION'; +export type InspectionChecklistItemKind = 'ANTECEDENT' | 'COMPANY_OVERDUE' | 'VERIFICATION_OVERDUE' | 'UPCOMING_CONTROL'; + +export interface InspectionPlannedAsset extends InspectionAssetSummary { + included: boolean; + planningSource: InspectionVisitAssetPlanningSource; + exclusionReason: string | null; + excludedAt: string | null; + excludedBy: InspectionPerson | null; +} + +export interface InspectionVisitChecklistItem { + id: string; + findingId: string; + findingCode: string; + findingTitle: string; + findingStatus: InspectionFindingStatus; + severity: number | null; + itemKind: InspectionChecklistItemKind; + referenceOn: string | null; + asset: InspectionAssetSummary; + assetIncluded: boolean; +} + +export interface InspectionVisitChecklistSummary { + generation: number; + generatedAt: string | null; + stale: boolean; + antecedents: number; + companyOverdue: number; + verificationOverdue: number; + upcomingControls: number; + actionableAssets: number; + excludedAssets: number; + items: InspectionVisitChecklistItem[]; +} + +export interface InspectionVisitListItem { + id: string; + code: string; + title: string; + objective: string | null; + status: InspectionVisitStatus; + scopeAsset: InspectionAssetSummary | null; + operationalArea: InspectionPlanningContextAsset | null; + operatorCompany: InspectionPlanningContextAsset | null; + leadInspector: InspectionPerson | null; + plannedStartAt: string | null; + plannedEndAt: string | null; + actualStartedAt: string | null; + actualClosedAt: string | null; + instructions: string | null; + cancellationReason: string | null; + checklistGeneration: number; + checklistGeneratedAt: string | null; + assetCount: number; + memberCount: number; + createdAt: string; + updatedAt: string; +} + +export type InspectionVerificationOutcome = 'RESOLVED' | 'NOT_RESOLVED' | 'REQUIRES_NEW_DATE'; + +export interface InspectionVerificationFindingSummary { + id: string; + code: string; + title: string; + status: InspectionFindingStatus; + nextControlOn: string | null; + assetId: string; + assetCode: string; + assetName: string; + targetControlOn: string | null; + outcome: InspectionVerificationOutcome | null; + resultNotes: string | null; + verifiedAt: string | null; + resultRecordedAt: string | null; + rescheduledControlOn: string | null; + verificationEvidenceCount: number; +} + +export interface InspectionVisit extends InspectionVisitListItem { + assets: InspectionAssetSummary[]; + planningAssets: InspectionPlannedAsset[]; + team: InspectionPerson[]; + verificationFindings: InspectionVerificationFindingSummary[]; + checklist: InspectionVisitChecklistSummary; +} + +export type InspectionActStatus = + | 'DRAFT' | 'READY' | 'CLOSED' | 'CANCELLED' | 'RECTIFIED'; + +export type InspectionActVersionEvent = + | 'CREATED' | 'UPDATED' | 'READY' | 'REOPENED' | 'CLOSED' | 'CANCELLED'; + +export interface InspectionActListItem { + id: string; + visitId: string; + visit: { + id: string; + code: string; + title: string; + status: InspectionVisitStatus; + actualStartedAt: string | null; + }; + actYear: number; + actNumber: number; + code: string; + status: InspectionActStatus; + occurredAt: string; + title: string; + summary: string; + observations: string | null; + currentVersion: number; + cancellationReason: string | null; + closedAt: string | null; + closedBy: string | null; + closureSha256: string | null; + assetCount: number; + findingCount: number; + companies: Array<{ id: string; code: string; name: string }>; + areas: Array<{ id: string; code: string; name: string }>; + report: null | { + id: string; + code: string; + status: InspectionReportStatus; + pdfStatus: InspectionReportPdfStatus; + generatedAt: string; + }; + createdBy: InspectionPerson | null; + updatedBy: InspectionPerson | null; + createdAt: string; + updatedAt: string; +} + +export interface InspectionActVersion { + id: string; + versionNumber: number; + event: InspectionActVersionEvent; + snapshot: Record; + actorUserId: string | null; + actorUsername: string | null; + createdAt: string; +} + +export interface InspectionAct extends InspectionActListItem { + assets: InspectionAssetSummary[]; + versions: InspectionActVersion[]; +} + +export type InspectionReportStatus = 'FROZEN' | 'CANCELLED'; +export type InspectionReportPdfStatus = 'PENDING' | 'READY' | 'FAILED'; +export type InspectionReportWordStatus = 'PENDING' | 'READY' | 'FAILED'; +export type InspectionReportReviewStatus = 'PENDING_REVIEW' | 'APPROVED' | 'SIGNED'; + +export interface InspectionReportListItem { + id: string; + visitId: string; + actId: string; + reportYear: number; + reportNumber: number; + code: string; + status: InspectionReportStatus; + pdfStatus: InspectionReportPdfStatus; + wordStatus: InspectionReportWordStatus; + wordGeneratedAt: string | null; + reviewStatus: InspectionReportReviewStatus; + currentRevisionNumber: number; + approvedAt: string | null; + signedAt: string | null; + title: string; + actVersion: number; + actClosureSha256: string; + frozenSha256: string; + generatedAt: string; + generatedBy: InspectionPerson; + act: { + id: string; + code: string; + title: string; + status: InspectionActStatus; + occurredAt: string; + closedAt: string | null; + }; + visit: { + id: string; + code: string; + title: string; + status: InspectionVisitStatus; + }; + companies: Array<{ id: string; code: string; name: string }>; + areas: Array<{ id: string; code: string; name: string }>; + findingCount: number; +} + +export interface InspectionReport extends InspectionReportListItem { + frozenSnapshot: Record; +} + +export type InspectionReportRevisionSource = 'AUTO' | 'DIRECTOR_UPLOAD'; + +export interface InspectionReportRevision { + id: string; + reportId: string; + revisionNumber: number; + source: InspectionReportRevisionSource; + originalName: string; + mimeType: string; + sizeBytes: number; + sha256: string; + changeSummary: string | null; + createdAt: string; + createdBy: InspectionPerson; +} + +export interface InspectionReportSignature { + id: string; + revisionId: string; + signedAt: string; + confirmationText: string; + signatureSha256: string; + signedBy: InspectionPerson; +} + +export interface InspectionReportReview { + reportId: string; + reportCode: string; + reviewStatus: InspectionReportReviewStatus; + currentRevisionNumber: number; + approvedRevisionId: string | null; + approvedAt: string | null; + reviewNote: string | null; + approvedBy: InspectionPerson | null; + signature: InspectionReportSignature | null; + revisions: InspectionReportRevision[]; +} + +export interface PendingInspectionReportItem { + actId: string; + visitId: string; + actCode: string; + actTitle: string; + actYear: number; + occurredAt: string; + closedAt: string; + closureSha256: string; + visitCode: string; + visitTitle: string; + companies: Array<{ id: string; code: string; name: string }>; + areas: Array<{ id: string; code: string; name: string }>; + findingCount: number; +} + +export type InspectionResponsibleAttendanceStatus = 'PRESENT' | 'ABSENT'; +export type InspectionResponsibleDocumentType = 'DNI' | 'CUIL' | 'PASSPORT' | 'OTHER'; +export type InspectionActSignatureStatus = 'SIGNED' | 'REFUSED' | 'ABSENT'; +export type InspectionActSignerType = 'INSPECTOR' | 'COMPANY_RESPONSIBLE'; +export type InspectionActUploadMode = 'IMMEDIATE' | 'DEFERRED'; + +export interface InspectionActResponsible { + actId: string; + attendanceStatus: InspectionResponsibleAttendanceStatus; + fullName: string | null; + documentType: InspectionResponsibleDocumentType | null; + documentNumber: string | null; + position: string | null; + email: string | null; + phone: string | null; + absenceReason: string | null; + updatedBy: string | null; + createdAt: string; + updatedAt: string; +} + +export interface InspectionActSignature { + id: string; + actId: string; + signerType: InspectionActSignerType; + signerUserId: string | null; + signerName: string; + documentType: InspectionResponsibleDocumentType | null; + documentNumber: string | null; + position: string | null; + status: InspectionActSignatureStatus; + reason: string | null; + companyManifestation: 'CONFORMITY' | 'DISSENT' | null; + companyStatement: string | null; + mimeType: string | null; + sizeBytes: number | null; + imageSha256: string | null; + consentText: string | null; + consentVersion: string | null; + consentAcceptedAt: string | null; + clientSignedAt: string | null; + signedAt: string | null; + latitude: number | null; + longitude: number | null; + accuracyM: number | null; + deviceLabel: string | null; + source: 'WEB' | 'ANDROID'; + preparedSha256: string; + signaturePayloadSha256: string; + uploadedBy: string; + uploadedByUsername: string; + createdAt: string; +} + +export interface InspectionClosure { + act: { + id: string; + code: string; + status: InspectionActStatus; + visitId: string; + currentVersion: number; + closedAt: string | null; + closedBy: string | null; + closureSha256: string | null; + }; + visit: { + id: string; + code: string; + status: InspectionVisitStatus; + actualClosedAt: string | null; + }; + responsible: InspectionActResponsible | null; + closure: null | { + schemaVersion: string; + preparedSha256: string; + preparedAt: string; + preparedBy: string; + finalSha256: string | null; + deviceClosedAt: string | null; + serverClosedAt: string | null; + uploadMode: InspectionActUploadMode | null; + closedBy: string | null; + isCurrent: boolean; + }; + signatures: InspectionActSignature[]; + consents: { + version: string; + inspector: string; + company: string; + }; +} + +export interface FindingCategory { + id: string; + code: string; + name: string; + sortOrder: number; +} + +export interface FindingCatalogItem { + id: string; + categoryId: string; + categoryName: string; + code: string; + sourceNumber: number; + title: string; + legalBasis: string | null; + glossary: string | null; + suggestedSeverity: number | null; + revision: number; +} + +export interface FindingCatalog { + categories: FindingCategory[]; + items: FindingCatalogItem[]; +} + +export interface FindingAdminCategory extends FindingCategory { + isActive: boolean; + itemCount: number; + activeItemCount: number; + createdAt: string; + updatedAt: string; +} + +export interface FindingAdminCatalogItem extends FindingCatalogItem { + categoryCode: string; + categoryActive: boolean; + importNote: string | null; + isActive: boolean; + usageCount: number; + createdAt: string; + updatedAt: string; +} + +export interface FindingAdminCatalog { + categories: FindingAdminCategory[]; + items: FindingAdminCatalogItem[]; +} + +export interface FindingCatalogSelectionItem { + id: string; + categoryId: string; + categoryName: string; + code: string; + sourceNumber: number; + title: string; + suggestedSeverity: number | null; + enabled: boolean; +} + +export interface FindingCatalogAssetTypeSelection { + assetType: { id: string; code: string; name: string; operationalRole: AssetTypeOperationalRole }; + configured: boolean; + reason: string | null; + items: FindingCatalogSelectionItem[]; +} + +export interface FindingCatalogAssetSelection { + asset: { + id: string; + code: string; + name: string; + assetTypeId: string; + assetTypeCode: string; + assetTypeName: string; + }; + typeConfigured: boolean; + typeReason: string | null; + items: Array; +} + +export interface FindingCatalogApplicable extends FindingCatalog { + asset: FindingCatalogAssetSelection['asset']; + typeConfigured: boolean; + configurationReason: string | null; + other: { enabled: true; code: 'OTHER'; label: string; help: string }; +} + +export type FindingCatalogProposalStatus = 'PENDING' | 'MATCHED' | 'REJECTED'; +export interface FindingCatalogProposal { + id: string; + findingId: string; + findingCode: string; + assetId: string; + assetCode: string; + assetName: string; + assetTypeId: string; + assetTypeCode: string; + assetTypeName: string; + proposedTitle: string; + proposedLegalBasis: string | null; + proposedSeverity: number | null; + description: string; + status: FindingCatalogProposalStatus; + resolvedCatalogItemId: string | null; + resolvedCatalogCode: string | null; + resolvedCatalogTitle: string | null; + officeNotes: string | null; + reviewedBy: string | null; + reviewedByUsername: string | null; + reviewedAt: string | null; + createdAt: string; + updatedAt: string; +} + +export type InspectionFindingWorkflow = + | 'ALL' + | 'OPEN' + | 'WAITING_COMPANY' + | 'COMPANY_OVERDUE' + | 'TO_SCHEDULE_VERIFICATION' + | 'TO_VERIFY' + | 'VERIFICATION_OVERDUE' + | 'READY_TO_CLOSE' + | 'CLOSED'; + +export interface InspectionFindingWorkflowCounters { + open: number; + waitingCompany: number; + companyOverdue: number; + companyDueNext7Days: number; + awaitingVerificationSchedule: number; + toVerify: number; + verificationOverdue: number; + verificationNext30Days: number; + readyToClose: number; + closed: number; +} + +export interface InspectionFindingGlobalPage extends Page { + counters: InspectionFindingWorkflowCounters; +} + +export type VerificationPlanningStatus = 'ALL' | 'UNPLANNED' | 'PLANNED'; + +export interface VerificationPlanningItem { + id: string; + code: string; + title: string; + status: 'OPEN'; + nextControlOn: string; + companyResponseReceivedOn: string; + asset: { id: string; code: string; name: string; typeName: string }; + company: { id: string; code: string; name: string } | null; + area: { id: string; code: string; name: string } | null; + act: { id: string; code: string }; + sourceVisit: { id: string; code: string; title: string }; + verificationVisit: null | { + id: string; + code: string; + title: string; + status: InspectionVisitStatus; + plannedStartAt: string | null; + plannedEndAt: string | null; + }; +} + +export interface VerificationPlanningCounters { + eligible: number; + unplanned: number; + overdueUnplanned: number; + dueNext30Days: number; + planned: number; +} + +export interface VerificationPlanningPage extends Page { + counters: VerificationPlanningCounters; +} + +export interface PlannedVerificationVisitResult { + visit: { + id: string; + code: string; + title: string; + status: InspectionVisitStatus; + plannedStartAt: string | null; + plannedEndAt: string | null; + }; + company: { id: string; code: string; name: string }; + area: { id: string; code: string; name: string }; + findingCount: number; + assetCount: number; +} + +export type InspectionFindingStatus = 'OPEN' | 'CLOSED' | 'VOIDED'; +export type InspectionFindingVerificationEventType = + | 'CONTROL_DATE_DEFINED' + | 'CONTROL_DATE_CHANGED' + | 'CONTROL_DATE_CLEARED' + | 'VISIT_PLANNED' + | 'RESULT_RECORDED'; + +export interface InspectionFindingVerificationEvent { + id: string; + eventType: InspectionFindingVerificationEventType; + occurredAt: string; + targetControlOn: string | null; + previousControlOn: string | null; + nextControlOn: string | null; + outcome: InspectionVerificationOutcome | null; + notes: string | null; + actorUserId: string | null; + actorUsername: string | null; + verificationVisit: null | { + id: string; + code: string; + title: string; + status: InspectionVisitStatus; + plannedStartAt: string | null; + }; + evidenceCount: number; +} +export type InspectionFindingVersionEvent = 'CREATED' | 'UPDATED' | 'FOLLOW_UP_UPDATED' | 'VERIFICATION_RECORDED'; +export type InspectionFindingOfficeStage = + | 'WAITING_COMPANY' + | 'COMPANY_OVERDUE' + | 'DEFINE_VERIFICATION' + | 'PLAN_VERIFICATION' + | 'VERIFICATION_PLANNED' + | 'RESCHEDULE_VERIFICATION' + | 'READY_TO_CLOSE' + | 'CLOSED'; + +export interface InspectionFindingOfficeWorkflow { + stage: InspectionFindingOfficeStage; + label: string; + action: string; + dueOn: string | null; + overdue: boolean; +} + +export interface InspectionFinding { + id: string; + actId: string; + assetId: string; + catalogItemId: string | null; + findingNumber: number; + code: string; + status: InspectionFindingStatus; + title: string; + description: string; + legalBasis: string | null; + glossary: string | null; + catalogRevision: number | null; + suggestedSeverity: number | null; + severity: number | null; + correctionDueOn: string | null; + responseDueBasis: 'FINDING_DATE' | 'REPORT_NOTIFICATION' | null; + responseDueDays: number | null; + responseDueBaseOn: string | null; + reportNotifiedOn: string | null; + companyResponse: string | null; + companyResponseReceivedOn: string | null; + companyCommittedCorrectionOn: string | null; + nextControlOn: string | null; + currentVersion: number; + closedAt: string | null; + closureNotes: string | null; + asset: InspectionAssetSummary; + catalog: { + id: string; + code: string; + categoryId: string; + categoryName: string; + sourceNumber: number; + revision: number; + suggestedSeverity: number | null; + } | null; + document: { + actId: string; + actCode: string; + actStatus: InspectionActStatus; + visitId: string; + visitCode: string; + visitTitle: string; + visitStatus: InspectionVisitStatus; + }; + verificationVisit: null | { + id: string; + code: string; + title: string; + status: InspectionVisitStatus; + plannedStartAt: string | null; + }; + latestVerification: null | { + visitId: string; + visitCode: string; + visitStatus: InspectionVisitStatus; + targetControlOn: string | null; + outcome: InspectionVerificationOutcome | null; + resultNotes: string | null; + verifiedAt: string | null; + resultRecordedAt: string | null; + rescheduledControlOn: string | null; + evidenceCount: number; + }; + verificationHistory?: InspectionFindingVerificationEvent[]; + createdAt: string; + updatedAt: string; + officeWorkflow?: InspectionFindingOfficeWorkflow; + versions?: Array<{ + id: string; + versionNumber: number; + event: InspectionFindingVersionEvent; + snapshot: Record; + actorUserId: string | null; + actorUsername: string | null; + createdAt: string; + }>; +} + +export type InspectionEvidenceKind = 'PHOTO' | 'DOCUMENT'; +export type InspectionEvidencePurpose = + | 'OBSERVATION' + | 'VERIFICATION' + | 'COMPANY_RESPONSE' + | 'COMMUNICATION_ATTACHMENT' + | 'OTHER_DOCUMENT'; + +export interface InspectionFindingCommunication { + id: string; + findingId: string; + direction: 'INBOUND' | 'OUTBOUND' | 'INTERNAL'; + channel: 'EMAIL' | 'IN_PERSON' | 'PHONE' | 'LETTER' | 'SYSTEM' | 'OTHER'; + type: 'COMPANY_RESPONSE' | 'AUTHORITY_NOTICE' | 'FOLLOW_UP' | 'OTHER'; + occurredAt: string; + subject: string; + details: string | null; + contactName: string | null; + contactEmail: string | null; + createdBy: string | null; + createdByUsername: string | null; + attachmentCount: number; + createdAt: string; +} + +export interface InspectionFindingEvidence { + id: string; + findingId: string; + communicationId: string | null; + verificationVisitId: string | null; + communication: { + id: string; + type: InspectionFindingCommunication['type']; + direction: InspectionFindingCommunication['direction']; + subject: string; + } | null; + kind: InspectionEvidenceKind; + purpose: InspectionEvidencePurpose; + originalName: string; + mimeType: string; + sizeBytes: number; + sha256: string; + title: string | null; + description: string | null; + capturedAt: string | null; + latitude: number | null; + longitude: number | null; + accuracyM: number | null; + deviceLabel: string | null; + source: 'WEB' | 'ANDROID' | 'IMPORT'; + uploadedBy: string | null; + uploadedByUsername: string | null; + createdAt: string; +} + +let csrfToken: string | null = null; +let refreshPromise: Promise | null = null; + +function readCsrfCookie(): string | null { + if (typeof document === 'undefined') return null; + const cookies = document.cookie.split(';').map((item) => item.trim()); + const exact = cookies.find((item) => item.startsWith('dhv2_csrf=')); + const fallback = cookies.find((item) => item.split('=', 1)[0]?.endsWith('_csrf')); + const raw = exact ?? fallback; + if (!raw) return null; + const separator = raw.indexOf('='); + return separator >= 0 ? decodeURIComponent(raw.slice(separator + 1)) : null; +} + +function isUnsafe(method: string): boolean { + return !['GET', 'HEAD', 'OPTIONS'].includes(method.toUpperCase()); +} + +async function decodeResponse(response: Response): Promise { + const body = await response.json().catch(() => null) as { + code?: string; + message?: string; + requestId?: string; + } | null; + if (!response.ok) { + throw new ApiError( + response.status, + body?.code ?? 'REQUEST_ERROR', + body?.message ?? `La solicitud falló con HTTP ${response.status}`, + body?.requestId, + ); + } + return body as T; +} + +async function rawRequest(path: string, init: RequestInit = {}): Promise { + const method = (init.method ?? 'GET').toUpperCase(); + const headers = new Headers(init.headers); + headers.set('Accept', 'application/json'); + if (init.body && !(init.body instanceof FormData)) { + headers.set('Content-Type', 'application/json'); + } + if (isUnsafe(method) && path !== '/auth/login') { + const token = csrfToken ?? readCsrfCookie(); + if (token) headers.set('x-csrf-token', token); + } + + const response = await fetch(`${API_BASE}${path}`, { + ...init, + method, + headers, + credentials: 'include', + cache: 'no-store', + }); + return decodeResponse(response); +} + +async function refreshAccess(): Promise { + if (!refreshPromise) { + refreshPromise = rawRequest('/auth/refresh', { method: 'POST' }) + .then((response) => { csrfToken = response.csrfToken; }) + .finally(() => { refreshPromise = null; }); + } + return refreshPromise; +} + +export async function apiRequest( + path: string, + init: RequestInit = {}, + retrySession = true, +): Promise { + try { + return await rawRequest(path, init); + } catch (error) { + if ( + retrySession && error instanceof ApiError && error.status === 401 && + path !== '/auth/login' && path !== '/auth/refresh' + ) { + try { + await refreshAccess(); + return await rawRequest(path, init); + } catch (refreshError) { + csrfToken = null; + window.dispatchEvent(new Event('dhv2:unauthorized')); + throw refreshError; + } + } + throw error; + } +} + +async function rawBlobRequest(path: string): Promise { + const response = await fetch(`${API_BASE}${path}`, { + method: 'GET', + headers: { Accept: '*/*' }, + credentials: 'include', + cache: 'no-store', + }); + if (!response.ok) return decodeResponse(response); + return response.blob(); +} + +async function apiBlobRequest(path: string): Promise { + try { + return await rawBlobRequest(path); + } catch (error) { + if (error instanceof ApiError && error.status === 401) { + try { + await refreshAccess(); + return await rawBlobRequest(path); + } catch (refreshError) { + csrfToken = null; + window.dispatchEvent(new Event('dhv2:unauthorized')); + throw refreshError; + } + } + throw error; + } +} + +export function getHealth() { return apiRequest('/health', {}, false); } + +export async function login(identifier: string, password: string) { + const response = await apiRequest('/auth/login', { + method: 'POST', + body: JSON.stringify({ identifier, password, deviceLabel: 'Panel web' }), + }, false); + csrfToken = response.csrfToken; + return response; +} + +export async function getMe() { + const response = await apiRequest<{ user: AuthUser }>('/auth/me'); + return response.user; +} + +export async function logout() { + await apiRequest<{ status: string }>('/auth/logout', { method: 'POST' }); + csrfToken = null; +} + +export function changePassword(currentPassword: string, newPassword: string) { + return apiRequest<{ status: string; mustChangePassword: false }>('/auth/change-password', { + method: 'POST', body: JSON.stringify({ currentPassword, newPassword }), + }); +} + +export function getDashboardSummary() { return apiRequest('/dashboard/summary'); } + +export function listUsers(params: { + page?: number; pageSize?: number; search?: string; status?: 'ACTIVE' | 'INACTIVE' | ''; +} = {}) { + const query = new URLSearchParams(); + query.set('page', String(params.page ?? 1)); + query.set('pageSize', String(params.pageSize ?? 20)); + if (params.search) query.set('search', params.search); + if (params.status) query.set('status', params.status); + return apiRequest>(`/users?${query}`); +} + +export function getUser(id: string) { return apiRequest(`/users/${id}`); } + +export function createUser(input: { + username: string; email?: string; firstName: string; lastName: string; + password: string; mustChangePassword: boolean; roleIds: string[]; +}) { + return apiRequest('/users', { + method: 'POST', body: JSON.stringify(input), + }); +} + +export function updateUser(id: string, input: { + username?: string; email?: string | null; firstName?: string; lastName?: string; +}) { + return apiRequest(`/users/${id}`, { + method: 'PATCH', body: JSON.stringify(input), + }); +} + +export function resetUserPassword(id: string, password: string, mustChangePassword = true) { + return apiRequest(`/users/${id}/reset-password`, { + method: 'POST', + body: JSON.stringify({ password, mustChangePassword }), + }); +} + +export function updateUserStatus(id: string, status: 'ACTIVE' | 'INACTIVE') { + return apiRequest(`/users/${id}/status`, { + method: 'PATCH', body: JSON.stringify({ status }), + }); +} + +export function replaceUserRoles(id: string, roleIds: string[]) { + return apiRequest(`/users/${id}/roles`, { + method: 'PUT', body: JSON.stringify({ roleIds }), + }); +} + +export async function listRoles() { + return (await apiRequest<{ data: AdministrativeRole[] }>('/roles')).data; +} + +export async function listPermissions() { + return (await apiRequest<{ data: Permission[] }>('/roles/permissions')).data; +} + +export function createRole(input: { + code: string; name: string; description: string; permissionIds: string[]; +}) { + return apiRequest('/roles', { + method: 'POST', body: JSON.stringify(input), + }); +} + +export function updateRole(id: string, input: { name: string; description: string }) { + return apiRequest(`/roles/${id}`, { + method: 'PATCH', body: JSON.stringify(input), + }); +} + +export function replaceRolePermissions(id: string, permissionIds: string[]) { + return apiRequest(`/roles/${id}/permissions`, { + method: 'PUT', body: JSON.stringify({ permissionIds }), + }); +} + +export function listAudit(params: Record) { + const query = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== '') query.set(key, String(value)); + }); + return apiRequest>(`/audit?${query}`); +} + +export function getAuditEvent(id: string) { + return apiRequest(`/audit/${id}`); +} + +export async function listAssetTypes() { + return (await apiRequest<{ data: AssetType[] }>('/asset-types')).data; +} + +export function getMasterBootstrapStatus() { + return apiRequest('/asset-types/bootstrap-status'); +} + +export function bootstrapMasterDefaults() { + return apiRequest('/asset-types/bootstrap-defaults', { + method: 'POST', + }); +} + +export function getMasterEnrichmentStatus() { + return apiRequest('/asset-types/enrichment-status'); +} + +export function enrichMasterDefaults() { + return apiRequest('/asset-types/enrich-defaults', { + method: 'POST', + }); +} + +export async function listOperationalAreas(parentId?: string) { + const query = parentId ? `?${new URLSearchParams({ parentId })}` : ''; + return (await apiRequest<{ data: OperationalAssetSummary[] }>(`/asset-operational-relations/areas${query}`)).data; +} + +export async function listOperationalCompanies() { + return (await apiRequest<{ data: OperationalAssetSummary[] }>('/asset-operational-relations/companies')).data; +} + +export async function listCompaniesForArea(areaId: string) { + return (await apiRequest<{ data: OperationalAssetSummary[] }>(`/asset-operational-relations/areas/${areaId}/companies`)).data; +} + +export async function listAreasForCompany(companyId: string) { + return (await apiRequest<{ data: OperationalAssetSummary[] }>(`/asset-operational-relations/companies/${companyId}/areas`)).data; +} + +export async function listAreaCompanyRelations(params: { + areaId?: string; + companyId?: string; + includeHistory?: boolean; +} = {}) { + const query = new URLSearchParams(); + if (params.areaId) query.set('areaId', params.areaId); + if (params.companyId) query.set('companyId', params.companyId); + if (params.includeHistory) query.set('includeHistory', 'true'); + const suffix = query.size ? `?${query}` : ''; + return (await apiRequest<{ data: AreaCompanyRelation[] }>(`/asset-operational-relations${suffix}`)).data; +} + +export function createAreaCompanyRelation(input: { areaId: string; companyId: string; relationRole?: AreaOrganizationRole; participationPercent?: number | null; legalInstrument?: string | null; sourceDocumentId?: string | null; reason: string }) { + return apiRequest('/asset-operational-relations', { + method: 'POST', body: JSON.stringify(input), + }); +} + +export function endAreaCompanyRelation(id: string, reason: string) { + return apiRequest(`/asset-operational-relations/${id}/end`, { + method: 'POST', body: JSON.stringify({ reason }), + }); +} + +export function createAssetType(input: { + code: string; + name: string; + description: string; + canBeRoot: boolean; + operationalRole: AssetTypeOperationalRole; + allowedParentTypeIds: string[]; +}) { + return apiRequest('/asset-types', { + method: 'POST', body: JSON.stringify(input), + }); +} + +export function updateAssetType(id: string, input: { + name?: string; + description?: string; + canBeRoot?: boolean; + isActive?: boolean; + operationalRole?: AssetTypeOperationalRole; + allowedParentTypeIds?: string[]; +}) { + return apiRequest(`/asset-types/${id}`, { + method: 'PATCH', body: JSON.stringify(input), + }); +} + +export function createAssetAttribute(typeId: string, input: { + code: string; + name: string; + dataType: AssetAttributeDataType; + isRequired: boolean; + unit?: string | null; + options?: string[]; + sortOrder: number; +}) { + return apiRequest(`/asset-types/${typeId}/attributes`, { + method: 'POST', body: JSON.stringify(input), + }); +} + +export function updateAssetAttribute( + typeId: string, + attributeId: string, + input: { + name?: string; + dataType?: AssetAttributeDataType; + isRequired?: boolean; + isActive?: boolean; + unit?: string | null; + options?: string[] | null; + sortOrder?: number; + }, +) { + return apiRequest(`/asset-types/${typeId}/attributes/${attributeId}`, { + method: 'PATCH', body: JSON.stringify(input), + }); +} + +export function listAssets(params: { + page?: number; + pageSize?: number; + search?: string; + typeId?: string; + status?: AssetInformationStatus | ''; + operationalStatus?: AssetOperationalStatus | ''; + needsValidation?: boolean; + hasGeometry?: boolean; + parentId?: string; + operationalAreaId?: string; + operatorCompanyId?: string; +} = {}) { + const query = new URLSearchParams(); + query.set('page', String(params.page ?? 1)); + query.set('pageSize', String(params.pageSize ?? 25)); + if (params.search) query.set('search', params.search); + if (params.typeId) query.set('typeId', params.typeId); + if (params.status) query.set('status', params.status); + if (params.operationalStatus) query.set('operationalStatus', params.operationalStatus); + if (params.needsValidation !== undefined) query.set('needsValidation', String(params.needsValidation)); + if (params.hasGeometry !== undefined) query.set('hasGeometry', String(params.hasGeometry)); + if (params.parentId) query.set('parentId', params.parentId); + if (params.operationalAreaId) query.set('operationalAreaId', params.operationalAreaId); + if (params.operatorCompanyId) query.set('operatorCompanyId', params.operatorCompanyId); + return apiRequest>(`/assets?${query}`); +} + +export async function listAssetTree(params: { + search?: string; + typeId?: string; + status?: AssetInformationStatus | ''; + operationalStatus?: AssetOperationalStatus | ''; + operationalAreaId?: string; + operatorCompanyId?: string; + needsValidation?: boolean; + hasGeometry?: boolean; +} = {}) { + const query = new URLSearchParams(); + if (params.search) query.set('search', params.search); + if (params.typeId) query.set('typeId', params.typeId); + if (params.status) query.set('status', params.status); + if (params.operationalStatus) query.set('operationalStatus', params.operationalStatus); + if (params.operationalAreaId) query.set('operationalAreaId', params.operationalAreaId); + if (params.operatorCompanyId) query.set('operatorCompanyId', params.operatorCompanyId); + if (params.needsValidation !== undefined) query.set('needsValidation', String(params.needsValidation)); + if (params.hasGeometry !== undefined) query.set('hasGeometry', String(params.hasGeometry)); + const suffix = query.toString() ? `?${query}` : ''; + return apiRequest<{ data: AssetListItem[]; meta: { count: number; truncated: boolean } }>(`/assets/tree${suffix}`); +} + +export async function listAssetTreeChildren(params: { + parentId?: string; search?: string; typeId?: string; status?: AssetInformationStatus | ''; + operationalStatus?: AssetOperationalStatus | ''; operationalAreaId?: string; operatorCompanyId?: string; + needsValidation?: boolean; hasGeometry?: boolean; limit?: number; +} = {}) { + const query = new URLSearchParams(); + if (params.parentId) query.set('parentId', params.parentId); + if (params.search) query.set('search', params.search); + if (params.typeId) query.set('typeId', params.typeId); + if (params.status) query.set('status', params.status); + if (params.operationalStatus) query.set('operationalStatus', params.operationalStatus); + if (params.operationalAreaId) query.set('operationalAreaId', params.operationalAreaId); + if (params.operatorCompanyId) query.set('operatorCompanyId', params.operatorCompanyId); + if (params.needsValidation !== undefined) query.set('needsValidation', String(params.needsValidation)); + if (params.hasGeometry !== undefined) query.set('hasGeometry', String(params.hasGeometry)); + query.set('limit', String(params.limit ?? 100)); + return apiRequest<{ data: AssetListItem[]; meta: { count: number; hasMore: boolean; parentId: string | null } }>(`/assets/tree-children?${query}`); +} + +export async function listAssetParentOptions( + childTypeId: string, + assetId?: string, + search?: string, +) { + const query = new URLSearchParams({ childTypeId }); + if (assetId) query.set('assetId', assetId); + if (search) query.set('search', search); + return (await apiRequest<{ data: AssetListItem[] }>(`/assets/parent-options?${query}`)).data; +} + +export function getAsset(id: string) { + return apiRequest(`/assets/${id}`); +} + +export function getAssetDossier(id: string) { + return apiRequest(`/assets/${id}/dossier`); +} + +export async function getAssetLineage(id: string) { + return (await apiRequest<{ data: AssetLineageItem[] }>(`/assets/${id}/lineage`)).data; +} + +export function createAsset(input: { + code: string; + name: string; + commonName?: string | null; + typeId: string; + parentId?: string | null; + operationalAreaId?: string | null; + operatorCompanyId?: string | null; + description?: string | null; + informationStatus?: AssetInformationStatus; + attributes: Record; +}) { + return apiRequest('/assets', { + method: 'POST', body: JSON.stringify(input), + }); +} + +export function updateAsset(id: string, input: { + typeId?: string; + code?: string; + name?: string; + commonName?: string | null; + parentId?: string | null; + operationalAreaId?: string | null; + operatorCompanyId?: string | null; + description?: string | null; + attributes?: Record; +}) { + return apiRequest(`/assets/${id}`, { + method: 'PATCH', body: JSON.stringify(input), + }); +} + +export function updateAssetInformationStatus( + id: string, + informationStatus: AssetInformationStatus, +) { + return apiRequest(`/assets/${id}/information-status`, { + method: 'PATCH', body: JSON.stringify({ informationStatus }), + }); +} + +export function updateAssetOperationalStatus(id: string, status: AssetOperationalStatus) { + return apiRequest(`/assets/${id}/operational-status`, { + method: 'PATCH', body: JSON.stringify({ status }), + }); +} + + +export async function listAssetContextHistory(assetId: string) { + return (await apiRequest<{ data: AssetContextHistoryItem[] }>(`/assets/${assetId}/context-history`)).data; +} + +export function changeAssetContext(assetId: string, input: { + parentId?: string | null; + operationalAreaId?: string | null; + operatorCompanyId?: string | null; + effectiveAt?: string; + reason: string; +}) { + return apiRequest(`/assets/${assetId}/context`, { + method: 'POST', body: JSON.stringify(input), + }); +} + +export function listFieldDiscoveries(params: { page?: number; pageSize?: number; status?: FieldDiscoveryStatus; search?: string } = {}) { + const query = new URLSearchParams(); + query.set('page', String(params.page ?? 1)); + query.set('pageSize', String(params.pageSize ?? 25)); + if (params.status) query.set('status', params.status); + if (params.search) query.set('search', params.search); + return apiRequest>(`/assets/field-discoveries?${query}`); +} + +export function approveFieldDiscovery(id: string, notes?: string | null) { + return apiRequest(`/assets/field-discoveries/${id}/approve`, { method: 'POST', body: JSON.stringify({ notes: notes || null }) }); +} + +export function rejectFieldDiscovery(id: string, reason: string) { + return apiRequest(`/assets/field-discoveries/${id}/reject`, { method: 'POST', body: JSON.stringify({ reason }) }); +} + +export function matchFieldDiscovery(id: string, matchedAssetId: string, reason: string) { + return apiRequest(`/assets/field-discoveries/${id}/match`, { method: 'POST', body: JSON.stringify({ matchedAssetId, reason }) }); +} + +export function getAssetRegistry(assetId: string) { + return apiRequest(`/assets/${assetId}/registry`); +} +export async function listSourceDocuments(search = '') { + const suffix = search.trim() ? `?search=${encodeURIComponent(search.trim())}` : ''; + return (await apiRequest<{ data: SourceDocumentSummary[] }>(`/source-documents${suffix}`)).data; +} +export function createSourceDocument(input: { documentType: SourceDocumentType; documentNumber?: string | null; title: string; issuer?: string | null; documentDate?: string | null; externalReference?: string | null; notes?: string | null }) { + return apiRequest('/source-documents', { method: 'POST', body: JSON.stringify(input) }); +} +export function linkAssetSourceDocument(assetId: string, documentId: string, input: { relationType: AssetSourceDocumentRelationType; notes?: string | null }) { + return apiRequest(`/assets/${assetId}/source-documents/${documentId}`, { method: 'POST', body: JSON.stringify(input) }); +} +export function addAssetExternalIdentifier(assetId: string, input: { namespace: string; value: string; validFrom?: string | null; sourceDocumentId?: string | null; notes?: string | null }) { + return apiRequest(`/assets/${assetId}/external-identifiers`, { method: 'POST', body: JSON.stringify(input) }); +} +export function endAssetExternalIdentifier(id: string, reason: string) { + return apiRequest(`/asset-external-identifiers/${id}/end`, { method: 'POST', body: JSON.stringify({ reason }) }); +} +export function upsertOrganizationProfile(assetId: string, input: { organizationKind: OrganizationKind; legalName?: string | null; taxId?: string | null; notificationEmail?: string | null; notes?: string | null }) { + return apiRequest(`/assets/${assetId}/organization-profile`, { method: 'PATCH', body: JSON.stringify(input) }); +} +export function addOrganizationMembership(organizationId: string, input: { memberOrganizationId: string; role: OrganizationMembershipRole; participationPercent?: number | null; validFrom?: string | null; sourceDocumentId?: string | null; notes?: string | null }) { + return apiRequest(`/organizations/${organizationId}/memberships`, { method: 'POST', body: JSON.stringify(input) }); +} +export function endOrganizationMembership(id: string, reason: string) { + return apiRequest(`/organization-memberships/${id}/end`, { method: 'POST', body: JSON.stringify({ reason }) }); +} +export function createAreaLegalRight(areaId: string, input: { rightType: AreaLegalRightType; name: string; instrumentNumber?: string | null; validFrom?: string | null; validUntil?: string | null; status?: AreaLegalRightStatus; sourceDocumentId?: string | null; notes?: string | null }) { + return apiRequest(`/areas/${areaId}/legal-rights`, { method: 'POST', body: JSON.stringify(input) }); +} +export function addAreaLegalRightOrganization(rightId: string, input: { organizationId: string; role: AreaLegalRightOrganizationRole; participationPercent?: number | null; validFrom?: string | null; notes?: string | null }) { + return apiRequest(`/area-legal-rights/${rightId}/organizations`, { method: 'POST', body: JSON.stringify(input) }); +} +export function endAreaLegalRightOrganization(id: string, reason: string) { + return apiRequest(`/area-legal-right-organizations/${id}/end`, { method: 'POST', body: JSON.stringify({ reason }) }); +} + +export async function getAssetGeometry(assetId: string) { + return (await apiRequest<{ data: AssetGeometry | null }>(`/assets/${assetId}/geometry`)).data; +} + +export function upsertAssetGeometry(assetId: string, input: { + geometry: GeoJsonGeometry; + accuracyM?: number | null; + capturedAt?: string | null; + deviceLabel?: string | null; +}) { + return apiRequest(`/assets/${assetId}/geometry`, { + method: 'PUT', body: JSON.stringify(input), + }); +} + +export function removeAssetGeometry(assetId: string) { + return apiRequest<{ status: 'removed' | 'absent' }>(`/assets/${assetId}/geometry`, { + method: 'DELETE', + }); +} + +export function getMapAssets(params: { + bbox?: string; + typeId?: string; + status?: AssetInformationStatus | ''; + geometryType?: AssetGeometryType | ''; +} = {}) { + const query = new URLSearchParams(); + if (params.bbox) query.set('bbox', params.bbox); + if (params.typeId) query.set('typeId', params.typeId); + if (params.status) query.set('status', params.status); + if (params.geometryType) query.set('geometryType', params.geometryType); + const suffix = query.size ? `?${query}` : ''; + return apiRequest(`/map/assets${suffix}`); +} + +export function listAssetVersions(params: { + page?: number; + pageSize?: number; + search?: string; + typeId?: string; + status?: AssetInformationStatus | ''; + changeType?: AssetVersionChangeType | ''; + from?: string; + to?: string; +} = {}) { + const query = new URLSearchParams(); + query.set('page', String(params.page ?? 1)); + query.set('pageSize', String(params.pageSize ?? 25)); + if (params.search) query.set('search', params.search); + if (params.typeId) query.set('typeId', params.typeId); + if (params.status) query.set('status', params.status); + if (params.changeType) query.set('changeType', params.changeType); + if (params.from) query.set('from', params.from); + if (params.to) query.set('to', params.to); + return apiRequest>(`/asset-versions?${query}`); +} + +export function listAssetVersionTimeline( + assetId: string, + params: { page?: number; pageSize?: number } = {}, +) { + const query = new URLSearchParams({ + page: String(params.page ?? 1), + pageSize: String(params.pageSize ?? 10), + }); + return apiRequest>(`/assets/${assetId}/versions?${query}`); +} + +export function getAssetVersion(assetId: string, versionNumber: number) { + return apiRequest(`/assets/${assetId}/versions/${versionNumber}`); +} + +export async function listAssetMedia(assetId: string) { + return (await apiRequest<{ data: AssetMedia[] }>(`/assets/${assetId}/media`)).data; +} + +export function uploadAssetMedia(assetId: string, input: { + file: File; + kind: AssetMediaKind; + title?: string; + description?: string; + capturedAt?: string; + latitude?: number; + longitude?: number; + accuracyM?: number; +}) { + const body = new FormData(); + body.append('file', input.file); + body.append('kind', input.kind); + if (input.title) body.append('title', input.title); + if (input.description) body.append('description', input.description); + if (input.capturedAt) body.append('capturedAt', input.capturedAt); + if (input.latitude !== undefined) body.append('latitude', String(input.latitude)); + if (input.longitude !== undefined) body.append('longitude', String(input.longitude)); + if (input.accuracyM !== undefined) body.append('accuracyM', String(input.accuracyM)); + return apiRequest(`/assets/${assetId}/media`, { + method: 'POST', + body, + }); +} + +export function updateAssetMedia(mediaId: string, input: { + title?: string | null; + description?: string | null; + capturedAt?: string | null; + latitude?: number | null; + longitude?: number | null; + accuracyM?: number | null; +}) { + return apiRequest(`/asset-media/${mediaId}`, { + method: 'PATCH', + body: JSON.stringify(input), + }); +} + +export function removeAssetMedia(mediaId: string) { + return apiRequest<{ status: 'removed' }>(`/asset-media/${mediaId}`, { + method: 'DELETE', + }); +} + +export function getAssetMediaBlob(mediaId: string, download = false) { + return apiBlobRequest(`/asset-media/${mediaId}/content${download ? '?download=1' : ''}`); +} + +export function getAssetProvenance(assetId: string) { + return apiRequest(`/assets/${assetId}/provenance`); +} + +export function updateAssetProvenance(assetId: string, input: { + origin: AssetDataOrigin; + sourceName?: string | null; + sourceReference?: string | null; + observedAt?: string | null; + notes?: string | null; +}) { + return apiRequest(`/assets/${assetId}/provenance`, { + method: 'PUT', + body: JSON.stringify(input), + }); +} + +export function verifyAssetProvenance(assetId: string) { + return apiRequest(`/assets/${assetId}/provenance/verify`, { + method: 'POST', + }); +} + +export function listTemporalAssets(params: { + at: string; + page?: number; + pageSize?: number; + search?: string; + typeId?: string; + status?: AssetInformationStatus | ''; +}) { + const query = new URLSearchParams({ + at: params.at, + page: String(params.page ?? 1), + pageSize: String(params.pageSize ?? 25), + }); + if (params.search) query.set('search', params.search); + if (params.typeId) query.set('typeId', params.typeId); + if (params.status) query.set('status', params.status); + return apiRequest & { asOf: string }>( + `/temporal-assets?${query}`, + ); +} + +export function getTemporalAsset(assetId: string, at: string) { + const query = new URLSearchParams({ at }); + return apiRequest(`/temporal-assets/${assetId}?${query}`); +} + +export function listSurveyCampaigns(params: { + page?: number; + pageSize?: number; + search?: string; + status?: SurveyCampaignStatus | ''; + coordinatorUserId?: string; +} = {}) { + const query = new URLSearchParams({ + page: String(params.page ?? 1), + pageSize: String(params.pageSize ?? 25), + }); + if (params.search) query.set('search', params.search); + if (params.status) query.set('status', params.status); + if (params.coordinatorUserId) query.set('coordinatorUserId', params.coordinatorUserId); + return apiRequest>(`/survey-campaigns?${query}`); +} + +export function getSurveyCampaign(id: string) { + return apiRequest(`/survey-campaigns/${id}`); +} + +export async function listSurveyAssignees() { + return (await apiRequest<{ data: SurveyPerson[] }>('/survey-campaigns/assignees')).data; +} + +export function createSurveyCampaign(input: { + code: string; + name: string; + description?: string | null; + plannedStartAt?: string | null; + plannedEndAt?: string | null; + scopeAssetId?: string | null; + coordinatorUserId?: string | null; +}) { + return apiRequest('/survey-campaigns', { + method: 'POST', + body: JSON.stringify(input), + }); +} + +export function updateSurveyCampaign(id: string, input: { + code?: string; + name?: string; + description?: string | null; + plannedStartAt?: string | null; + plannedEndAt?: string | null; + scopeAssetId?: string | null; + coordinatorUserId?: string | null; +}) { + return apiRequest(`/survey-campaigns/${id}`, { + method: 'PATCH', + body: JSON.stringify(input), + }); +} + +export function updateSurveyCampaignStatus(id: string, status: SurveyCampaignStatus) { + return apiRequest(`/survey-campaigns/${id}/status`, { + method: 'PUT', + body: JSON.stringify({ status }), + }); +} + +export function addSurveyTarget(campaignId: string, input: { + assetId: string; + assignedUserId?: string | null; + dueAt?: string | null; + instructions?: string | null; +}) { + return apiRequest(`/survey-campaigns/${campaignId}/targets`, { + method: 'POST', + body: JSON.stringify(input), + }); +} + +export function updateSurveyTarget(id: string, input: { + dueAt?: string | null; + instructions?: string | null; +}) { + return apiRequest(`/survey-campaign-targets/${id}`, { + method: 'PATCH', + body: JSON.stringify(input), + }); +} + +export function assignSurveyTarget(id: string, assignedUserId: string | null) { + return apiRequest(`/survey-campaign-targets/${id}/assignment`, { + method: 'PUT', + body: JSON.stringify({ assignedUserId }), + }); +} + +export function updateSurveyTargetStatus(id: string, status: SurveyTargetStatus) { + return apiRequest(`/survey-campaign-targets/${id}/status`, { + method: 'PUT', + body: JSON.stringify({ status }), + }); +} + +export function getSurveyExecution(targetId: string) { + return apiRequest(`/survey-campaign-targets/${targetId}/report`); +} + +export function saveSurveyReport(targetId: string, input: { + outcome: SurveyReportOutcome | null; + observedAt: string | null; + latitude: number | null; + longitude: number | null; + accuracyM: number | null; + notes: string | null; + mediaIds: string[]; +}) { + return apiRequest(`/survey-campaign-targets/${targetId}/report`, { + method: 'PUT', + body: JSON.stringify(input), + }); +} + +export function submitSurveyReport(targetId: string) { + return apiRequest(`/survey-campaign-targets/${targetId}/report/submit`, { + method: 'POST', + }); +} + +export function reviewSurveyReport( + targetId: string, + decision: 'APPROVE' | 'REJECT', + notes: string | null, +) { + return apiRequest(`/survey-campaign-targets/${targetId}/report/review`, { + method: 'POST', + body: JSON.stringify({ decision, notes }), + }); +} + +export function listInspectionVisits(params: { + page?: number; + pageSize?: number; + search?: string; + status?: InspectionVisitStatus | ''; + companyId?: string; + areaId?: string; + inspectorId?: string; + dateFrom?: string; + dateTo?: string; +}) { + const query = new URLSearchParams({ + page: String(params.page ?? 1), + pageSize: String(params.pageSize ?? 25), + }); + if (params.search) query.set('search', params.search); + if (params.status) query.set('status', params.status); + if (params.companyId) query.set('companyId', params.companyId); + if (params.areaId) query.set('areaId', params.areaId); + if (params.inspectorId) query.set('inspectorId', params.inspectorId); + if (params.dateFrom) query.set('dateFrom', params.dateFrom); + if (params.dateTo) query.set('dateTo', params.dateTo); + return apiRequest>(`/inspection-visits?${query}`); +} + +export function getInspectionVisit(id: string) { + return apiRequest(`/inspection-visits/${id}`); +} + +export async function listInspectionAssignees() { + return (await apiRequest<{ data: InspectionPerson[] }>('/inspection-visits/assignees')).data; +} + +export async function listInspectionPlanningAreas() { + return (await apiRequest<{ data: InspectionPlanningContextAsset[] }>('/inspection-visits/planning-context/areas')).data; +} + +export async function listInspectionPlanningOperators(areaId: string) { + return (await apiRequest<{ data: InspectionPlanningContextAsset[] }>(`/inspection-visits/planning-context/areas/${areaId}/operators`)).data; +} + +export function createInspectionVisit(input: { + operationalAreaId: string; + operatorCompanyId: string; + plannedStartAt: string; + leadInspectorUserId: string; +}) { + return apiRequest('/inspection-visits', { + method: 'POST', + body: JSON.stringify(input), + }); +} + +export function updateInspectionVisit(id: string, input: { + objective?: string | null; + scopeAssetId?: string | null; + operationalAreaId?: string | null; + operatorCompanyId?: string | null; + plannedStartAt?: string | null; + instructions?: string | null; +}) { + return apiRequest(`/inspection-visits/${id}`, { + method: 'PATCH', + body: JSON.stringify(input), + }); +} + +export function replaceInspectionVisitAssets(id: string, assetIds: string[]) { + return apiRequest(`/inspection-visits/${id}/assets`, { + method: 'PUT', + body: JSON.stringify({ assetIds }), + }); +} + +export function generateInspectionVisitChecklist(id: string) { + return apiRequest(`/inspection-visits/${id}/checklist/generate`, { + method: 'POST', + }); +} + +export function excludeInspectionVisitAsset(id: string, assetId: string, reason: string) { + return apiRequest(`/inspection-visits/${id}/assets/${assetId}/exclude`, { + method: 'POST', + body: JSON.stringify({ reason }), + }); +} + +export function includeInspectionVisitAsset(id: string, assetId: string) { + return apiRequest(`/inspection-visits/${id}/assets/${assetId}/include`, { + method: 'POST', + }); +} + +export function replaceInspectionVisitTeam( + id: string, + leadInspectorUserId: string | null, + memberUserIds: string[], +) { + return apiRequest(`/inspection-visits/${id}/team`, { + method: 'PUT', + body: JSON.stringify({ leadInspectorUserId, memberUserIds }), + }); +} + +export function updateInspectionVisitStatus( + id: string, + status: InspectionVisitStatus, + reason?: string | null, +) { + return apiRequest(`/inspection-visits/${id}/status`, { + method: 'PUT', + body: JSON.stringify({ status, reason: reason ?? null }), + }); +} + +export function listInspectionActs(visitId: string, params: { + page?: number; + pageSize?: number; + search?: string; + status?: InspectionActStatus | ''; +} = {}) { + const query = new URLSearchParams({ + page: String(params.page ?? 1), + pageSize: String(params.pageSize ?? 25), + }); + if (params.search) query.set('search', params.search); + if (params.status) query.set('status', params.status); + return apiRequest>( + `/inspection-visits/${visitId}/acts?${query}`, + ); +} + +export function listInspectionActsGlobal(params: { + page?: number; + pageSize?: number; + search?: string; + status?: InspectionActStatus | ''; + year?: number | ''; + companyId?: string; + areaId?: string; + inspectorId?: string; + dateFrom?: string; + dateTo?: string; +} = {}) { + const query = new URLSearchParams({ + page: String(params.page ?? 1), + pageSize: String(params.pageSize ?? 25), + }); + if (params.search) query.set('search', params.search); + if (params.status) query.set('status', params.status); + if (params.year) query.set('year', String(params.year)); + if (params.companyId) query.set('companyId', params.companyId); + if (params.areaId) query.set('areaId', params.areaId); + if (params.inspectorId) query.set('inspectorId', params.inspectorId); + if (params.dateFrom) query.set('dateFrom', params.dateFrom); + if (params.dateTo) query.set('dateTo', params.dateTo); + return apiRequest>(`/inspection-acts?${query}`); +} + +export function listInspectionReports(params: { + page?: number; + pageSize?: number; + search?: string; + year?: number | ''; + companyId?: string; + areaId?: string; + inspectorId?: string; + dateFrom?: string; + dateTo?: string; +} = {}) { + const query = new URLSearchParams({ + page: String(params.page ?? 1), + pageSize: String(params.pageSize ?? 25), + }); + if (params.search) query.set('search', params.search); + if (params.year) query.set('year', String(params.year)); + if (params.companyId) query.set('companyId', params.companyId); + if (params.areaId) query.set('areaId', params.areaId); + if (params.inspectorId) query.set('inspectorId', params.inspectorId); + if (params.dateFrom) query.set('dateFrom', params.dateFrom); + if (params.dateTo) query.set('dateTo', params.dateTo); + return apiRequest>(`/inspection-reports?${query}`); +} + +export function listPendingInspectionReports(params: { + page?: number; + pageSize?: number; + search?: string; + year?: number | ''; + companyId?: string; + areaId?: string; + inspectorId?: string; + dateFrom?: string; + dateTo?: string; +} = {}) { + const query = new URLSearchParams({ + page: String(params.page ?? 1), + pageSize: String(params.pageSize ?? 25), + }); + if (params.search) query.set('search', params.search); + if (params.year) query.set('year', String(params.year)); + if (params.companyId) query.set('companyId', params.companyId); + if (params.areaId) query.set('areaId', params.areaId); + if (params.inspectorId) query.set('inspectorId', params.inspectorId); + if (params.dateFrom) query.set('dateFrom', params.dateFrom); + if (params.dateTo) query.set('dateTo', params.dateTo); + return apiRequest>(`/inspection-reports/pending?${query}`); +} + +export function getInspectionReport(id: string) { + return apiRequest(`/inspection-reports/${id}`); +} + +export function getInspectionReportReview(id: string) { + return apiRequest(`/inspection-reports/${id}/review`); +} + +export function uploadInspectionReportRevision(id: string, file: File, changeSummary: string) { + const form = new FormData(); + form.append('file', file); + form.append('changeSummary', changeSummary); + return apiRequest(`/inspection-reports/${id}/review/revisions`, { method: 'POST', body: form }); +} + +export function approveInspectionReport(id: string, note?: string) { + return apiRequest(`/inspection-reports/${id}/review/approve`, { method: 'POST', body: JSON.stringify({ note: note?.trim() || undefined }) }); +} + +export function signFinalInspectionReport(id: string) { + return apiRequest(`/inspection-reports/${id}/review/sign-final`, { method: 'POST', body: JSON.stringify({ confirmation: true }) }); +} + +export function inspectionReportRevisionUrl(revisionId: string) { + return `${API_BASE}/inspection-report-revisions/${revisionId}/content`; +} + +export function getInspectionAct(id: string) { + return apiRequest(`/inspection-acts/${id}`); +} + +export function getInspectionClosure(actId: string) { + return apiRequest(`/inspection-acts/${actId}/closure`); +} + +export function getInspectionSignatureBlob(signatureId: string) { + return apiBlobRequest(`/inspection-act-signatures/${signatureId}/content`); +} + +export function getFindingCatalog(params: { categoryId?: string; search?: string } = {}) { + const query = new URLSearchParams(); + if (params.categoryId) query.set('categoryId', params.categoryId); + if (params.search) query.set('search', params.search); + return apiRequest(`/finding-catalog${query.size ? `?${query}` : ''}`); +} + +export function getFindingCatalogAdmin() { + return apiRequest('/finding-catalog/admin'); +} + +export function getApplicableFindingCatalog(assetId: string, params: { categoryId?: string; search?: string } = {}) { + const query = new URLSearchParams(); + if (params.categoryId) query.set('categoryId', params.categoryId); + if (params.search) query.set('search', params.search); + return apiRequest(`/finding-catalog/applicable/${assetId}${query.size ? `?${query}` : ''}`); +} + +export function getFindingCatalogAssetTypeSelection(assetTypeId: string) { + return apiRequest(`/finding-catalog/asset-types/${assetTypeId}/selection`); +} + +export function replaceFindingCatalogAssetTypeSelection(assetTypeId: string, input: { enabledItemIds: string[]; reason: string }) { + return apiRequest(`/finding-catalog/asset-types/${assetTypeId}/selection`, { + method: 'PUT', + body: JSON.stringify(input), + }); +} + +export function getFindingCatalogAssetSelection(assetId: string) { + return apiRequest(`/finding-catalog/assets/${assetId}/selection`); +} + +export function replaceFindingCatalogAssetSelection(assetId: string, input: { enabledItemIds: string[]; reason: string }) { + return apiRequest(`/finding-catalog/assets/${assetId}/selection`, { + method: 'PUT', + body: JSON.stringify(input), + }); +} + +export function listFindingCatalogProposals(params: { status?: FindingCatalogProposalStatus; assetTypeId?: string } = {}) { + const query = new URLSearchParams(); + if (params.status) query.set('status', params.status); + if (params.assetTypeId) query.set('assetTypeId', params.assetTypeId); + return apiRequest(`/finding-catalog/proposals${query.size ? `?${query}` : ''}`); +} + +export function reviewFindingCatalogProposal(id: string, input: { + decision: 'MATCH' | 'REJECT'; + catalogItemId?: string; + notes?: string | null; +}) { + return apiRequest(`/finding-catalog/proposals/${id}/review`, { + method: 'PATCH', + body: JSON.stringify(input), + }); +} + +export function createFindingCategory(input: { + code: string; + name: string; + sortOrder: number; +}) { + return apiRequest('/finding-catalog/categories', { + method: 'POST', + body: JSON.stringify(input), + }); +} + +export function updateFindingCategory(id: string, input: { + name?: string; + sortOrder?: number; + isActive?: boolean; +}) { + return apiRequest(`/finding-catalog/categories/${id}`, { + method: 'PATCH', + body: JSON.stringify(input), + }); +} + +export interface FindingCatalogItemInput { + categoryId: string; + sourceNumber: number; + title: string; + legalBasis?: string | null; + glossary?: string | null; + importNote?: string | null; + suggestedSeverity?: number | null; +} + +export function createFindingCatalogItem(input: FindingCatalogItemInput & { code: string }) { + return apiRequest('/finding-catalog/items', { + method: 'POST', + body: JSON.stringify(input), + }); +} + +export function updateFindingCatalogItem(id: string, input: FindingCatalogItemInput & { + isActive: boolean; +}) { + return apiRequest(`/finding-catalog/items/${id}`, { + method: 'PATCH', + body: JSON.stringify(input), + }); +} + +export async function listInspectionFindings(actId: string) { + return (await apiRequest<{ data: InspectionFinding[] }>( + `/inspection-acts/${actId}/findings`, + )).data; +} + +export function listInspectionFindingsGlobal(params: { + page?: number; + pageSize?: number; + search?: string; + workflow?: InspectionFindingWorkflow; + companyId?: string; + areaId?: string; + inspectorId?: string; + dateFrom?: string; + dateTo?: string; +} = {}) { + const query = new URLSearchParams({ + page: String(params.page ?? 1), + pageSize: String(params.pageSize ?? 25), + }); + if (params.search) query.set('search', params.search); + if (params.workflow) query.set('workflow', params.workflow); + if (params.companyId) query.set('companyId', params.companyId); + if (params.areaId) query.set('areaId', params.areaId); + if (params.inspectorId) query.set('inspectorId', params.inspectorId); + if (params.dateFrom) query.set('dateFrom', params.dateFrom); + if (params.dateTo) query.set('dateTo', params.dateTo); + return apiRequest(`/inspection-findings?${query}`); +} + +export function getInspectionFinding(id: string) { + return apiRequest(`/inspection-findings/${id}`); +} + +export function getInspectionFindingVerificationHistory(id: string) { + return apiRequest(`/inspection-findings/${id}/verification-history`); +} + +export function listVerificationPlanning(params: { + page?: number; + pageSize?: number; + search?: string; + planningStatus?: VerificationPlanningStatus; + companyId?: string; + areaId?: string; + dueFrom?: string; + dueTo?: string; +} = {}) { + const query = new URLSearchParams({ + page: String(params.page ?? 1), + pageSize: String(params.pageSize ?? 50), + }); + if (params.search) query.set('search', params.search); + if (params.planningStatus) query.set('planningStatus', params.planningStatus); + if (params.companyId) query.set('companyId', params.companyId); + if (params.areaId) query.set('areaId', params.areaId); + if (params.dueFrom) query.set('dueFrom', params.dueFrom); + if (params.dueTo) query.set('dueTo', params.dueTo); + return apiRequest(`/inspection-verifications?${query}`); +} + +export function planVerificationVisit(input: { + findingIds: string[]; + plannedStartAt: string; + notes?: string | null; +}) { + return apiRequest('/inspection-verifications/plan', { + method: 'POST', + body: JSON.stringify(input), + }); +} + +export function recordVerificationResult(visitId: string, findingId: string, input: { + outcome: InspectionVerificationOutcome; + notes: string; + verifiedAt?: string; + nextControlOn?: string; +}) { + return apiRequest<{ + findingId: string; + visitId: string; + targetControlOn: string | null; + outcome: InspectionVerificationOutcome; + notes: string; + verifiedAt: string; + recordedAt: string; + rescheduledControlOn: string | null; + nextControlOn: string | null; + findingStatus: InspectionFindingStatus; + visitStatus: InspectionVisitStatus; + }>(`/inspection-verifications/visits/${visitId}/findings/${findingId}/result`, { + method: 'PATCH', + body: JSON.stringify(input), + }); +} + +export function closeInspectionFinding(id: string, closureNotes: string) { + return apiRequest(`/inspection-findings/${id}/close`, { + method: 'PATCH', + body: JSON.stringify({ closureNotes }), + }); +} + +export function updateInspectionFindingFollowUp(id: string, input: { + responseDueBasis?: 'FINDING_DATE' | 'REPORT_NOTIFICATION' | null; + responseDueDays?: number | null; + reportNotifiedOn?: string | null; + companyResponse?: string | null; + companyResponseReceivedOn?: string | null; + companyCommittedCorrectionOn?: string | null; + nextControlOn?: string | null; +}) { + return apiRequest(`/inspection-findings/${id}/follow-up`, { + method: 'PATCH', + body: JSON.stringify(input), + }); +} + +export async function listInspectionFindingEvidence(findingId: string) { + return (await apiRequest<{ data: InspectionFindingEvidence[] }>( + `/inspection-findings/${findingId}/evidence`, + )).data; +} + +export function uploadInspectionFindingEvidence(findingId: string, input: { + file: File; + kind: InspectionEvidenceKind; + purpose: InspectionEvidencePurpose; + communicationId?: string; + verificationVisitId?: string; + title?: string; + description?: string; + capturedAt?: string; + latitude?: number; + longitude?: number; + accuracyM?: number; + deviceLabel?: string; +}) { + const body = new FormData(); + body.append('file', input.file); + body.append('kind', input.kind); + body.append('purpose', input.purpose); + if (input.communicationId) body.append('communicationId', input.communicationId); + if (input.verificationVisitId) body.append('verificationVisitId', input.verificationVisitId); + if (input.title) body.append('title', input.title); + if (input.description) body.append('description', input.description); + if (input.capturedAt) body.append('capturedAt', input.capturedAt); + if (input.latitude !== undefined) body.append('latitude', String(input.latitude)); + if (input.longitude !== undefined) body.append('longitude', String(input.longitude)); + if (input.accuracyM !== undefined) body.append('accuracyM', String(input.accuracyM)); + if (input.deviceLabel) body.append('deviceLabel', input.deviceLabel); + return apiRequest( + `/inspection-findings/${findingId}/evidence`, + { method: 'POST', body }, + ); +} + +export function getInspectionFindingEvidenceBlob(evidenceId: string, download = false) { + return apiBlobRequest( + `/inspection-finding-evidence/${evidenceId}/content${download ? '?download=1' : ''}`, + ); +} + +export async function listInspectionFindingCommunications(findingId: string) { + return (await apiRequest<{ data: InspectionFindingCommunication[] }>( + `/inspection-findings/${findingId}/communications`, + )).data; +} + +export function createInspectionFindingCommunication(findingId: string, input: { + direction: InspectionFindingCommunication['direction']; + channel: InspectionFindingCommunication['channel']; + type: InspectionFindingCommunication['type']; + occurredAt: string; + subject: string; + details?: string | null; + contactName?: string | null; + contactEmail?: string | null; +}) { + return apiRequest( + `/inspection-findings/${findingId}/communications`, + { method: 'POST', body: JSON.stringify(input) }, + ); +} + +export function listAssetImportBatches(params: { page?: number; pageSize?: number; status?: AssetImportBatchStatus | '' } = {}) { + const query = new URLSearchParams({ page: String(params.page ?? 1), pageSize: String(params.pageSize ?? 20) }); + if (params.status) query.set('status', params.status); + return apiRequest>(`/asset-imports?${query}`); +} + +export function listAssetImportReviews(params: { page?: number; pageSize?: number; kind?: 'ALL' | 'DIRECT' | 'DEPENDENCY' } = {}) { + const query = new URLSearchParams(); + if (params.page) query.set('page', String(params.page)); + if (params.pageSize) query.set('pageSize', String(params.pageSize)); + if (params.kind && params.kind !== 'ALL') query.set('kind', params.kind); + const suffix = query.toString() ? `?${query}` : ''; + return apiRequest<{ data: AssetImportReviewItem[]; meta: { page: number; pageSize: number; total: number; totalPages: number } }>(`/asset-imports/reviews${suffix}`); +} + +export function getAssetImportBatch(id: string) { + return apiRequest(`/asset-imports/${id}`); +} + +export function listAssetImportRows(id: string, params: { page?: number; pageSize?: number; status?: AssetImportRowStatus | ''; search?: string } = {}) { + const query = new URLSearchParams({ page: String(params.page ?? 1), pageSize: String(params.pageSize ?? 50) }); + if (params.status) query.set('status', params.status); + if (params.search) query.set('search', params.search); + return apiRequest>(`/asset-imports/${id}/rows?${query}`); +} + +export function uploadAssetImport(input: { file: File; sourceLabel?: string; notes?: string; profileCode?: Exclude }) { + const body = new FormData(); + body.append('file', input.file); + if (input.sourceLabel?.trim()) body.append('sourceLabel', input.sourceLabel.trim()); + if (input.notes?.trim()) body.append('notes', input.notes.trim()); + if (input.profileCode) body.append('profileCode', input.profileCode); + return apiRequest('/asset-imports', { method: 'POST', body }); +} + +export function reconcileAssetImport(id: string) { + return apiRequest(`/asset-imports/${id}/reconcile`, { method: 'POST' }); +} + +export function listAssetImportOrganizations(search?: string) { + const query = new URLSearchParams(); + if (search?.trim()) query.set('search', search.trim()); + const suffix = query.toString() ? `?${query}` : ''; + return apiRequest<{ data: AssetImportOrganizationOption[] }>(`/asset-imports/context/organizations${suffix}`); +} + +export function getAssetImportPlan(id: string) { + return apiRequest(`/asset-imports/${id}/plan`); +} + +export function generateAssetImportPlan(id: string, input: { operatorAssetId?: string; externalIdNamespace?: string }) { + return apiRequest(`/asset-imports/${id}/plan`, { method: 'POST', body: JSON.stringify(input) }); +} + +export function listAssetImportPlanItems(id: string, input: { entityKind?: AssetImportPlanEntityKind; action?: AssetImportPlanAction; page?: number; pageSize?: number } = {}) { + const query = new URLSearchParams(); + if (input.entityKind) query.set('entityKind', input.entityKind); + if (input.action) query.set('action', input.action); + if (input.page) query.set('page', String(input.page)); + if (input.pageSize) query.set('pageSize', String(input.pageSize)); + const suffix = query.toString() ? `?${query}` : ''; + return apiRequest<{ data: AssetImportPlanItem[]; meta: { page: number; pageSize: number; total: number; totalPages: number } }>(`/asset-imports/${id}/plan/items${suffix}`); +} + +export function resolveAssetImportPlanItem(id: string, itemId: string, input: { action: 'CREATE' | 'MATCH' | 'IGNORE'; matchedAssetId?: string; reason: string }) { + return apiRequest(`/asset-imports/${id}/plan/items/${itemId}/resolve`, { method: 'POST', body: JSON.stringify(input) }); +} + +export function applySafeAssetImportPlan(id: string, planHash: string) { + return apiRequest(`/asset-imports/${id}/apply-safe`, { method: 'POST', body: JSON.stringify({ planHash }) }); +} + +export function applyAssetImportPlan(id: string, planHash: string) { + return apiRequest(`/asset-imports/${id}/apply`, { method: 'POST', body: JSON.stringify({ planHash }) }); +} + +export function rollbackAssetImportPlan(id: string, reason: string) { + return apiRequest(`/asset-imports/${id}/rollback`, { method: 'POST', body: JSON.stringify({ reason }) }); +} + +export function cancelAssetImport(id: string, reason: string) { + return apiRequest(`/asset-imports/${id}/cancel`, { method: 'POST', body: JSON.stringify({ reason }) }); +} + + +export function inspectionReportWordUrl(id: string) { + return `${API_BASE}/inspection-reports/${id}/word`; +} + +export function generateInspectionReportWord(id: string) { + return apiRequest(`/inspection-reports/${id}/word`, { method: 'POST' }); +} + + +export interface DocumentDeliverySettings { officeEmail:string|null; directorEmail:string|null; updatedAt:string; smtpConfigured:boolean; mailFrom:string|null; } +export interface DocumentDeliveryItem { id:string; actId:string; reportId:string|null; documentKind:'ACT_PDF'|'REPORT_WORD'; recipientKind:'COMPANY'|'OFFICE'|'DIRECTOR'; recipientAssetId:string|null; recipientAssetName:string|null; recipientEmail:string|null; status:'PENDING'|'WAITING_RECIPIENT'|'WAITING_TRANSPORT'|'WAITING_ARTIFACT'|'SENT'|'FAILED'; attempts:number; lastAttemptAt:string|null; sentAt:string|null; providerMessageId:string|null; lastError:string|null; createdAt:string; actCode:string; reportCode:string|null; } +export function getDocumentDeliverySettings(){return apiRequest('/document-delivery/settings');} +export function updateDocumentDeliverySettings(input:{officeEmail?:string|null;directorEmail?:string|null}){return apiRequest('/document-delivery/settings',{method:'PATCH',body:JSON.stringify(input)});} +export function listDocumentDeliveries(){return apiRequest<{data:DocumentDeliveryItem[]}>('/document-delivery/outbox');} +export function retryDocumentDelivery(id:string){return apiRequest(`/document-delivery/outbox/${id}/retry`,{method:'POST'});} +export function retryPendingDocumentDeliveries(){return apiRequest<{processed:number}>('/document-delivery/outbox/retry-pending',{method:'POST'});} + +export type ActAdministrationState = 'NEW' | 'WAITING_RESPONSE' | 'DUE_SOON' | 'OVERDUE' | 'RESPONSE_RECEIVED' | 'VERIFICATION_PENDING' | 'COMMITMENT_OVERDUE' | 'REGULARIZED'; +export interface ActAdministrationQueueItem { + actId: string; actCode: string; actStatus: string; occurredAt: string; closedAt: string | null; + visitId: string; visitCode: string; areaId: string | null; companyId: string | null; areaName: string | null; companyName: string | null; + responseDueOn: string | null; deadlineReason: string | null; responseReceivedOn: string | null; committedCorrectionOn: string | null; + findingCount: number; openFindingCount: number; scheduledControlCount: number; adminState: ActAdministrationState; +} +export interface ActAdministrationQueuePage { data: ActAdministrationQueueItem[]; counters: Partial>; meta: PageMeta } +export interface ActAdministrationDetail { + act: ActAdministrationQueueItem; + deadlines: Array<{ id:string; responseDueOn:string; reason:string; createdAt:string }>; + responses: Array<{ id:string; receivedOn:string; details:string|null; committedCorrectionOn:string|null; contactName:string|null; contactEmail:string|null; originalName:string|null; sizeBytes:number|null; sha256:string|null; createdAt:string }>; + findings: Array<{ id:string; code:string; title:string; status:string; nextControlOn:string|null }>; +} +export function listActAdministrationQueue(params: { state?: ActAdministrationState | 'ALL'; areaId?: string; companyId?: string; page?: number; pageSize?: number } = {}) { + const q = new URLSearchParams({ page: String(params.page ?? 1), pageSize: String(params.pageSize ?? 25) }); + if (params.state && params.state !== 'ALL') q.set('state', params.state); + if (params.areaId) q.set('areaId', params.areaId); + if (params.companyId) q.set('companyId', params.companyId); + return apiRequest(`/act-administration/queue?${q}`); +} +export function getActAdministration(actId: string) { return apiRequest(`/inspection-acts/${actId}/administration`); } +export function setActResponseDeadline(actId: string, input: { responseDueOn: string; reason: string }) { return apiRequest(`/inspection-acts/${actId}/administration/deadline`, { method: 'PATCH', body: JSON.stringify(input) }); } +export function addActCompanyResponse(actId: string, input: { receivedOn: string; details?: string; committedCorrectionOn?: string; contactName?: string; contactEmail?: string; file?: File }) { + const body = new FormData(); body.append('receivedOn', input.receivedOn); + if (input.details) body.append('details', input.details); if (input.committedCorrectionOn) body.append('committedCorrectionOn', input.committedCorrectionOn); + if (input.contactName) body.append('contactName', input.contactName); if (input.contactEmail) body.append('contactEmail', input.contactEmail); if (input.file) body.append('file', input.file); + return apiRequest(`/inspection-acts/${actId}/administration/responses`, { method: 'POST', body }); +} +export function actCompanyResponseContentUrl(responseId: string) { return `${API_BASE}/act-company-responses/${responseId}/content`; } +export function listActAdministrationCalendar(from?: string, to?: string) { const q=new URLSearchParams(); if(from)q.set('from',from); if(to)q.set('to',to); return apiRequest<{from:string;to:string;data:Array<{type:'RESPONSE_DUE'|'COMMITMENT_DUE';date:string;actId:string;actCode:string;areaName:string|null;companyName:string|null;state:ActAdministrationState}>}>(`/act-administration/calendar${q.size?`?${q}`:''}`); } +