7c9edf6cb6
Apply the existing groups delete-warning pattern to user, app, and
service deletions so admins are warned (and have to confirm twice)
before destroying records that still own dependent data.
Backend
- openapi.yaml: added `force` query param to DELETE /users/{id},
/apps/{id}, /services/{id} plus three new conflict schemas
(UserDeletionConflict, AppDeletionConflict, ServiceDeletionConflict).
- routes/users.ts, apps.ts, services.ts: rewrote DELETE handlers to
count dependents (notes/orders/conversations/messages for users;
group_apps/restrictions/open events for apps; service_orders for
services), return 409 with counts when non-empty and force is not set,
and on `?force=true` perform the cascade in a transaction and write
an `*.force_delete` audit_logs row.
- Existing 204 success path preserved for empty deletes.
UI (artifacts/tx-os/src/pages/admin.tsx)
- New `DeletionWarningDialog` helper, plus app/service/user delete state
+ lazy 409 detection (first click probes; on 409 the dialog upgrades
to show counts + "Delete anyway"; second click sends ?force=true).
- Replaced the three plain `confirm(t("admin.deleteConfirm"))` callsites.
i18n
- Added admin.deleteApp/deleteService/deleteUser keys (title, warning,
forceHint, emptyBody, count keys, confirm, anyway) in en.json + ar.json.
Tests
- artifacts/api-server/tests/delete-force-warnings.test.mjs covers all
9 cases (clean delete, 409 with counts, force=true 204 + audit log)
for users, apps, and services. Existing groups-crud and service-orders
tests still pass.
Notes / drift
- Lazy detection (vs eager counts on the row like Groups already does)
was chosen because list endpoints don't return counts yet — proposed
follow-up #96 covers eager counts so the warning appears on first
click everywhere.
- E2E test for the UI flow flaked twice; backend integration tests
(9/9 pass), direct curl validation of force=true returning 204, and
typecheck across the monorepo all confirm correctness.
Replit-Task-Id: 91404d92-e74c-4720-8fc9-8eb772eefc33
986 lines
20 KiB
TypeScript
986 lines
20 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 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;
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
export interface CreateRoleBody {
|
|
/** @minLength 1 */
|
|
name: string;
|
|
/** @nullable */
|
|
descriptionAr?: string | null;
|
|
/** @nullable */
|
|
descriptionEn?: string | null;
|
|
}
|
|
|
|
export interface UpdateRoleBody {
|
|
/** @nullable */
|
|
descriptionAr?: string | null;
|
|
/** @nullable */
|
|
descriptionEn?: string | null;
|
|
}
|
|
|
|
export interface RoleDeletionConflict {
|
|
error: string;
|
|
userCount: number;
|
|
groupCount: 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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 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 AuditLogActor {
|
|
id: number;
|
|
username: string;
|
|
/** @nullable */
|
|
displayNameAr?: string | null;
|
|
/** @nullable */
|
|
displayNameEn?: string | null;
|
|
/** @nullable */
|
|
avatarUrl?: string | 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 ListGroupsParams = {
|
|
q?: string;
|
|
};
|
|
|
|
export type DeleteGroupParams = {
|
|
/**
|
|
* Force deletion of a non-empty group (records an audit log entry).
|
|
*/
|
|
force?: boolean;
|
|
};
|
|
|
|
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;
|
|
/**
|
|
* Start date (inclusive, YYYY-MM-DD UTC).
|
|
*/
|
|
from?: string;
|
|
/**
|
|
* End date (inclusive, YYYY-MM-DD UTC).
|
|
*/
|
|
to?: string;
|
|
/**
|
|
* @minimum 1
|
|
* @maximum 200
|
|
*/
|
|
limit?: number;
|
|
/**
|
|
* @minimum 0
|
|
*/
|
|
offset?: number;
|
|
};
|