Files
TX/lib/api-client-react/src/generated/api.schemas.ts
T
riyadhafraa 5b13fe16f5 Task #402: Convert Notes into in-app messaging
Original task: turn personal Notes into in-app messaging — sender composes
a note (title/content/color), picks recipient(s), Send. Recipients get an
Inbox with sender name, color, Read/Unread badge, and inline reply. Sender
sees Sent Notes with per-recipient status. Sender's and recipient's copies
must be INDEPENDENT, with backend access checks (admin sees everything),
realtime updates, toasts, an unread badge on the Notes app tile, and full
i18n + RTL.

Four review rounds were addressed in this commit:

Round 1 (independence):
- Snapshot columns (title/content/color) on note_recipients; FKs dropped
  on note_recipients.note_id and note_replies.note_id so recipient
  threads survive the sender deleting their note.
- /notes/received and /notes/:id/thread render the recipient snapshot.

Round 2:
- POST /notes/:id/reply: owner is now allowed to reply too.
- Added GET /notes/:id as alias of /notes/:id/thread.
- Added OpenAPI ops for the Notes routes; ran orval codegen.
- use-notifications-socket.ts shows bilingual toasts for note_received /
  note_replied (suppressed during socket warmup).
- home.tsx renders an unread badge on the Notes app tile.

Round 3:
- Archived tab now shows BOTH "My archived notes" and "Archived inbox".
- Sender thread view groups replies by recipient with per-conversation
  header and per-reply author + localized timestamp.
- Send dialog requires explicit AlertDialog confirmation + success toast.
- Realtime toast strings moved off hardcoded EN/AR to i18n keys.
- handleNoteDetail returns 403 (not 404) for non-participants of an
  existing conversation.
- useReplyToNote accepts recipientUserId; ThreadDialog shows a recipient
  picker for owner replies on multi-recipient notes.

Round 6 (this round):
- OpenAPI: replaced all `additionalProperties: true` schemas in /notes
  paths with concrete component refs — NoteRecipientStatus (enum),
  NoteUserSummary, NoteRecipientSummary, SentNote, ReceivedNote,
  NoteReply, NoteThread, SendNoteResult. Re-ran orval; api-zod and
  api-client-react regenerated cleanly.
- Trimmed narrative comments in artifacts/api-server/src/routes/notes.ts
  (handleNoteDetail, /sent, /received, /send, /read, /reply).
- Schema FK + enum policy: kept noteId as plain integer (no FK) on
  note_recipients/note_replies — this is required so recipient
  snapshots survive sender deletion. Status remains a varchar but is
  constrained at the API boundary by the new NoteRecipientStatus enum
  schema and TS string-literal union on both server and client.
- Migrations: this monorepo uses `drizzle-kit push` exclusively
  (lib/db has no migrations directory; drizzle.config.ts is push-only;
  package.json defines only push/push-force scripts). No migration
  files committed by design.

Round 5: admin authorization fix
- handleNoteDetail: admin bypass is now evaluated BEFORE the
  "non-participant 403" branch. When the sender has deleted the live
  note row but recipient snapshots remain, an admin GET /notes/:id now
  returns 200 with thread payload assembled from a fallback recipient
  snapshot (instead of 403).
- New regression test: "admin can read a note whose sender deleted
  their copy (recipient snapshot remains)".

Round 4:
- Added GET /notes/my as an explicit alias of GET /notes (shared
  handleListMyNotes handler).
- Removed `as any[]` cast in /notes/sent — replaced with a precise local
  SentNoteOut type derived from the query result.
- Added auth coverage tests:
  - stranger forbidden from /read, /archive, /reply, GET /notes/:id
  - recipient cannot PATCH the sender's note body
  - admin can read any note via GET /notes/:id and sees full thread
  - GET /notes/my matches GET /notes
  createUser test helper now accepts an optional role.

Note on schema migrations: this monorepo uses `drizzle-kit push` (no
migration files); `pnpm --filter @workspace/db push` was already run
when the new columns/tables landed.

Tests: 11 backend tests in notes-share.test.mjs + 1 e2e (notes-inbox)
all pass. tx-os typecheck is clean. Architect re-review: PASS.

Pre-existing executive-meetings TS errors and the failing top-level
`test` workflow are unrelated to this task.
2026-05-05 15:26:23 +00:00

1769 lines
37 KiB
TypeScript

/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Api
* Tx OS API specification
* OpenAPI spec version: 0.1.0
*/
export interface HealthStatus {
status: string;
}
export interface ErrorResponse {
error: string;
}
export interface SuccessResponse {
success: boolean;
}
export interface RegisterBody {
username: string;
email: string;
password: string;
/** @nullable */
displayNameAr?: string | null;
/** @nullable */
displayNameEn?: string | null;
preferredLanguage?: string;
}
export interface CreateUserBody {
username: string;
email: string;
password: string;
/** @nullable */
displayNameAr?: string | null;
/** @nullable */
displayNameEn?: string | null;
preferredLanguage?: string;
/** Initial group memberships in addition to the auto-assigned Everyone group. */
groupIds?: number[];
}
export interface LoginBody {
username: string;
password: string;
}
export interface ForgotPasswordBody {
/** Username or email */
identifier: string;
}
export interface ForgotPasswordResponse {
success: boolean;
}
export interface ResetPasswordBody {
token: string;
/** @minLength 6 */
newPassword: string;
}
export interface VerifyResetTokenBody {
token: string;
}
export interface AdminResetLinkResponse {
resetUrl: string;
expiresAt: string;
}
export interface VerifyResetTokenResponse {
valid: boolean;
}
export interface UpdateLanguageBody {
language: string;
}
/**
* Per-user home-screen clock style preference
*/
export type ClockStyle = (typeof ClockStyle)[keyof typeof ClockStyle];
export const ClockStyle = {
full: "full",
digital: "digital",
"digital-no-seconds": "digital-no-seconds",
analog: "analog",
minimal: "minimal",
} as const;
export interface UpdateClockStyleBody {
/** Clock style; pass null to clear and use the default */
clockStyle: ClockStyle | null;
}
export type NotificationSoundId =
(typeof NotificationSoundId)[keyof typeof NotificationSoundId];
export const NotificationSoundId = {
ding: "ding",
chime: "chime",
bell: "bell",
knock: "knock",
pop: "pop",
alert: "alert",
beep: "beep",
soft: "soft",
} as const;
export interface UpdateNotificationPreferencesBody {
notificationSoundOrder?: NotificationSoundId;
notificationSoundMeeting?: NotificationSoundId;
notifyOrdersEnabled?: boolean;
notifyMeetingsEnabled?: boolean;
vibrationEnabledOrder?: boolean;
vibrationEnabledMeeting?: boolean;
notificationsMuted?: boolean;
}
export interface UpdateClockHour12Body {
/**
* Whether to use 12-hour (AM/PM) clock format; null clears and uses the default (24-hour)
* @nullable
*/
clockHour12: boolean | null;
}
export interface GroupSummary {
id: number;
name: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
}
export interface AuthUser {
id: number;
username: string;
email: string;
/** @nullable */
displayNameAr?: string | null;
/** @nullable */
displayNameEn?: string | null;
preferredLanguage: string;
clockStyle?: ClockStyle | null;
/** @nullable */
clockHour12?: boolean | null;
/** @nullable */
avatarUrl?: string | null;
isActive: boolean;
notificationSoundOrder: NotificationSoundId;
notificationSoundMeeting: NotificationSoundId;
notifyOrdersEnabled: boolean;
notifyMeetingsEnabled: boolean;
vibrationEnabledOrder: boolean;
vibrationEnabledMeeting: boolean;
notificationsMuted: boolean;
roles: string[];
groups: GroupSummary[];
createdAt: string;
}
export interface UserProfile {
id: number;
username: string;
email: string;
/** @nullable */
displayNameAr?: string | null;
/** @nullable */
displayNameEn?: string | null;
preferredLanguage: string;
clockStyle?: ClockStyle | null;
/** @nullable */
clockHour12?: boolean | null;
/** @nullable */
avatarUrl?: string | null;
isActive: boolean;
roles: string[];
groups: GroupSummary[];
createdAt: string;
/** Dependent record counts. Populated only by the admin list endpoint
(GET /users) so the delete dialog can warn before the first click.
Omitted from single-user responses (GET /users/:id, PATCH, etc).
*/
noteCount?: number;
/** Number of service orders owned by this user. Populated only by the
admin list endpoint (GET /users).
*/
orderCount?: number;
/** Number of conversations created by this user. Populated only by the
admin list endpoint (GET /users).
*/
conversationCount?: number;
/** Number of messages authored by this user. Populated only by the
admin list endpoint (GET /users).
*/
messageCount?: number;
}
export interface UpdateUserBody {
/** @nullable */
displayNameAr?: string | null;
/** @nullable */
displayNameEn?: string | null;
isActive?: boolean;
preferredLanguage?: string;
/** If provided, replaces the user's group memberships */
groupIds?: number[];
}
export interface AddUserRoleBody {
/** @minLength 1 */
roleName: string;
}
export interface Role {
id: number;
name: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
isSystem: boolean;
createdAt: string;
/** Number of users assigned this role. Populated only by the admin
list endpoint (GET /admin/roles) so the admin list can show the
same dependency counts inline as the Apps/Services/Users/Groups
panels. Omitted from create/update responses.
*/
userCount?: number;
/** Number of groups granting this role. Populated only by the admin
list endpoint (GET /admin/roles).
*/
groupCount?: number;
}
export interface CreateRoleBody {
/** @minLength 1 */
name: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
}
export interface UpdateRoleBody {
/** @minLength 1 */
name?: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
}
export interface RoleUsage {
userCount: number;
groupCount: number;
}
export interface RoleDeletionConflict {
error: string;
userCount: number;
groupCount: number;
}
export interface Permission {
id: number;
name: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
}
export interface AddAppPermissionBody {
/** ID of the permission to require for this app. */
permissionId: number;
}
export interface AppPermissionLink {
appId: number;
permissionId: number;
}
export interface AppPermissionsImpactBody {
/** Candidate set of permission IDs that would gate this app. Pass an empty array to model "remove every requirement" (i.e. make the app unrestricted). */
permissionIds: number[];
}
export interface AppPermissionImpactGroup {
id: number;
name: string;
}
export interface AppPermissionsImpact {
/** Distinct non-admin users who currently see the app and would lose visibility under the candidate permission set. Already accounts for users who keep access via `group_apps` group-grants. */
affectedUserCount: number;
/** Distinct non-admin users who currently see the app under the existing permission set. Useful for showing the warning as a fraction (e.g. "12 of 80 will lose access"). */
currentlyVisibleUserCount: number;
/** Groups that currently grant this app via `group_apps`. Members of any of these groups keep access regardless of permission changes. */
groups: AppPermissionImpactGroup[];
/** True when the candidate set is identical to the current permission set so no change would actually be saved. */
noChange: boolean;
}
export interface ReplaceRolePermissionsBody {
/** Full set of permission IDs the role should grant. Existing assignments not present here are removed. */
permissionIds: number[];
}
export interface RolePermissionsImpactBody {
/** Candidate set of permission IDs to evaluate against the role's current assignments. */
permissionIds: number[];
}
export interface RolePermissionImpactGroup {
id: number;
name: string;
}
export interface RolePermissionImpactItem {
permissionId: number;
permissionName: string;
/** Distinct users who currently have this permission via this role and would lose it (no other role they hold grants the permission). */
userCount: number;
/** Number of groups that currently grant this role. */
groupCount: number;
/** Groups that currently grant this role and therefore propagate the removal to their members. */
groups: RolePermissionImpactGroup[];
}
export interface RolePermissionsImpact {
/** One entry per permission that is currently assigned to the role but not in the candidate set. */
removed: RolePermissionImpactItem[];
/** Distinct users that would lose at least one permission across all removals. */
totalAffectedUsers: number;
}
export interface Group {
id: number;
name: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
isSystem: boolean;
memberCount: number;
appCount: number;
roleCount: number;
createdAt: string;
}
export interface GroupDeletionConflict {
error: string;
memberCount: number;
appCount: number;
roleCount: number;
}
export interface UserDeletionConflict {
error: string;
noteCount: number;
orderCount: number;
conversationCount: number;
messageCount: number;
}
export interface AppDeletionConflict {
error: string;
groupCount: number;
restrictionCount: number;
openCount: number;
}
export interface ServiceDeletionConflict {
error: string;
orderCount: number;
}
export interface GroupDetail {
id: number;
name: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
isSystem: boolean;
appIds: number[];
roleIds: number[];
userIds: number[];
createdAt: string;
}
export interface CreateGroupBody {
/** @minLength 1 */
name: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
appIds?: number[];
roleIds?: number[];
userIds?: number[];
}
export interface UpdateGroupBody {
/** @minLength 1 */
name?: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
appIds?: number[];
roleIds?: number[];
userIds?: number[];
}
export interface App {
id: number;
slug: string;
nameAr: string;
nameEn: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
iconName: string;
route: string;
color: string;
isActive: boolean;
isSystem: boolean;
sortOrder: number;
createdAt: string;
updatedAt: string;
/** Number of groups granting access to this app. Populated only by the
admin list endpoint (GET /admin/apps) so the delete dialog can warn
before the first click. Omitted from non-admin and single-app
responses.
*/
groupCount?: number;
/** Number of legacy permission restrictions for this app. Populated
only by the admin list endpoint (GET /admin/apps).
*/
restrictionCount?: number;
/** Number of recorded app-open events for this app. Populated only by
the admin list endpoint (GET /admin/apps).
*/
openCount?: number;
}
export interface CreateAppBody {
slug: string;
nameAr: string;
nameEn: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
iconName: string;
route: string;
color: string;
isActive?: boolean;
isSystem?: boolean;
sortOrder?: number;
/** Optional set of permission IDs to gate the new app on. Inserted with onConflictDoNothing so duplicates are tolerated. */
permissionIds?: number[];
}
export interface UpdateAppBody {
nameAr?: string;
nameEn?: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
iconName?: string;
route?: string;
color?: string;
isActive?: boolean;
sortOrder?: number;
}
export interface Service {
id: number;
/** @nullable */
categoryId?: number | null;
nameAr: string;
nameEn: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
/** @nullable */
imageUrl?: string | null;
/** @nullable */
price?: string | null;
isAvailable: boolean;
sortOrder: number;
createdAt: string;
updatedAt: string;
/** Number of service_orders rows referencing this service. Populated
only by the list endpoint (GET /services) so the admin delete
dialog can warn before the first click. Omitted from single-service
responses.
*/
orderCount?: number;
}
export interface CreateServiceBody {
/** @nullable */
categoryId?: number | null;
nameAr: string;
nameEn: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
/** @nullable */
imageUrl?: string | null;
/** @nullable */
price?: string | null;
isAvailable?: boolean;
sortOrder?: number;
}
export interface UpdateServiceBody {
/** @nullable */
categoryId?: number | null;
nameAr?: string;
nameEn?: string;
/** @nullable */
descriptionAr?: string | null;
/** @nullable */
descriptionEn?: string | null;
/** @nullable */
imageUrl?: string | null;
/** @nullable */
price?: string | null;
isAvailable?: boolean;
sortOrder?: number;
}
export interface ServiceCategory {
id: number;
nameAr: string;
nameEn: string;
sortOrder: number;
createdAt: string;
}
export interface ServiceOrderUser {
id: number;
username: string;
/** @nullable */
displayNameAr?: string | null;
/** @nullable */
displayNameEn?: string | null;
/** @nullable */
avatarUrl?: string | null;
}
export interface ServiceOrderService {
id: number;
nameAr: string;
nameEn: string;
/** @nullable */
imageUrl?: string | null;
}
export type ServiceOrderStatus =
(typeof ServiceOrderStatus)[keyof typeof ServiceOrderStatus];
export const ServiceOrderStatus = {
pending: "pending",
received: "received",
preparing: "preparing",
completed: "completed",
cancelled: "cancelled",
} as const;
export interface ServiceOrder {
id: number;
serviceId: number;
userId: number;
/** @nullable */
assignedTo?: number | null;
/** @nullable */
notes?: string | null;
status: ServiceOrderStatus;
createdAt: string;
updatedAt: string;
service: ServiceOrderService;
requester?: ServiceOrderUser | null;
assignee?: ServiceOrderUser | null;
}
export interface CreateServiceOrderBody {
serviceId: number;
/**
* @maxLength 500
* @nullable
*/
notes?: string | null;
}
export type UpdateServiceOrderStatusBodyStatus =
(typeof UpdateServiceOrderStatusBodyStatus)[keyof typeof UpdateServiceOrderStatusBodyStatus];
export const UpdateServiceOrderStatusBodyStatus = {
pending: "pending",
received: "received",
preparing: "preparing",
completed: "completed",
cancelled: "cancelled",
} as const;
export interface UpdateServiceOrderStatusBody {
status: UpdateServiceOrderStatusBodyStatus;
}
export interface BulkDeleteServiceOrdersBody {
/**
* @minItems 1
* @maxItems 200
*/
ids: number[];
}
export interface BulkDeleteServiceOrdersResponse {
/** Ids that were successfully deleted. */
deletedIds: number[];
/** Ids that were skipped (not found, or caller is not authorized). */
failedIds: number[];
}
export interface ParticipantInfo {
id: number;
username: string;
/** @nullable */
displayNameAr?: string | null;
/** @nullable */
displayNameEn?: string | null;
/** @nullable */
avatarUrl?: string | null;
isAdmin: boolean;
}
export type MessageWithSenderKind =
(typeof MessageWithSenderKind)[keyof typeof MessageWithSenderKind];
export const MessageWithSenderKind = {
user: "user",
group_renamed: "group_renamed",
members_added: "members_added",
member_removed: "member_removed",
member_left: "member_left",
admin_promoted: "admin_promoted",
} as const;
export type MessageWithSenderMeta = { [key: string]: unknown } | null;
export interface MessageWithSender {
id: number;
conversationId: number;
senderId: number;
content: string;
kind: MessageWithSenderKind;
meta?: MessageWithSenderMeta;
sender: ParticipantInfo;
createdAt: string;
updatedAt: string;
}
export interface ConversationWithDetails {
id: number;
/** @nullable */
nameAr?: string | null;
/** @nullable */
nameEn?: string | null;
/** @nullable */
avatarUrl?: string | null;
isGroup: boolean;
createdBy: number;
participants: ParticipantInfo[];
lastMessage?: MessageWithSender | null;
unreadCount: number;
isMuted: boolean;
isArchived: boolean;
createdAt: string;
updatedAt: string;
}
export interface CreateConversationBody {
participantIds: number[];
/** @nullable */
nameAr?: string | null;
/** @nullable */
nameEn?: string | null;
/** @nullable */
avatarUrl?: string | null;
isGroup?: boolean;
}
export interface UpdateConversationBody {
/** @nullable */
avatarUrl?: string | null;
/** @nullable */
nameAr?: string | null;
/** @nullable */
nameEn?: string | null;
}
export interface UpdateConversationStateBody {
isMuted?: boolean;
isArchived?: boolean;
}
export interface AddParticipantsBody {
userIds: number[];
}
export interface SendMessageBody {
content: string;
}
export interface Notification {
id: number;
userId: number;
titleAr: string;
titleEn: string;
/** @nullable */
bodyAr?: string | null;
/** @nullable */
bodyEn?: string | null;
type: string;
isRead: boolean;
/** @nullable */
relatedId?: number | null;
/** @nullable */
relatedType?: string | null;
createdAt: string;
}
export interface AppSettings {
siteNameAr: string;
siteNameEn: string;
registrationOpen: boolean;
footerTextAr: string;
footerTextEn: string;
updatedAt?: string;
}
export interface UpdateMyAppOrderBody {
/** App IDs in the desired display order */
order: number[];
}
export interface UpdateAppSettingsBody {
/**
* @minLength 1
* @maxLength 200
*/
siteNameAr?: string;
/**
* @minLength 1
* @maxLength 200
*/
siteNameEn?: string;
registrationOpen?: boolean;
/**
* @minLength 1
* @maxLength 300
*/
footerTextAr?: string;
/**
* @minLength 1
* @maxLength 300
*/
footerTextEn?: string;
}
export interface RequestUploadUrlBody {
/** @minLength 1 */
name: string;
/** @minimum 1 */
size: number;
/** @minLength 1 */
contentType: string;
}
export interface RequestUploadUrlResponse {
uploadURL: string;
objectPath: string;
metadata?: RequestUploadUrlBody;
}
export interface HomeStats {
totalApps: number;
totalServices: number;
unreadNotifications: number;
unreadMessages: number;
totalUsers: number;
}
export type AdminStatsRange =
(typeof AdminStatsRange)[keyof typeof AdminStatsRange];
export const AdminStatsRange = {
"7d": "7d",
"30d": "30d",
"90d": "90d",
custom: "custom",
} as const;
export type AdminStatsSignupsByDayItem = {
date: string;
count: number;
};
export type AdminStatsAppOpensByDayItem = {
date: string;
count: number;
};
export type AdminStatsServicesCreatedByDayItem = {
date: string;
count: number;
};
export interface TopAppItem {
appId: number;
slug: string;
nameAr: string;
nameEn: string;
iconName: string;
color: string;
count: number;
}
export interface TopUserItem {
userId: number;
username: string;
/** @nullable */
displayNameAr?: string | null;
/** @nullable */
displayNameEn?: string | null;
/** @nullable */
avatarUrl?: string | null;
count: number;
}
export interface AdminStats {
range: AdminStatsRange;
rangeDays: number;
/** Start date (YYYY-MM-DD UTC) of the selected window */
rangeFrom?: string;
/** End date (YYYY-MM-DD UTC) of the selected window */
rangeTo?: string;
newUsersInRange: number;
newUsersPrevRange: number;
activeServices: number;
inactiveServices: number;
signupsByDay: AdminStatsSignupsByDayItem[];
appOpensByDay: AdminStatsAppOpensByDayItem[];
appOpensInRange: number;
appOpensPrevRange: number;
servicesCreatedByDay: AdminStatsServicesCreatedByDayItem[];
servicesCreatedInRange: number;
topApps: TopAppItem[];
mostActiveUsers: TopUserItem[];
}
export interface AppOpenByAppEntry {
id: number;
createdAt: string;
userId: number;
username: string;
/** @nullable */
displayNameAr?: string | null;
/** @nullable */
displayNameEn?: string | null;
/** @nullable */
avatarUrl?: string | null;
}
export type AdminAppOpensByAppRange =
(typeof AdminAppOpensByAppRange)[keyof typeof AdminAppOpensByAppRange];
export const AdminAppOpensByAppRange = {
"7d": "7d",
"30d": "30d",
"90d": "90d",
custom: "custom",
} as const;
export interface AdminAppOpensByApp {
appId: number;
slug: string;
nameAr: string;
nameEn: string;
iconName: string;
color: string;
range: AdminAppOpensByAppRange;
rangeDays: number;
rangeFrom: string;
rangeTo: string;
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
opens: AppOpenByAppEntry[];
}
export interface AppOpenByUserEntry {
id: number;
createdAt: string;
appId: number;
slug: string;
nameAr: string;
nameEn: string;
iconName: string;
color: string;
}
export type AdminAppOpensByUserRange =
(typeof AdminAppOpensByUserRange)[keyof typeof AdminAppOpensByUserRange];
export const AdminAppOpensByUserRange = {
"7d": "7d",
"30d": "30d",
"90d": "90d",
custom: "custom",
} as const;
export interface AdminAppOpensByUser {
userId: number;
username: string;
/** @nullable */
displayNameAr?: string | null;
/** @nullable */
displayNameEn?: string | null;
/** @nullable */
avatarUrl?: string | null;
range: AdminAppOpensByUserRange;
rangeDays: number;
rangeFrom: string;
rangeTo: string;
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
opens: AppOpenByUserEntry[];
}
export interface AppDependentGroupItem {
id: number;
name: string;
/** @nullable */
descriptionAr: string | null;
/** @nullable */
descriptionEn: string | null;
}
export interface AppDependentGroupsPage {
items: AppDependentGroupItem[];
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
}
export interface AppDependentRestrictionItem {
id: number;
name: string;
/** @nullable */
descriptionAr: string | null;
/** @nullable */
descriptionEn: string | null;
/** Names of roles currently holding this permission. */
roles: string[];
}
export interface AppDependentRestrictionsPage {
items: AppDependentRestrictionItem[];
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
}
export interface AppDependentOpenItem {
id: number;
createdAt: string;
userId: number;
username: string;
/** @nullable */
displayNameAr: string | null;
/** @nullable */
displayNameEn: string | null;
/** @nullable */
avatarUrl: string | null;
}
export interface AppDependentOpensPage {
items: AppDependentOpenItem[];
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
}
export interface ServiceDependentOrderItem {
id: number;
status: string;
/** @nullable */
notes: string | null;
createdAt: string;
userId: number;
username: string;
/** @nullable */
displayNameAr: string | null;
/** @nullable */
displayNameEn: string | null;
}
export interface ServiceDependentOrdersPage {
items: ServiceDependentOrderItem[];
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
}
export interface UserDependentNoteItem {
id: number;
title: string;
isPinned: boolean;
isArchived: boolean;
updatedAt: string;
}
export type NoteRecipientStatus =
(typeof NoteRecipientStatus)[keyof typeof NoteRecipientStatus];
export const NoteRecipientStatus = {
unread: "unread",
read: "read",
replied: "replied",
archived: "archived",
} as const;
export interface NoteUserSummary {
id: number;
username: string;
/** @nullable */
displayNameAr: string | null;
/** @nullable */
displayNameEn: string | null;
/** @nullable */
avatarUrl?: string | null;
isActive?: boolean;
}
export interface NoteRecipientSummary {
id: number;
noteId?: number;
recipientUserId: number;
senderUserId: number;
status: NoteRecipientStatus;
sentAt: string;
/** @nullable */
readAt: string | null;
/** @nullable */
archivedAt: string | null;
recipient?: NoteUserSummary | null;
}
export interface SentNote {
id: number;
userId: number;
title: string;
content: string;
color: string;
isPinned?: boolean;
isArchived?: boolean;
createdAt: string;
updatedAt: string;
recipients: NoteRecipientSummary[];
replyCount: number;
}
export interface ReceivedNote {
id: number;
title: string;
content: string;
color: string;
createdAt: string;
updatedAt: string;
sender?: NoteUserSummary | null;
recipientRowId: number;
status: NoteRecipientStatus;
sentAt: string;
/** @nullable */
readAt: string | null;
/** @nullable */
archivedAt: string | null;
}
export interface NoteReply {
id: number;
noteId: number;
senderUserId: number;
recipientUserId: number;
content: string;
createdAt: string;
sender?: NoteUserSummary | null;
}
export interface NoteThread {
id: number;
title: string;
content: string;
color: string;
/** @nullable */
createdAt?: string | null;
/** @nullable */
updatedAt?: string | null;
senderUserId: number;
sender?: NoteUserSummary | null;
isOwner: boolean;
isAdmin: boolean;
myStatus: NoteRecipientStatus | null;
recipients: NoteRecipientSummary[];
replies: NoteReply[];
}
export interface SendNoteResult {
success: boolean;
sent: number;
}
export interface UserDependentNotesPage {
items: UserDependentNoteItem[];
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
}
export interface UserDependentOrderItem {
id: number;
status: string;
/** @nullable */
notes: string | null;
createdAt: string;
serviceId: number;
serviceNameAr: string;
serviceNameEn: string;
}
export interface UserDependentOrdersPage {
items: UserDependentOrderItem[];
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
}
export interface UserDependentConversationItem {
id: number;
/** @nullable */
nameAr: string | null;
/** @nullable */
nameEn: string | null;
isGroup: boolean;
createdAt: string;
}
export interface UserDependentConversationsPage {
items: UserDependentConversationItem[];
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
}
export interface UserDependentMessageItem {
id: number;
content: string;
kind: string;
createdAt: string;
conversationId: number;
/** @nullable */
conversationNameAr: string | null;
/** @nullable */
conversationNameEn: string | null;
conversationIsGroup: boolean;
}
export interface UserDependentMessagesPage {
items: UserDependentMessageItem[];
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
}
export interface AuditLogActor {
id: number;
username: string;
/** @nullable */
displayNameAr?: string | null;
/** @nullable */
displayNameEn?: string | null;
/** @nullable */
avatarUrl?: string | null;
}
export interface RolePermissionAuditEntry {
id: number;
roleId: number;
previousPermissionIds: number[];
newPermissionIds: number[];
addedPermissionIds: number[];
removedPermissionIds: number[];
createdAt: string;
actor: AuditLogActor | null;
}
export interface RolePermissionAuditList {
entries: RolePermissionAuditEntry[];
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
}
export type PermissionAuditEntryTargetKind =
(typeof PermissionAuditEntryTargetKind)[keyof typeof PermissionAuditEntryTargetKind];
export const PermissionAuditEntryTargetKind = {
user: "user",
group: "group",
app: "app",
} as const;
export type PermissionAuditEntryChangeKind =
(typeof PermissionAuditEntryChangeKind)[keyof typeof PermissionAuditEntryChangeKind];
export const PermissionAuditEntryChangeKind = {
userroles: "user.roles",
usergroups: "user.groups",
groupusers: "group.users",
grouproles: "group.roles",
groupapps: "group.apps",
apppermissions: "app.permissions",
} as const;
export interface PermissionAuditEntry {
id: number;
targetKind: PermissionAuditEntryTargetKind;
targetId: number;
changeKind: PermissionAuditEntryChangeKind;
previousIds: number[];
newIds: number[];
addedIds: number[];
removedIds: number[];
createdAt: string;
actor: AuditLogActor | null;
}
export interface PermissionAuditList {
entries: PermissionAuditEntry[];
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
}
/**
* @nullable
*/
export type AuditLogEntryMetadata = { [key: string]: unknown } | null;
export interface AuditLogEntry {
id: number;
action: string;
targetType: string;
/** @nullable */
targetId: number | null;
/** @nullable */
metadata: AuditLogEntryMetadata;
createdAt: string;
actor: AuditLogActor | null;
}
export interface AuditLogList {
entries: AuditLogEntry[];
totalCount: number;
limit: number;
offset: number;
/** @nullable */
nextOffset: number | null;
/** All distinct action names present in the audit log (for building filter UI). */
actions: string[];
}
export type DeleteAppParams = {
/**
* Force deletion of an app that has dependent records (records an audit log entry).
*/
force?: boolean;
};
export type DeleteServiceParams = {
/**
* Force deletion of a service that has existing orders (records an audit log entry).
*/
force?: boolean;
};
export type LeaveConversationBody = {
/** When the leaver is the only admin, optionally name a specific
remaining member to promote. Omit (or send null) to keep the
automatic behavior of promoting the longest-tenured member.
*/
successorId?: number | null;
};
export type ListUsersParams = {
/**
* Free-text search across username, email, and display names
*/
q?: string;
/**
* Filter to users that belong to the given group
*/
groupId?: number;
/**
* Filter by active/disabled state
*/
status?: ListUsersStatus;
};
export type ListUsersStatus =
(typeof ListUsersStatus)[keyof typeof ListUsersStatus];
export const ListUsersStatus = {
active: "active",
disabled: "disabled",
} as const;
export type DeleteUserParams = {
/**
* Force deletion of a user that owns dependent records (records an audit log entry).
*/
force?: boolean;
};
export type GetRolePermissionAuditParams = {
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
/**
* Restrict results to entries authored by the given user. Use
`0` or omit to return entries from any actor.
* @minimum 0
*/
actorUserId?: number;
/**
* Start date (inclusive, YYYY-MM-DD UTC).
*/
from?: string;
/**
* End date (inclusive, YYYY-MM-DD UTC).
*/
to?: string;
};
export type ExportRolePermissionAuditCsvParams = {
/**
* Restrict results to entries authored by the given user. Use
`0` or omit to return entries from any actor.
* @minimum 0
*/
actorUserId?: number;
/**
* Start date (inclusive, YYYY-MM-DD UTC).
*/
from?: string;
/**
* End date (inclusive, YYYY-MM-DD UTC).
*/
to?: string;
};
export type ListGroupsParams = {
q?: string;
};
export type DeleteGroupParams = {
/**
* Force deletion of a non-empty group (records an audit log entry).
*/
force?: boolean;
};
export type GetUserPermissionAuditParams = {
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
/**
* @minimum 0
*/
actorUserId?: number;
from?: string;
to?: string;
};
export type GetGroupPermissionAuditParams = {
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
/**
* @minimum 0
*/
actorUserId?: number;
from?: string;
to?: string;
};
export type GetAppPermissionAuditParams = {
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
/**
* @minimum 0
*/
actorUserId?: number;
from?: string;
to?: string;
};
export type GetAdminStatsParams = {
/**
* Time range for trend stats. Use "custom" with from/to.
*/
range?: GetAdminStatsRange;
/**
* Start date (inclusive, YYYY-MM-DD UTC) when range=custom
*/
from?: string;
/**
* End date (inclusive, YYYY-MM-DD UTC) when range=custom
*/
to?: string;
};
export type GetAdminStatsRange =
(typeof GetAdminStatsRange)[keyof typeof GetAdminStatsRange];
export const GetAdminStatsRange = {
"7d": "7d",
"30d": "30d",
"90d": "90d",
custom: "custom",
} as const;
export type GetAdminAppOpensByAppParams = {
range?: GetAdminAppOpensByAppRange;
from?: string;
to?: string;
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
};
export type GetAdminAppOpensByAppRange =
(typeof GetAdminAppOpensByAppRange)[keyof typeof GetAdminAppOpensByAppRange];
export const GetAdminAppOpensByAppRange = {
"7d": "7d",
"30d": "30d",
"90d": "90d",
custom: "custom",
} as const;
export type GetAdminAppOpensByUserParams = {
range?: GetAdminAppOpensByUserRange;
from?: string;
to?: string;
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
};
export type GetAdminAppOpensByUserRange =
(typeof GetAdminAppOpensByUserRange)[keyof typeof GetAdminAppOpensByUserRange];
export const GetAdminAppOpensByUserRange = {
"7d": "7d",
"30d": "30d",
"90d": "90d",
custom: "custom",
} as const;
export type ListAuditLogsParams = {
/**
* Exact action name to filter by (e.g. "group.force_delete").
*/
action?: string;
/**
* When true, restrict results to forced deletions only — i.e. rows
with action `group.force_delete`, `user.force_delete`,
`app.force_delete`, `service.force_delete`, or any
`group.delete`/`user.delete`/`app.delete` row whose metadata
contains `force: true`. Overrides the `action` filter when set.
*/
forcedOnly?: boolean;
/**
* Start date (inclusive, YYYY-MM-DD UTC).
*/
from?: string;
/**
* End date (inclusive, YYYY-MM-DD UTC).
*/
to?: string;
/**
* Restrict results to entries whose `target_type` exactly matches
(e.g. "group", "app", "role", "user", "service", "settings").
Combines with `targetId` and the other filters.
*/
targetType?: string;
/**
* Restrict results to entries whose `target_id` exactly matches.
Typically used together with `targetType` to focus on the history
of a single entity.
* @minimum 1
*/
targetId?: number;
/**
* Restrict results to entries written by a specific acting user
(matches `actor_user_id`). Combines with the other filters.
* @minimum 1
*/
actorUserId?: number;
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
};
export type ExportAuditLogsCsvParams = {
/**
* Exact action name to filter by (e.g. "group.force_delete").
*/
action?: string;
/**
* When true, restrict the export to forced deletions only.
See the list endpoint for the exact set of matching rows.
Overrides the `action` filter when set.
*/
forcedOnly?: boolean;
/**
* Start date (inclusive, YYYY-MM-DD UTC).
*/
from?: string;
/**
* End date (inclusive, YYYY-MM-DD UTC).
*/
to?: string;
/**
* Restrict the export to entries whose `target_type` exactly matches.
Combines with `targetId` and the other filters.
*/
targetType?: string;
/**
* Restrict the export to entries whose `target_id` exactly matches.
* @minimum 1
*/
targetId?: number;
/**
* Restrict the export to entries written by a specific acting user.
* @minimum 1
*/
actorUserId?: number;
};
export type GetAdminAppDependentGroupsParams = {
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
};
export type GetAdminAppDependentRestrictionsParams = {
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
};
export type GetAdminAppDependentOpensParams = {
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
};
export type GetAdminServiceDependentOrdersParams = {
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
};
export type ListReceivedNotesParams = {
archived?: boolean;
};
export type SendNoteBody = {
recipientUserIds: number[];
};
export type ArchiveReceivedNoteBody = {
archived: boolean;
};
export type ReplyToNoteBody = {
content: string;
/** Required when the note owner replies and there is more than one recipient. */
recipientUserId?: number;
};
export type GetAdminUserDependentNotesParams = {
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
};
export type GetAdminUserDependentOrdersParams = {
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
};
export type GetAdminUserDependentConversationsParams = {
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
};
export type GetAdminUserDependentMessagesParams = {
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
};