Files
TX/lib/api-zod/src/generated/api.ts
T
riyadhafraa 376918f983 Drag-to-reorder service cards (iOS jiggle mode)
Long-press a service tile (~500 ms) → grid enters edit mode: every
card jiggles, a floating "تم / Done" pill appears, and cards become
draggable via @dnd-kit. Drop on another tile to reorder. Tap Done or
outside the grid to persist; the new order is shared globally via
servicesTable.sortOrder.

Backend
- POST /api/services/reorder (admin-gated, requireAdmin)
- Validates full-set match (no missing/dup/unknown IDs)
- Transactional sortOrder rewrite — no partial writes
- Route registered before /services/:id to avoid path-param collision
- OpenAPI op + ReorderServicesBody Zod schema → regenerated client

Frontend
- artifacts/tx-os/src/pages/services.tsx rewritten
- PointerSensor (distance 6) + TouchSensor (delay 150 ms) — long-press
  and tap-to-open stay distinct
- touch-none only applied while in edit mode so normal scrolling is
  unaffected outside it
- exitEditMode awaits mutation + invalidate before flipping editMode,
  preventing snap-back from stale react-query cache
- exitingRef guard against duplicate exit calls (Done button +
  outside-tap handler firing for the same gesture)
- Non-admins can enter edit mode (haptic affordance) but local reorder
  is silently reverted on exit
- i18n: services.editMode.{done,hint,saveFailed} added to ar.json/en.json

Pre-existing unrelated typecheck errors in push.ts and
executive-meeting-font-settings.ts are not touched by this change.
2026-05-20 11:51:28 +00:00

3540 lines
99 KiB
TypeScript

/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Api
* Tx OS API specification
* OpenAPI spec version: 0.1.0
*/
import * as zod from "zod";
/**
* @summary Health check
*/
export const HealthCheckResponse = zod.object({
status: zod.string(),
});
/**
* Returns the local build version (read from version.json embedded at
build time) and, if `LATEST_VERSION_URL` is configured on the
server, the latest published version fetched from that URL. Admin
only. Never performs an update — pure read.
* @summary Get current and latest system version
*/
export const GetSystemVersionResponse = zod.object({
current: zod.object({
version: zod.string(),
build: zod.string(),
}),
latest: zod.union([
zod.object({
version: zod.string(),
build: zod.string(),
}),
zod.null(),
]),
status: zod.enum([
"up-to-date",
"update-available",
"not-configured",
"error",
]),
errorMessage: zod.string().nullable(),
checkedAt: zod.coerce.date().nullable(),
configured: zod.boolean(),
startedAt: zod.coerce
.date()
.describe(
"ISO-8601 timestamp captured when the API process booted.\nFrozen for the lifetime of the process — used by the admin\nSystem Updates panel as visible proof that the container\nactually restarted (independent of build\/version changes).\n",
),
});
/**
* @summary Register a new user
*/
export const registerBodyPreferredLanguageDefault = `ar`;
export const RegisterBody = zod.object({
username: zod.string(),
email: zod.string(),
password: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
preferredLanguage: zod.string().default(registerBodyPreferredLanguageDefault),
});
/**
* @summary Login
*/
export const LoginBody = zod.object({
username: zod.string(),
password: zod.string(),
});
export const LoginResponse = zod.object({
id: zod.number(),
username: zod.string(),
email: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
preferredLanguage: zod.string(),
clockStyle: zod
.union([
zod
.enum(["full", "digital", "digital-no-seconds", "analog", "minimal"])
.describe("Per-user home-screen clock style preference"),
zod.null(),
])
.optional(),
clockHour12: zod.boolean().nullish(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean(),
notificationSoundOrder: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notificationSoundMeeting: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notificationSoundNote: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notifyOrdersEnabled: zod.boolean(),
notifyMeetingsEnabled: zod.boolean(),
notifyNotesEnabled: zod.boolean(),
vibrationEnabledOrder: zod.boolean(),
vibrationEnabledMeeting: zod.boolean(),
vibrationEnabledNote: zod.boolean(),
notificationsMuted: zod.boolean(),
roles: zod.array(zod.string()),
groups: zod.array(
zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
}),
),
createdAt: zod.coerce.date(),
});
/**
* @summary Logout
*/
export const LogoutResponse = zod.object({
success: zod.boolean(),
});
/**
* @summary Request a password reset link
*/
export const ForgotPasswordBody = zod.object({
identifier: zod.string().describe("Username or email"),
});
export const ForgotPasswordResponse = zod.object({
success: zod.boolean(),
});
/**
* @summary Set a new password using a reset token
*/
export const resetPasswordBodyNewPasswordMin = 6;
export const ResetPasswordBody = zod.object({
token: zod.string(),
newPassword: zod.string().min(resetPasswordBodyNewPasswordMin),
});
export const ResetPasswordResponse = zod.object({
success: zod.boolean(),
});
/**
* @summary Check whether a reset token is valid
*/
export const VerifyResetTokenBody = zod.object({
token: zod.string(),
});
export const VerifyResetTokenResponse = zod.object({
valid: zod.boolean(),
});
/**
* @summary Admin generates a one-time password reset link for a user
*/
export const AdminIssueResetLinkParams = zod.object({
id: zod.coerce.number(),
});
export const AdminIssueResetLinkResponse = zod.object({
resetUrl: zod.string(),
expiresAt: zod.coerce.date(),
});
/**
* @summary Get current user
*/
export const GetMeResponse = zod.object({
id: zod.number(),
username: zod.string(),
email: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
preferredLanguage: zod.string(),
clockStyle: zod
.union([
zod
.enum(["full", "digital", "digital-no-seconds", "analog", "minimal"])
.describe("Per-user home-screen clock style preference"),
zod.null(),
])
.optional(),
clockHour12: zod.boolean().nullish(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean(),
notificationSoundOrder: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notificationSoundMeeting: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notificationSoundNote: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notifyOrdersEnabled: zod.boolean(),
notifyMeetingsEnabled: zod.boolean(),
notifyNotesEnabled: zod.boolean(),
vibrationEnabledOrder: zod.boolean(),
vibrationEnabledMeeting: zod.boolean(),
vibrationEnabledNote: zod.boolean(),
notificationsMuted: zod.boolean(),
roles: zod.array(zod.string()),
groups: zod.array(
zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
}),
),
createdAt: zod.coerce.date(),
});
/**
* @summary Update preferred language
*/
export const UpdateLanguageBody = zod.object({
language: zod.string(),
});
export const UpdateLanguageResponse = zod.object({
id: zod.number(),
username: zod.string(),
email: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
preferredLanguage: zod.string(),
clockStyle: zod
.union([
zod
.enum(["full", "digital", "digital-no-seconds", "analog", "minimal"])
.describe("Per-user home-screen clock style preference"),
zod.null(),
])
.optional(),
clockHour12: zod.boolean().nullish(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean(),
notificationSoundOrder: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notificationSoundMeeting: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notificationSoundNote: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notifyOrdersEnabled: zod.boolean(),
notifyMeetingsEnabled: zod.boolean(),
notifyNotesEnabled: zod.boolean(),
vibrationEnabledOrder: zod.boolean(),
vibrationEnabledMeeting: zod.boolean(),
vibrationEnabledNote: zod.boolean(),
notificationsMuted: zod.boolean(),
roles: zod.array(zod.string()),
groups: zod.array(
zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
}),
),
createdAt: zod.coerce.date(),
});
/**
* @summary Update preferred home-screen clock style
*/
export const UpdateClockStyleBody = zod.object({
clockStyle: zod
.union([
zod
.enum(["full", "digital", "digital-no-seconds", "analog", "minimal"])
.describe("Per-user home-screen clock style preference"),
zod.null(),
])
.describe("Clock style; pass null to clear and use the default"),
});
export const UpdateClockStyleResponse = zod.object({
id: zod.number(),
username: zod.string(),
email: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
preferredLanguage: zod.string(),
clockStyle: zod
.union([
zod
.enum(["full", "digital", "digital-no-seconds", "analog", "minimal"])
.describe("Per-user home-screen clock style preference"),
zod.null(),
])
.optional(),
clockHour12: zod.boolean().nullish(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean(),
notificationSoundOrder: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notificationSoundMeeting: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notificationSoundNote: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notifyOrdersEnabled: zod.boolean(),
notifyMeetingsEnabled: zod.boolean(),
notifyNotesEnabled: zod.boolean(),
vibrationEnabledOrder: zod.boolean(),
vibrationEnabledMeeting: zod.boolean(),
vibrationEnabledNote: zod.boolean(),
notificationsMuted: zod.boolean(),
roles: zod.array(zod.string()),
groups: zod.array(
zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
}),
),
createdAt: zod.coerce.date(),
});
/**
* @summary Update notification sound, vibration & mute preferences
*/
export const UpdateNotificationPreferencesBody = zod.object({
notificationSoundOrder: zod
.enum(["ding", "chime", "bell", "knock", "pop", "alert", "beep", "soft"])
.optional(),
notificationSoundMeeting: zod
.enum(["ding", "chime", "bell", "knock", "pop", "alert", "beep", "soft"])
.optional(),
notificationSoundNote: zod
.enum(["ding", "chime", "bell", "knock", "pop", "alert", "beep", "soft"])
.optional(),
notifyOrdersEnabled: zod.boolean().optional(),
notifyMeetingsEnabled: zod.boolean().optional(),
notifyNotesEnabled: zod.boolean().optional(),
vibrationEnabledOrder: zod.boolean().optional(),
vibrationEnabledMeeting: zod.boolean().optional(),
vibrationEnabledNote: zod.boolean().optional(),
notificationsMuted: zod.boolean().optional(),
});
export const UpdateNotificationPreferencesResponse = zod.object({
id: zod.number(),
username: zod.string(),
email: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
preferredLanguage: zod.string(),
clockStyle: zod
.union([
zod
.enum(["full", "digital", "digital-no-seconds", "analog", "minimal"])
.describe("Per-user home-screen clock style preference"),
zod.null(),
])
.optional(),
clockHour12: zod.boolean().nullish(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean(),
notificationSoundOrder: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notificationSoundMeeting: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notificationSoundNote: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notifyOrdersEnabled: zod.boolean(),
notifyMeetingsEnabled: zod.boolean(),
notifyNotesEnabled: zod.boolean(),
vibrationEnabledOrder: zod.boolean(),
vibrationEnabledMeeting: zod.boolean(),
vibrationEnabledNote: zod.boolean(),
notificationsMuted: zod.boolean(),
roles: zod.array(zod.string()),
groups: zod.array(
zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
}),
),
createdAt: zod.coerce.date(),
});
/**
* @summary Update preferred 12/24-hour clock format
*/
export const UpdateClockHour12Body = zod.object({
clockHour12: zod
.boolean()
.nullable()
.describe(
"Whether to use 12-hour (AM\/PM) clock format; null clears and uses the default (24-hour)",
),
});
export const UpdateClockHour12Response = zod.object({
id: zod.number(),
username: zod.string(),
email: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
preferredLanguage: zod.string(),
clockStyle: zod
.union([
zod
.enum(["full", "digital", "digital-no-seconds", "analog", "minimal"])
.describe("Per-user home-screen clock style preference"),
zod.null(),
])
.optional(),
clockHour12: zod.boolean().nullish(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean(),
notificationSoundOrder: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notificationSoundMeeting: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notificationSoundNote: zod.enum([
"ding",
"chime",
"bell",
"knock",
"pop",
"alert",
"beep",
"soft",
]),
notifyOrdersEnabled: zod.boolean(),
notifyMeetingsEnabled: zod.boolean(),
notifyNotesEnabled: zod.boolean(),
vibrationEnabledOrder: zod.boolean(),
vibrationEnabledMeeting: zod.boolean(),
vibrationEnabledNote: zod.boolean(),
notificationsMuted: zod.boolean(),
roles: zod.array(zod.string()),
groups: zod.array(
zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
}),
),
createdAt: zod.coerce.date(),
});
/**
* @summary List all active apps
*/
export const ListAppsResponseItem = zod.object({
id: zod.number(),
slug: zod.string(),
nameAr: zod.string(),
nameEn: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
iconName: zod.string(),
imageUrl: zod
.string()
.nullish()
.describe(
'Optional uploaded image used in place of the Lucide icon on the\nlauncher tile. Stored as the object-storage path returned by the\nupload presigner (e.g. \"\/objects\/<id>\").\n',
),
route: zod.string(),
externalUrl: zod
.string()
.nullish()
.describe(
'For openMode \"external_tab\" or \"external_iframe\": the URL the\nlauncher should open. Ignored when openMode is \"internal\".\n',
),
openMode: zod
.enum(["internal", "external_tab", "external_iframe"])
.describe(
'How the launcher opens this app: \"internal\" navigates to `route`\ninside the SPA; \"external_tab\" opens externalUrl in a new tab;\n\"external_iframe\" navigates to \/embedded\/:id, which renders\nexternalUrl inside an iframe shell.\n',
),
color: zod.string(),
isActive: zod.boolean(),
isSystem: zod.boolean(),
sortOrder: zod.number(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
groupCount: zod
.number()
.optional()
.describe(
"Number of groups granting access to this app. Populated only by the\nadmin list endpoint (GET \/admin\/apps) so the delete dialog can warn\nbefore the first click. Omitted from non-admin and single-app\nresponses.\n",
),
restrictionCount: zod
.number()
.optional()
.describe(
"Number of legacy permission restrictions for this app. Populated\nonly by the admin list endpoint (GET \/admin\/apps).\n",
),
openCount: zod
.number()
.optional()
.describe(
"Number of recorded app-open events for this app. Populated only by\nthe admin list endpoint (GET \/admin\/apps).\n",
),
});
export const ListAppsResponse = zod.array(ListAppsResponseItem);
/**
* @summary Create a new app (admin)
*/
export const CreateAppBody = zod.object({
slug: zod.string(),
nameAr: zod.string(),
nameEn: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
iconName: zod.string(),
imageUrl: zod.string().nullish(),
route: zod.string(),
externalUrl: zod.string().nullish(),
openMode: zod
.enum(["internal", "external_tab", "external_iframe"])
.optional(),
color: zod.string(),
isActive: zod.boolean().optional(),
isSystem: zod.boolean().optional(),
sortOrder: zod.number().optional(),
permissionIds: zod
.array(zod.number())
.optional()
.describe(
"Optional set of permission IDs to gate the new app on. Inserted with onConflictDoNothing so duplicates are tolerated.",
),
});
/**
* @summary Get app by ID
*/
export const GetAppParams = zod.object({
id: zod.coerce.number(),
});
export const GetAppResponse = zod.object({
id: zod.number(),
slug: zod.string(),
nameAr: zod.string(),
nameEn: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
iconName: zod.string(),
imageUrl: zod
.string()
.nullish()
.describe(
'Optional uploaded image used in place of the Lucide icon on the\nlauncher tile. Stored as the object-storage path returned by the\nupload presigner (e.g. \"\/objects\/<id>\").\n',
),
route: zod.string(),
externalUrl: zod
.string()
.nullish()
.describe(
'For openMode \"external_tab\" or \"external_iframe\": the URL the\nlauncher should open. Ignored when openMode is \"internal\".\n',
),
openMode: zod
.enum(["internal", "external_tab", "external_iframe"])
.describe(
'How the launcher opens this app: \"internal\" navigates to `route`\ninside the SPA; \"external_tab\" opens externalUrl in a new tab;\n\"external_iframe\" navigates to \/embedded\/:id, which renders\nexternalUrl inside an iframe shell.\n',
),
color: zod.string(),
isActive: zod.boolean(),
isSystem: zod.boolean(),
sortOrder: zod.number(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
groupCount: zod
.number()
.optional()
.describe(
"Number of groups granting access to this app. Populated only by the\nadmin list endpoint (GET \/admin\/apps) so the delete dialog can warn\nbefore the first click. Omitted from non-admin and single-app\nresponses.\n",
),
restrictionCount: zod
.number()
.optional()
.describe(
"Number of legacy permission restrictions for this app. Populated\nonly by the admin list endpoint (GET \/admin\/apps).\n",
),
openCount: zod
.number()
.optional()
.describe(
"Number of recorded app-open events for this app. Populated only by\nthe admin list endpoint (GET \/admin\/apps).\n",
),
});
/**
* @summary Update app (admin)
*/
export const UpdateAppParams = zod.object({
id: zod.coerce.number(),
});
export const UpdateAppBody = zod.object({
nameAr: zod.string().optional(),
nameEn: zod.string().optional(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
iconName: zod.string().optional(),
imageUrl: zod.string().nullish(),
route: zod.string().optional(),
externalUrl: zod.string().nullish(),
openMode: zod
.enum(["internal", "external_tab", "external_iframe"])
.optional(),
color: zod.string().optional(),
isActive: zod.boolean().optional(),
sortOrder: zod.number().optional(),
});
export const UpdateAppResponse = zod.object({
id: zod.number(),
slug: zod.string(),
nameAr: zod.string(),
nameEn: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
iconName: zod.string(),
imageUrl: zod
.string()
.nullish()
.describe(
'Optional uploaded image used in place of the Lucide icon on the\nlauncher tile. Stored as the object-storage path returned by the\nupload presigner (e.g. \"\/objects\/<id>\").\n',
),
route: zod.string(),
externalUrl: zod
.string()
.nullish()
.describe(
'For openMode \"external_tab\" or \"external_iframe\": the URL the\nlauncher should open. Ignored when openMode is \"internal\".\n',
),
openMode: zod
.enum(["internal", "external_tab", "external_iframe"])
.describe(
'How the launcher opens this app: \"internal\" navigates to `route`\ninside the SPA; \"external_tab\" opens externalUrl in a new tab;\n\"external_iframe\" navigates to \/embedded\/:id, which renders\nexternalUrl inside an iframe shell.\n',
),
color: zod.string(),
isActive: zod.boolean(),
isSystem: zod.boolean(),
sortOrder: zod.number(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
groupCount: zod
.number()
.optional()
.describe(
"Number of groups granting access to this app. Populated only by the\nadmin list endpoint (GET \/admin\/apps) so the delete dialog can warn\nbefore the first click. Omitted from non-admin and single-app\nresponses.\n",
),
restrictionCount: zod
.number()
.optional()
.describe(
"Number of legacy permission restrictions for this app. Populated\nonly by the admin list endpoint (GET \/admin\/apps).\n",
),
openCount: zod
.number()
.optional()
.describe(
"Number of recorded app-open events for this app. Populated only by\nthe admin list endpoint (GET \/admin\/apps).\n",
),
});
/**
* @summary Delete app (admin)
*/
export const DeleteAppParams = zod.object({
id: zod.coerce.number(),
});
export const deleteAppQueryForceDefault = false;
export const DeleteAppQueryParams = zod.object({
force: zod.coerce
.boolean()
.default(deleteAppQueryForceDefault)
.describe(
"Force deletion of an app that has dependent records (records an audit log entry).",
),
});
/**
* Returns the permissions currently required for users to see this app
in their launcher. An app with no rows here is unrestricted (visible
to anyone). When more than one permission is configured, holding any
one of them is sufficient.
* @summary List the permissions that gate this app (admin)
*/
export const ListAppPermissionsParams = zod.object({
id: zod.coerce.number(),
});
export const ListAppPermissionsResponseItem = zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
});
export const ListAppPermissionsResponse = zod.array(
ListAppPermissionsResponseItem,
);
/**
* Adds a row to `app_permissions` for the (app_id, permission_id) pair.
Uses `ON CONFLICT DO NOTHING` against the composite primary key so
re-adding an existing pair is a no-op.
* @summary Add a required permission to this app (admin)
*/
export const AddAppPermissionParams = zod.object({
id: zod.coerce.number(),
});
export const AddAppPermissionBody = zod.object({
permissionId: zod
.number()
.describe("ID of the permission to require for this app."),
});
/**
* Given a candidate set of permission IDs that would gate this app,
returns how many users would lose visibility of the app (excluding
admins, who always see every app), the count of users who currently
see the app, and the groups that grant the app via `group_apps`. A
member of any returned group always sees the app regardless of
permission changes, so listing them lets the admin see which
populations are protected from the loss.
Mirrors `POST /roles/{id}/permissions/impact-preview` so the admin
UI can show the same kind of warning before committing the change.
* @summary Preview the impact of changing the permissions required for an app (admin)
*/
export const PreviewAppPermissionsImpactParams = zod.object({
id: zod.coerce.number(),
});
export const PreviewAppPermissionsImpactBody = zod.object({
permissionIds: zod
.array(zod.number())
.describe(
'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).',
),
});
export const PreviewAppPermissionsImpactResponse = zod.object({
affectedUserCount: zod
.number()
.describe(
"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.",
),
currentlyVisibleUserCount: zod
.number()
.describe(
'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\").',
),
groups: zod
.array(
zod.object({
id: zod.number(),
name: zod.string(),
}),
)
.describe(
"Groups that currently grant this app via `group_apps`. Members of any of these groups keep access regardless of permission changes.",
),
noChange: zod
.boolean()
.describe(
"True when the candidate set is identical to the current permission set so no change would actually be saved.",
),
});
/**
* @summary Remove a required permission from this app (admin)
*/
export const RemoveAppPermissionParams = zod.object({
id: zod.coerce.number(),
permissionId: zod.coerce.number(),
});
/**
* @summary Log an app open event for the current user
*/
export const LogAppOpenParams = zod.object({
id: zod.coerce.number(),
});
/**
* @summary Set the current user's preferred home apps order
*/
export const UpdateMyAppOrderBody = zod.object({
order: zod
.array(zod.number())
.describe("App IDs in the desired display order"),
});
export const UpdateMyAppOrderResponseItem = zod.object({
id: zod.number(),
slug: zod.string(),
nameAr: zod.string(),
nameEn: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
iconName: zod.string(),
imageUrl: zod
.string()
.nullish()
.describe(
'Optional uploaded image used in place of the Lucide icon on the\nlauncher tile. Stored as the object-storage path returned by the\nupload presigner (e.g. \"\/objects\/<id>\").\n',
),
route: zod.string(),
externalUrl: zod
.string()
.nullish()
.describe(
'For openMode \"external_tab\" or \"external_iframe\": the URL the\nlauncher should open. Ignored when openMode is \"internal\".\n',
),
openMode: zod
.enum(["internal", "external_tab", "external_iframe"])
.describe(
'How the launcher opens this app: \"internal\" navigates to `route`\ninside the SPA; \"external_tab\" opens externalUrl in a new tab;\n\"external_iframe\" navigates to \/embedded\/:id, which renders\nexternalUrl inside an iframe shell.\n',
),
color: zod.string(),
isActive: zod.boolean(),
isSystem: zod.boolean(),
sortOrder: zod.number(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
groupCount: zod
.number()
.optional()
.describe(
"Number of groups granting access to this app. Populated only by the\nadmin list endpoint (GET \/admin\/apps) so the delete dialog can warn\nbefore the first click. Omitted from non-admin and single-app\nresponses.\n",
),
restrictionCount: zod
.number()
.optional()
.describe(
"Number of legacy permission restrictions for this app. Populated\nonly by the admin list endpoint (GET \/admin\/apps).\n",
),
openCount: zod
.number()
.optional()
.describe(
"Number of recorded app-open events for this app. Populated only by\nthe admin list endpoint (GET \/admin\/apps).\n",
),
});
export const UpdateMyAppOrderResponse = zod.array(UpdateMyAppOrderResponseItem);
/**
* @summary Get application settings (public)
*/
export const GetAppSettingsResponse = zod.object({
siteNameAr: zod.string(),
siteNameEn: zod.string(),
registrationOpen: zod.boolean(),
footerTextAr: zod.string(),
footerTextEn: zod.string(),
updatedAt: zod.coerce.date().optional(),
});
/**
* @summary Update application settings (admin)
*/
export const updateAppSettingsBodySiteNameArMax = 200;
export const updateAppSettingsBodySiteNameEnMax = 200;
export const updateAppSettingsBodyFooterTextArMax = 300;
export const updateAppSettingsBodyFooterTextEnMax = 300;
export const UpdateAppSettingsBody = zod.object({
siteNameAr: zod
.string()
.min(1)
.max(updateAppSettingsBodySiteNameArMax)
.optional(),
siteNameEn: zod
.string()
.min(1)
.max(updateAppSettingsBodySiteNameEnMax)
.optional(),
registrationOpen: zod.boolean().optional(),
footerTextAr: zod
.string()
.min(1)
.max(updateAppSettingsBodyFooterTextArMax)
.optional(),
footerTextEn: zod
.string()
.min(1)
.max(updateAppSettingsBodyFooterTextEnMax)
.optional(),
});
export const UpdateAppSettingsResponse = zod.object({
siteNameAr: zod.string(),
siteNameEn: zod.string(),
registrationOpen: zod.boolean(),
footerTextAr: zod.string(),
footerTextEn: zod.string(),
updatedAt: zod.coerce.date().optional(),
});
/**
* @summary Request a presigned URL for file upload
*/
export const RequestUploadUrlBody = zod.object({
name: zod.string().min(1),
size: zod.number().min(1),
contentType: zod.string().min(1),
});
export const RequestUploadUrlResponse = zod.object({
uploadURL: zod
.string()
.min(1)
.describe(
"Presigned upload destination. May be either an absolute URL (S3 driver) or a same-origin path starting with `\/` (local driver) — the browser resolves the latter against `window.location.origin` so uploads always target the host serving the SPA.\n",
),
objectPath: zod.string(),
metadata: zod
.object({
name: zod.string().min(1),
size: zod.number().min(1),
contentType: zod.string().min(1),
})
.optional(),
});
/**
* @summary List all services
*/
export const ListServicesResponseItem = zod.object({
id: zod.number(),
categoryId: zod.number().nullish(),
nameAr: zod.string(),
nameEn: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
imageUrl: zod.string().nullish(),
price: zod.string().nullish(),
isAvailable: zod.boolean(),
sortOrder: zod.number(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
orderCount: zod
.number()
.optional()
.describe(
"Number of service_orders rows referencing this service. Populated\nonly by the list endpoint (GET \/services) so the admin delete\ndialog can warn before the first click. Omitted from single-service\nresponses.\n",
),
});
export const ListServicesResponse = zod.array(ListServicesResponseItem);
/**
* @summary Create a service (admin)
*/
export const CreateServiceBody = zod.object({
categoryId: zod.number().nullish(),
nameAr: zod.string(),
nameEn: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
imageUrl: zod.string().nullish(),
price: zod.string().nullish(),
isAvailable: zod.boolean().optional(),
sortOrder: zod.number().optional(),
});
/**
* Persists a new global `sortOrder` for the service catalogue by
applying the index of each id in `orderedIds`. Admin-only.
The body must contain every existing service id exactly once;
partial reorders are rejected with 400 so the client can never
accidentally drop services from the sort plan.
* @summary Replace the global service sort order (admin)
*/
export const reorderServicesBodyOrderedIdsMax = 1000;
export const ReorderServicesBody = zod
.object({
orderedIds: zod
.array(zod.number().min(1))
.min(1)
.max(reorderServicesBodyOrderedIdsMax),
})
.describe(
"New global service order. `orderedIds` must contain every\nexisting service id exactly once. The server sets each\nservice's `sortOrder` to its position in the list (0-based),\nso a later GET \/services returns them in the same order.\n",
);
/**
* @summary Get service by ID
*/
export const GetServiceParams = zod.object({
id: zod.coerce.number(),
});
export const GetServiceResponse = zod.object({
id: zod.number(),
categoryId: zod.number().nullish(),
nameAr: zod.string(),
nameEn: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
imageUrl: zod.string().nullish(),
price: zod.string().nullish(),
isAvailable: zod.boolean(),
sortOrder: zod.number(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
orderCount: zod
.number()
.optional()
.describe(
"Number of service_orders rows referencing this service. Populated\nonly by the list endpoint (GET \/services) so the admin delete\ndialog can warn before the first click. Omitted from single-service\nresponses.\n",
),
});
/**
* @summary Update service (admin)
*/
export const UpdateServiceParams = zod.object({
id: zod.coerce.number(),
});
export const UpdateServiceBody = zod.object({
categoryId: zod.number().nullish(),
nameAr: zod.string().optional(),
nameEn: zod.string().optional(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
imageUrl: zod.string().nullish(),
price: zod.string().nullish(),
isAvailable: zod.boolean().optional(),
sortOrder: zod.number().optional(),
});
export const UpdateServiceResponse = zod.object({
id: zod.number(),
categoryId: zod.number().nullish(),
nameAr: zod.string(),
nameEn: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
imageUrl: zod.string().nullish(),
price: zod.string().nullish(),
isAvailable: zod.boolean(),
sortOrder: zod.number(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
orderCount: zod
.number()
.optional()
.describe(
"Number of service_orders rows referencing this service. Populated\nonly by the list endpoint (GET \/services) so the admin delete\ndialog can warn before the first click. Omitted from single-service\nresponses.\n",
),
});
/**
* @summary Delete service (admin)
*/
export const DeleteServiceParams = zod.object({
id: zod.coerce.number(),
});
export const deleteServiceQueryForceDefault = false;
export const DeleteServiceQueryParams = zod.object({
force: zod.coerce
.boolean()
.default(deleteServiceQueryForceDefault)
.describe(
"Force deletion of a service that has existing orders (records an audit log entry).",
),
});
/**
* @summary Place a service order
*/
export const createServiceOrderBodyNotesMax = 500;
export const CreateServiceOrderBody = zod.object({
serviceId: zod.number(),
notes: zod.string().max(createServiceOrderBodyNotesMax).nullish(),
});
/**
* @summary List orders placed by the current user
*/
export const ListMyServiceOrdersResponseItem = zod.object({
id: zod.number(),
serviceId: zod.number(),
userId: zod.number(),
assignedTo: zod.number().nullish(),
notes: zod.string().nullish(),
status: zod.enum([
"pending",
"received",
"preparing",
"completed",
"cancelled",
]),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
service: zod.object({
id: zod.number(),
nameAr: zod.string(),
nameEn: zod.string(),
imageUrl: zod.string().nullish(),
}),
requester: zod
.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
}),
zod.null(),
])
.optional(),
assignee: zod
.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
}),
zod.null(),
])
.optional(),
});
export const ListMyServiceOrdersResponse = zod.array(
ListMyServiceOrdersResponseItem,
);
/**
* @summary List active orders for receivers (requires orders.receive permission). Returns pending+unassigned orders plus orders assigned to me that are not completed/cancelled.
*/
export const ListIncomingServiceOrdersResponseItem = zod.object({
id: zod.number(),
serviceId: zod.number(),
userId: zod.number(),
assignedTo: zod.number().nullish(),
notes: zod.string().nullish(),
status: zod.enum([
"pending",
"received",
"preparing",
"completed",
"cancelled",
]),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
service: zod.object({
id: zod.number(),
nameAr: zod.string(),
nameEn: zod.string(),
imageUrl: zod.string().nullish(),
}),
requester: zod
.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
}),
zod.null(),
])
.optional(),
assignee: zod
.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
}),
zod.null(),
])
.optional(),
});
export const ListIncomingServiceOrdersResponse = zod.array(
ListIncomingServiceOrdersResponseItem,
);
/**
* @summary Update order status. Preparing/completed restricted to assigned receiver or admin; cancelled allowed for owner (while pending|received) or admin (any time); pending/received are restore-from-cancelled transitions allowed for owner or admin.
*/
export const UpdateServiceOrderStatusParams = zod.object({
id: zod.coerce.number(),
});
export const UpdateServiceOrderStatusBody = zod.object({
status: zod.enum([
"pending",
"received",
"preparing",
"completed",
"cancelled",
]),
});
export const UpdateServiceOrderStatusResponse = zod.object({
id: zod.number(),
serviceId: zod.number(),
userId: zod.number(),
assignedTo: zod.number().nullish(),
notes: zod.string().nullish(),
status: zod.enum([
"pending",
"received",
"preparing",
"completed",
"cancelled",
]),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
service: zod.object({
id: zod.number(),
nameAr: zod.string(),
nameEn: zod.string(),
imageUrl: zod.string().nullish(),
}),
requester: zod
.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
}),
zod.null(),
])
.optional(),
assignee: zod
.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
}),
zod.null(),
])
.optional(),
});
/**
* @summary Receiver atomically claims a pending order (sets status=received, assigned_to=current user). Requires orders.receive permission. Returns 409 already_claimed if the order is no longer pending+unassigned.
*/
export const ConfirmServiceOrderReceiptParams = zod.object({
id: zod.coerce.number(),
});
export const ConfirmServiceOrderReceiptResponse = zod.object({
id: zod.number(),
serviceId: zod.number(),
userId: zod.number(),
assignedTo: zod.number().nullish(),
notes: zod.string().nullish(),
status: zod.enum([
"pending",
"received",
"preparing",
"completed",
"cancelled",
]),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
service: zod.object({
id: zod.number(),
nameAr: zod.string(),
nameEn: zod.string(),
imageUrl: zod.string().nullish(),
}),
requester: zod
.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
}),
zod.null(),
])
.optional(),
assignee: zod
.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
}),
zod.null(),
])
.optional(),
});
/**
* @summary List service categories
*/
export const ListServiceCategoriesResponseItem = zod.object({
id: zod.number(),
nameAr: zod.string(),
nameEn: zod.string(),
sortOrder: zod.number(),
createdAt: zod.coerce.date(),
});
export const ListServiceCategoriesResponse = zod.array(
ListServiceCategoriesResponseItem,
);
/**
* @summary List notifications for current user
*/
export const ListNotificationsResponseItem = zod.object({
id: zod.number(),
userId: zod.number(),
titleAr: zod.string(),
titleEn: zod.string(),
bodyAr: zod.string().nullish(),
bodyEn: zod.string().nullish(),
type: zod.string(),
isRead: zod.boolean(),
relatedId: zod.number().nullish(),
relatedType: zod.string().nullish(),
createdAt: zod.coerce.date(),
});
export const ListNotificationsResponse = zod.array(
ListNotificationsResponseItem,
);
/**
* @summary Mark notification as read
*/
export const MarkNotificationReadParams = zod.object({
id: zod.coerce.number(),
});
export const MarkNotificationReadResponse = zod.object({
id: zod.number(),
userId: zod.number(),
titleAr: zod.string(),
titleEn: zod.string(),
bodyAr: zod.string().nullish(),
bodyEn: zod.string().nullish(),
type: zod.string(),
isRead: zod.boolean(),
relatedId: zod.number().nullish(),
relatedType: zod.string().nullish(),
createdAt: zod.coerce.date(),
});
/**
* @summary Mark all notifications as read
*/
export const MarkAllNotificationsReadResponse = zod.object({
success: zod.boolean(),
});
/**
* @summary Get the server's VAPID public key for Web Push subscriptions
*/
export const GetPushVapidPublicKeyResponse = zod.object({
publicKey: zod.string(),
});
/**
* @summary Register a Web Push subscription for the current user
*/
export const SubscribePushBody = zod.object({
endpoint: zod.string(),
keys: zod.object({
p256dh: zod.string(),
auth: zod.string(),
}),
});
export const SubscribePushResponse = zod.object({
success: zod.boolean(),
});
/**
* @summary Remove a Web Push subscription by endpoint (spec-aligned alias of POST /push/unsubscribe)
*/
export const DeletePushSubscriptionQueryParams = zod.object({
endpoint: zod.coerce
.string()
.describe("The push endpoint URL to unsubscribe"),
});
export const DeletePushSubscriptionResponse = zod.object({
success: zod.boolean(),
});
/**
* @summary Remove a Web Push subscription for the current user
*/
export const UnsubscribePushBody = zod.object({
endpoint: zod.string(),
});
export const UnsubscribePushResponse = zod.object({
success: zod.boolean(),
});
/**
* @summary List all users (admin)
*/
export const ListUsersQueryParams = zod.object({
q: zod.coerce
.string()
.optional()
.describe("Free-text search across username, email, and display names"),
groupId: zod.coerce
.number()
.optional()
.describe("Filter to users that belong to the given group"),
status: zod
.enum(["active", "disabled"])
.optional()
.describe("Filter by active\/disabled state"),
});
export const ListUsersResponseItem = zod.object({
id: zod.number(),
username: zod.string(),
email: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
preferredLanguage: zod.string(),
clockStyle: zod
.union([
zod
.enum(["full", "digital", "digital-no-seconds", "analog", "minimal"])
.describe("Per-user home-screen clock style preference"),
zod.null(),
])
.optional(),
clockHour12: zod.boolean().nullish(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean(),
roles: zod.array(zod.string()),
directRoles: zod
.array(zod.string())
.optional()
.describe(
"Roles assigned directly to the user (user_roles table only). The\n`roles` field above is the effective set (direct + inherited via\ngroup membership). The admin Users page uses the difference to\nmark a role as \"inherited from group X\" and disables the toggle\nso the admin doesn't try to remove a role that won't disappear.\n",
),
groups: zod.array(
zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
}),
),
createdAt: zod.coerce.date(),
noteCount: zod
.number()
.optional()
.describe(
"Dependent record counts. Populated only by the admin list endpoint\n(GET \/users) so the delete dialog can warn before the first click.\nOmitted from single-user responses (GET \/users\/:id, PATCH, etc).\n",
),
orderCount: zod
.number()
.optional()
.describe(
"Number of service orders owned by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
});
export const ListUsersResponse = zod.array(ListUsersResponseItem);
/**
* @summary Create a user (admin)
*/
export const createUserBodyPreferredLanguageDefault = `ar`;
export const CreateUserBody = zod.object({
username: zod.string(),
email: zod.string(),
password: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
preferredLanguage: zod
.string()
.default(createUserBodyPreferredLanguageDefault),
groupIds: zod
.array(zod.number())
.optional()
.describe(
"Initial group memberships in addition to the auto-assigned Everyone group.",
),
});
/**
* @summary Get user by ID (admin)
*/
export const GetUserParams = zod.object({
id: zod.coerce.number(),
});
export const GetUserResponse = zod.object({
id: zod.number(),
username: zod.string(),
email: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
preferredLanguage: zod.string(),
clockStyle: zod
.union([
zod
.enum(["full", "digital", "digital-no-seconds", "analog", "minimal"])
.describe("Per-user home-screen clock style preference"),
zod.null(),
])
.optional(),
clockHour12: zod.boolean().nullish(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean(),
roles: zod.array(zod.string()),
directRoles: zod
.array(zod.string())
.optional()
.describe(
"Roles assigned directly to the user (user_roles table only). The\n`roles` field above is the effective set (direct + inherited via\ngroup membership). The admin Users page uses the difference to\nmark a role as \"inherited from group X\" and disables the toggle\nso the admin doesn't try to remove a role that won't disappear.\n",
),
groups: zod.array(
zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
}),
),
createdAt: zod.coerce.date(),
noteCount: zod
.number()
.optional()
.describe(
"Dependent record counts. Populated only by the admin list endpoint\n(GET \/users) so the delete dialog can warn before the first click.\nOmitted from single-user responses (GET \/users\/:id, PATCH, etc).\n",
),
orderCount: zod
.number()
.optional()
.describe(
"Number of service orders owned by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
});
/**
* @summary Update user (admin)
*/
export const UpdateUserParams = zod.object({
id: zod.coerce.number(),
});
export const UpdateUserBody = zod.object({
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
isActive: zod.boolean().optional(),
preferredLanguage: zod.string().optional(),
groupIds: zod
.array(zod.number())
.optional()
.describe("If provided, replaces the user's group memberships"),
});
export const UpdateUserResponse = zod.object({
id: zod.number(),
username: zod.string(),
email: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
preferredLanguage: zod.string(),
clockStyle: zod
.union([
zod
.enum(["full", "digital", "digital-no-seconds", "analog", "minimal"])
.describe("Per-user home-screen clock style preference"),
zod.null(),
])
.optional(),
clockHour12: zod.boolean().nullish(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean(),
roles: zod.array(zod.string()),
directRoles: zod
.array(zod.string())
.optional()
.describe(
"Roles assigned directly to the user (user_roles table only). The\n`roles` field above is the effective set (direct + inherited via\ngroup membership). The admin Users page uses the difference to\nmark a role as \"inherited from group X\" and disables the toggle\nso the admin doesn't try to remove a role that won't disappear.\n",
),
groups: zod.array(
zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
}),
),
createdAt: zod.coerce.date(),
noteCount: zod
.number()
.optional()
.describe(
"Dependent record counts. Populated only by the admin list endpoint\n(GET \/users) so the delete dialog can warn before the first click.\nOmitted from single-user responses (GET \/users\/:id, PATCH, etc).\n",
),
orderCount: zod
.number()
.optional()
.describe(
"Number of service orders owned by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
});
/**
* @summary Delete user (admin)
*/
export const DeleteUserParams = zod.object({
id: zod.coerce.number(),
});
export const deleteUserQueryForceDefault = false;
export const DeleteUserQueryParams = zod.object({
force: zod.coerce
.boolean()
.default(deleteUserQueryForceDefault)
.describe(
"Force deletion of a user that owns dependent records (records an audit log entry).",
),
});
/**
* Deletes several orders in one round-trip. Authorization is still
enforced per id using the same rules as DELETE /orders/{id}, so the
client cannot bypass the permission check by batching. Returns a
summary of which ids were deleted and which were rejected.
* @summary Delete multiple service orders in one request
*/
export const bulkDeleteServiceOrdersBodyIdsMax = 200;
export const BulkDeleteServiceOrdersBody = zod.object({
ids: zod.array(zod.number()).min(1).max(bulkDeleteServiceOrdersBodyIdsMax),
});
export const BulkDeleteServiceOrdersResponse = zod.object({
deletedIds: zod
.array(zod.number())
.describe("Ids that were successfully deleted."),
failedIds: zod
.array(zod.number())
.describe(
"Ids that were skipped (not found, or caller is not authorized).",
),
});
/**
* Permanently deletes the order. Allowed when:
- the caller is an admin (any order, any status); or
- the caller owns the order AND it is in a terminal status
(completed/cancelled); or
- the caller holds the `orders.receive` permission AND the order
is currently visible in their own incoming view — i.e. an
unclaimed pending order, or one they have themselves claimed
and is still active (received/preparing). Receivers cannot
delete another receiver's claimed order, nor terminal orders.
* @summary Delete a service order
*/
export const DeleteServiceOrderParams = zod.object({
id: zod.coerce.number(),
});
/**
* @summary Idempotently add a role to a user (admin)
*/
export const AddUserRoleParams = zod.object({
id: zod.coerce.number(),
});
export const AddUserRoleBody = zod.object({
roleName: zod.string().min(1),
});
export const AddUserRoleResponse = zod.object({
id: zod.number(),
username: zod.string(),
email: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
preferredLanguage: zod.string(),
clockStyle: zod
.union([
zod
.enum(["full", "digital", "digital-no-seconds", "analog", "minimal"])
.describe("Per-user home-screen clock style preference"),
zod.null(),
])
.optional(),
clockHour12: zod.boolean().nullish(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean(),
roles: zod.array(zod.string()),
directRoles: zod
.array(zod.string())
.optional()
.describe(
"Roles assigned directly to the user (user_roles table only). The\n`roles` field above is the effective set (direct + inherited via\ngroup membership). The admin Users page uses the difference to\nmark a role as \"inherited from group X\" and disables the toggle\nso the admin doesn't try to remove a role that won't disappear.\n",
),
groups: zod.array(
zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
}),
),
createdAt: zod.coerce.date(),
noteCount: zod
.number()
.optional()
.describe(
"Dependent record counts. Populated only by the admin list endpoint\n(GET \/users) so the delete dialog can warn before the first click.\nOmitted from single-user responses (GET \/users\/:id, PATCH, etc).\n",
),
orderCount: zod
.number()
.optional()
.describe(
"Number of service orders owned by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
});
/**
* @summary Idempotently remove a role from a user (admin)
*/
export const RemoveUserRoleParams = zod.object({
id: zod.coerce.number(),
roleName: zod.coerce.string(),
});
export const RemoveUserRoleResponse = zod.object({
id: zod.number(),
username: zod.string(),
email: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
preferredLanguage: zod.string(),
clockStyle: zod
.union([
zod
.enum(["full", "digital", "digital-no-seconds", "analog", "minimal"])
.describe("Per-user home-screen clock style preference"),
zod.null(),
])
.optional(),
clockHour12: zod.boolean().nullish(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean(),
roles: zod.array(zod.string()),
directRoles: zod
.array(zod.string())
.optional()
.describe(
"Roles assigned directly to the user (user_roles table only). The\n`roles` field above is the effective set (direct + inherited via\ngroup membership). The admin Users page uses the difference to\nmark a role as \"inherited from group X\" and disables the toggle\nso the admin doesn't try to remove a role that won't disappear.\n",
),
groups: zod.array(
zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
}),
),
createdAt: zod.coerce.date(),
noteCount: zod
.number()
.optional()
.describe(
"Dependent record counts. Populated only by the admin list endpoint\n(GET \/users) so the delete dialog can warn before the first click.\nOmitted from single-user responses (GET \/users\/:id, PATCH, etc).\n",
),
orderCount: zod
.number()
.optional()
.describe(
"Number of service orders owned by this user. Populated only by the\nadmin list endpoint (GET \/users).\n",
),
});
/**
* @summary List all available permissions (admin)
*/
export const ListPermissionsResponseItem = zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
});
export const ListPermissionsResponse = zod.array(ListPermissionsResponseItem);
/**
* @summary List roles (admin)
*/
export const ListRolesResponseItem = zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
isSystem: zod.boolean(),
createdAt: zod.coerce.date(),
userCount: zod
.number()
.optional()
.describe(
"Number of users assigned this role. Populated only by the admin\nlist endpoint (GET \/admin\/roles) so the admin list can show the\nsame dependency counts inline as the Apps\/Services\/Users\/Groups\npanels. Omitted from create\/update responses.\n",
),
groupCount: zod
.number()
.optional()
.describe(
"Number of groups granting this role. Populated only by the admin\nlist endpoint (GET \/admin\/roles).\n",
),
});
export const ListRolesResponse = zod.array(ListRolesResponseItem);
/**
* @summary Create role (admin)
*/
export const CreateRoleBody = zod.object({
name: zod.string().min(1),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
});
/**
* @summary Update role name and descriptions (admin)
*/
export const UpdateRoleParams = zod.object({
id: zod.coerce.number(),
});
export const UpdateRoleBody = zod.object({
name: zod.string().min(1).optional(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
});
export const UpdateRoleResponse = zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
isSystem: zod.boolean(),
createdAt: zod.coerce.date(),
userCount: zod
.number()
.optional()
.describe(
"Number of users assigned this role. Populated only by the admin\nlist endpoint (GET \/admin\/roles) so the admin list can show the\nsame dependency counts inline as the Apps\/Services\/Users\/Groups\npanels. Omitted from create\/update responses.\n",
),
groupCount: zod
.number()
.optional()
.describe(
"Number of groups granting this role. Populated only by the admin\nlist endpoint (GET \/admin\/roles).\n",
),
});
/**
* @summary Delete role (admin)
*/
export const DeleteRoleParams = zod.object({
id: zod.coerce.number(),
});
/**
* @summary Count users and groups currently assigned to a role (admin)
*/
export const GetRoleUsageParams = zod.object({
id: zod.coerce.number(),
});
export const GetRoleUsageResponse = zod.object({
userCount: zod.number(),
groupCount: zod.number(),
});
/**
* Returns permission-change audit records for the given role, newest
first. Supports offset-based pagination via `limit`/`offset` and
optional filters by acting user (`actorUserId`) and a date range
(`from`/`to`, inclusive UTC days). The response includes the total
matching count and the next offset to load (or `null` when the end
has been reached). Each entry captures the actor (if known), the
previous permission IDs, the new permission IDs, and the timestamp.
* @summary List recent permission-change audit entries for a role (admin)
*/
export const GetRolePermissionAuditParams = zod.object({
id: zod.coerce.number(),
});
export const getRolePermissionAuditQueryLimitDefault = 10;
export const getRolePermissionAuditQueryLimitMax = 200;
export const getRolePermissionAuditQueryOffsetDefault = 0;
export const getRolePermissionAuditQueryOffsetMin = 0;
export const getRolePermissionAuditQueryActorUserIdMin = 0;
export const GetRolePermissionAuditQueryParams = zod.object({
limit: zod.coerce
.number()
.min(1)
.max(getRolePermissionAuditQueryLimitMax)
.default(getRolePermissionAuditQueryLimitDefault),
offset: zod.coerce
.number()
.min(getRolePermissionAuditQueryOffsetMin)
.default(getRolePermissionAuditQueryOffsetDefault),
actorUserId: zod.coerce
.number()
.min(getRolePermissionAuditQueryActorUserIdMin)
.optional()
.describe(
"Restrict results to entries authored by the given user. Use\n`0` or omit to return entries from any actor.\n",
),
from: zod
.date()
.optional()
.describe("Start date (inclusive, YYYY-MM-DD UTC)."),
to: zod.date().optional().describe("End date (inclusive, YYYY-MM-DD UTC)."),
});
export const GetRolePermissionAuditResponse = zod.object({
entries: zod.array(
zod.object({
id: zod.number(),
roleId: zod.number(),
previousPermissionIds: zod.array(zod.number()),
newPermissionIds: zod.array(zod.number()),
addedPermissionIds: zod.array(zod.number()),
removedPermissionIds: zod.array(zod.number()),
createdAt: zod.coerce.date(),
actor: zod.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
}),
zod.null(),
]),
}),
),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
});
/**
* Returns the same audit rows as `GET /roles/{id}/audit` but as a
downloadable CSV file with permission IDs resolved to names.
Honors the optional `actorUserId`, `from`, and `to` filters
identically. Pagination parameters are NOT accepted: the file
contains all matching rows up to a server-side cap (currently
10,000 — narrow the date range for a longer history).
* @summary Download permission-change audit entries for a role as CSV (admin)
*/
export const ExportRolePermissionAuditCsvParams = zod.object({
id: zod.coerce.number(),
});
export const exportRolePermissionAuditCsvQueryActorUserIdMin = 0;
export const ExportRolePermissionAuditCsvQueryParams = zod.object({
actorUserId: zod.coerce
.number()
.min(exportRolePermissionAuditCsvQueryActorUserIdMin)
.optional()
.describe(
"Restrict results to entries authored by the given user. Use\n`0` or omit to return entries from any actor.\n",
),
from: zod
.date()
.optional()
.describe("Start date (inclusive, YYYY-MM-DD UTC)."),
to: zod.date().optional().describe("End date (inclusive, YYYY-MM-DD UTC)."),
});
/**
* Given a candidate set of permission IDs for the role, returns the list of
permissions that would be removed and, for each one, how many users would
lose the permission (because they hold this role directly or via a group
and have no other role granting the permission), plus the groups that
currently grant this role.
* @summary Preview the impact of changing a role's permission set (admin)
*/
export const PreviewRolePermissionsImpactParams = zod.object({
id: zod.coerce.number(),
});
export const PreviewRolePermissionsImpactBody = zod.object({
permissionIds: zod
.array(zod.number())
.describe(
"Candidate set of permission IDs to evaluate against the role's current assignments.",
),
});
export const PreviewRolePermissionsImpactResponse = zod.object({
removed: zod
.array(
zod.object({
permissionId: zod.number(),
permissionName: zod.string(),
userCount: zod
.number()
.describe(
"Distinct users who currently have this permission via this role and would lose it (no other role they hold grants the permission).",
),
groupCount: zod
.number()
.describe("Number of groups that currently grant this role."),
groups: zod
.array(
zod.object({
id: zod.number(),
name: zod.string(),
}),
)
.describe(
"Groups that currently grant this role and therefore propagate the removal to their members.",
),
}),
)
.describe(
"One entry per permission that is currently assigned to the role but not in the candidate set.",
),
totalAffectedUsers: zod
.number()
.describe(
"Distinct users that would lose at least one permission across all removals.",
),
});
/**
* @summary List permissions assigned to a role (admin)
*/
export const GetRolePermissionsParams = zod.object({
id: zod.coerce.number(),
});
export const GetRolePermissionsResponseItem = zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
});
export const GetRolePermissionsResponse = zod.array(
GetRolePermissionsResponseItem,
);
/**
* @summary Atomically replace the permissions assigned to a role (admin)
*/
export const ReplaceRolePermissionsParams = zod.object({
id: zod.coerce.number(),
});
export const ReplaceRolePermissionsBody = zod.object({
permissionIds: zod
.array(zod.number())
.describe(
"Full set of permission IDs the role should grant. Existing assignments not present here are removed.",
),
});
export const ReplaceRolePermissionsResponseItem = zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
});
export const ReplaceRolePermissionsResponse = zod.array(
ReplaceRolePermissionsResponseItem,
);
/**
* @summary List groups (admin)
*/
export const ListGroupsQueryParams = zod.object({
q: zod.coerce.string().optional(),
});
export const ListGroupsResponseItem = zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
isSystem: zod.boolean(),
memberCount: zod.number(),
appCount: zod.number(),
roleCount: zod.number(),
createdAt: zod.coerce.date(),
});
export const ListGroupsResponse = zod.array(ListGroupsResponseItem);
/**
* @summary Create group (admin)
*/
export const CreateGroupBody = zod.object({
name: zod.string().min(1),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
appIds: zod.array(zod.number()).optional(),
roleIds: zod.array(zod.number()).optional(),
userIds: zod.array(zod.number()).optional(),
});
/**
* @summary Get group with members and apps (admin)
*/
export const GetGroupParams = zod.object({
id: zod.coerce.number(),
});
export const GetGroupResponse = zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
isSystem: zod.boolean(),
appIds: zod.array(zod.number()),
roleIds: zod.array(zod.number()),
userIds: zod.array(zod.number()),
createdAt: zod.coerce.date(),
});
/**
* @summary Update group (admin)
*/
export const UpdateGroupParams = zod.object({
id: zod.coerce.number(),
});
export const UpdateGroupBody = zod.object({
name: zod.string().min(1).optional(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
appIds: zod.array(zod.number()).optional(),
roleIds: zod.array(zod.number()).optional(),
userIds: zod.array(zod.number()).optional(),
});
export const UpdateGroupResponse = zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullish(),
descriptionEn: zod.string().nullish(),
isSystem: zod.boolean(),
appIds: zod.array(zod.number()),
roleIds: zod.array(zod.number()),
userIds: zod.array(zod.number()),
createdAt: zod.coerce.date(),
});
/**
* @summary Delete group (admin)
*/
export const DeleteGroupParams = zod.object({
id: zod.coerce.number(),
});
export const deleteGroupQueryForceDefault = false;
export const DeleteGroupQueryParams = zod.object({
force: zod.coerce
.boolean()
.default(deleteGroupQueryForceDefault)
.describe(
"Force deletion of a non-empty group (records an audit log entry).",
),
});
/**
* Returns permission-change audit records for the given user, newest
first. Mirrors the role audit endpoint shape and supports the same
`limit`/`offset`/`actorUserId`/`from`/`to` filters. Each entry
captures the actor (if known), the previous and new id sets, and
the change kind (`user.roles` or `user.groups`).
* @summary List recent permission-change audit entries for a user (admin)
*/
export const GetUserPermissionAuditParams = zod.object({
id: zod.coerce.number(),
});
export const getUserPermissionAuditQueryLimitDefault = 10;
export const getUserPermissionAuditQueryLimitMax = 200;
export const getUserPermissionAuditQueryOffsetDefault = 0;
export const getUserPermissionAuditQueryOffsetMin = 0;
export const getUserPermissionAuditQueryActorUserIdMin = 0;
export const GetUserPermissionAuditQueryParams = zod.object({
limit: zod.coerce
.number()
.min(1)
.max(getUserPermissionAuditQueryLimitMax)
.default(getUserPermissionAuditQueryLimitDefault),
offset: zod.coerce
.number()
.min(getUserPermissionAuditQueryOffsetMin)
.default(getUserPermissionAuditQueryOffsetDefault),
actorUserId: zod.coerce
.number()
.min(getUserPermissionAuditQueryActorUserIdMin)
.optional(),
from: zod.date().optional(),
to: zod.date().optional(),
});
export const GetUserPermissionAuditResponse = zod.object({
entries: zod.array(
zod.object({
id: zod.number(),
targetKind: zod.enum(["user", "group", "app"]),
targetId: zod.number(),
changeKind: zod.enum([
"user.roles",
"user.groups",
"group.users",
"group.roles",
"group.apps",
"app.permissions",
]),
previousIds: zod.array(zod.number()),
newIds: zod.array(zod.number()),
addedIds: zod.array(zod.number()),
removedIds: zod.array(zod.number()),
createdAt: zod.coerce.date(),
actor: zod.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
}),
zod.null(),
]),
}),
),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
});
/**
* Returns permission-change audit records for the given group, newest
first. Captures changes to the group's user, role and app sets via
the discriminator field `changeKind`.
* @summary List recent permission-change audit entries for a group (admin)
*/
export const GetGroupPermissionAuditParams = zod.object({
id: zod.coerce.number(),
});
export const getGroupPermissionAuditQueryLimitDefault = 10;
export const getGroupPermissionAuditQueryLimitMax = 200;
export const getGroupPermissionAuditQueryOffsetDefault = 0;
export const getGroupPermissionAuditQueryOffsetMin = 0;
export const getGroupPermissionAuditQueryActorUserIdMin = 0;
export const GetGroupPermissionAuditQueryParams = zod.object({
limit: zod.coerce
.number()
.min(1)
.max(getGroupPermissionAuditQueryLimitMax)
.default(getGroupPermissionAuditQueryLimitDefault),
offset: zod.coerce
.number()
.min(getGroupPermissionAuditQueryOffsetMin)
.default(getGroupPermissionAuditQueryOffsetDefault),
actorUserId: zod.coerce
.number()
.min(getGroupPermissionAuditQueryActorUserIdMin)
.optional(),
from: zod.date().optional(),
to: zod.date().optional(),
});
export const GetGroupPermissionAuditResponse = zod.object({
entries: zod.array(
zod.object({
id: zod.number(),
targetKind: zod.enum(["user", "group", "app"]),
targetId: zod.number(),
changeKind: zod.enum([
"user.roles",
"user.groups",
"group.users",
"group.roles",
"group.apps",
"app.permissions",
]),
previousIds: zod.array(zod.number()),
newIds: zod.array(zod.number()),
addedIds: zod.array(zod.number()),
removedIds: zod.array(zod.number()),
createdAt: zod.coerce.date(),
actor: zod.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
}),
zod.null(),
]),
}),
),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
});
/**
* Returns permission-change audit records for the given app, newest
first. Captures changes to the set of permissions required to open
the app (`changeKind: app.permissions`).
* @summary List recent permission-change audit entries for an app (admin)
*/
export const GetAppPermissionAuditParams = zod.object({
id: zod.coerce.number(),
});
export const getAppPermissionAuditQueryLimitDefault = 10;
export const getAppPermissionAuditQueryLimitMax = 200;
export const getAppPermissionAuditQueryOffsetDefault = 0;
export const getAppPermissionAuditQueryOffsetMin = 0;
export const getAppPermissionAuditQueryActorUserIdMin = 0;
export const GetAppPermissionAuditQueryParams = zod.object({
limit: zod.coerce
.number()
.min(1)
.max(getAppPermissionAuditQueryLimitMax)
.default(getAppPermissionAuditQueryLimitDefault),
offset: zod.coerce
.number()
.min(getAppPermissionAuditQueryOffsetMin)
.default(getAppPermissionAuditQueryOffsetDefault),
actorUserId: zod.coerce
.number()
.min(getAppPermissionAuditQueryActorUserIdMin)
.optional(),
from: zod.date().optional(),
to: zod.date().optional(),
});
export const GetAppPermissionAuditResponse = zod.object({
entries: zod.array(
zod.object({
id: zod.number(),
targetKind: zod.enum(["user", "group", "app"]),
targetId: zod.number(),
changeKind: zod.enum([
"user.roles",
"user.groups",
"group.users",
"group.roles",
"group.apps",
"app.permissions",
]),
previousIds: zod.array(zod.number()),
newIds: zod.array(zod.number()),
addedIds: zod.array(zod.number()),
removedIds: zod.array(zod.number()),
createdAt: zod.coerce.date(),
actor: zod.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
}),
zod.null(),
]),
}),
),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
});
/**
* @summary Get home screen stats
*/
export const GetHomeStatsResponse = zod.object({
totalApps: zod.number(),
totalServices: zod.number(),
unreadNotifications: zod.number(),
totalUsers: zod.number(),
});
/**
* @summary Get admin dashboard trend stats
*/
export const getAdminStatsQueryRangeDefault = `7d`;
export const GetAdminStatsQueryParams = zod.object({
range: zod
.enum(["7d", "30d", "90d", "custom"])
.default(getAdminStatsQueryRangeDefault)
.describe('Time range for trend stats. Use \"custom\" with from\/to.'),
from: zod
.date()
.optional()
.describe("Start date (inclusive, YYYY-MM-DD UTC) when range=custom"),
to: zod
.date()
.optional()
.describe("End date (inclusive, YYYY-MM-DD UTC) when range=custom"),
});
export const GetAdminStatsResponse = zod.object({
range: zod.enum(["7d", "30d", "90d", "custom"]),
rangeDays: zod.number(),
rangeFrom: zod
.string()
.optional()
.describe("Start date (YYYY-MM-DD UTC) of the selected window"),
rangeTo: zod
.string()
.optional()
.describe("End date (YYYY-MM-DD UTC) of the selected window"),
newUsersInRange: zod.number(),
newUsersPrevRange: zod.number(),
activeServices: zod.number(),
inactiveServices: zod.number(),
signupsByDay: zod.array(
zod.object({
date: zod.string(),
count: zod.number(),
}),
),
appOpensByDay: zod.array(
zod.object({
date: zod.string(),
count: zod.number(),
}),
),
appOpensInRange: zod.number(),
appOpensPrevRange: zod.number(),
servicesCreatedByDay: zod.array(
zod.object({
date: zod.string(),
count: zod.number(),
}),
),
servicesCreatedInRange: zod.number(),
topApps: zod.array(
zod.object({
appId: zod.number(),
slug: zod.string(),
nameAr: zod.string(),
nameEn: zod.string(),
iconName: zod.string(),
color: zod.string(),
count: zod.number(),
}),
),
mostActiveUsers: zod.array(
zod.object({
userId: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
count: zod.number(),
}),
),
});
/**
* @summary Recent opens of a single app within the selected range
*/
export const GetAdminAppOpensByAppParams = zod.object({
appId: zod.coerce.number(),
});
export const getAdminAppOpensByAppQueryRangeDefault = `7d`;
export const getAdminAppOpensByAppQueryLimitDefault = 100;
export const getAdminAppOpensByAppQueryLimitMax = 200;
export const getAdminAppOpensByAppQueryOffsetDefault = 0;
export const getAdminAppOpensByAppQueryOffsetMin = 0;
export const GetAdminAppOpensByAppQueryParams = zod.object({
range: zod
.enum(["7d", "30d", "90d", "custom"])
.default(getAdminAppOpensByAppQueryRangeDefault),
from: zod.date().optional(),
to: zod.date().optional(),
limit: zod.coerce
.number()
.min(1)
.max(getAdminAppOpensByAppQueryLimitMax)
.default(getAdminAppOpensByAppQueryLimitDefault),
offset: zod.coerce
.number()
.min(getAdminAppOpensByAppQueryOffsetMin)
.default(getAdminAppOpensByAppQueryOffsetDefault),
});
export const GetAdminAppOpensByAppResponse = zod.object({
appId: zod.number(),
slug: zod.string(),
nameAr: zod.string(),
nameEn: zod.string(),
iconName: zod.string(),
color: zod.string(),
range: zod.enum(["7d", "30d", "90d", "custom"]),
rangeDays: zod.number(),
rangeFrom: zod.string(),
rangeTo: zod.string(),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
opens: zod.array(
zod.object({
id: zod.number(),
createdAt: zod.coerce.date(),
userId: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
}),
),
});
/**
* @summary Recent app opens by a single user within the selected range
*/
export const GetAdminAppOpensByUserParams = zod.object({
userId: zod.coerce.number(),
});
export const getAdminAppOpensByUserQueryRangeDefault = `7d`;
export const getAdminAppOpensByUserQueryLimitDefault = 100;
export const getAdminAppOpensByUserQueryLimitMax = 200;
export const getAdminAppOpensByUserQueryOffsetDefault = 0;
export const getAdminAppOpensByUserQueryOffsetMin = 0;
export const GetAdminAppOpensByUserQueryParams = zod.object({
range: zod
.enum(["7d", "30d", "90d", "custom"])
.default(getAdminAppOpensByUserQueryRangeDefault),
from: zod.date().optional(),
to: zod.date().optional(),
limit: zod.coerce
.number()
.min(1)
.max(getAdminAppOpensByUserQueryLimitMax)
.default(getAdminAppOpensByUserQueryLimitDefault),
offset: zod.coerce
.number()
.min(getAdminAppOpensByUserQueryOffsetMin)
.default(getAdminAppOpensByUserQueryOffsetDefault),
});
export const GetAdminAppOpensByUserResponse = zod.object({
userId: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
range: zod.enum(["7d", "30d", "90d", "custom"]),
rangeDays: zod.number(),
rangeFrom: zod.string(),
rangeTo: zod.string(),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
opens: zod.array(
zod.object({
id: zod.number(),
createdAt: zod.coerce.date(),
appId: zod.number(),
slug: zod.string(),
nameAr: zod.string(),
nameEn: zod.string(),
iconName: zod.string(),
color: zod.string(),
}),
),
});
/**
* Returns recent entries from the audit log, newest first. Admin only.
Filter by action (exact match) and/or a date range (inclusive).
* @summary List audit log entries (admin)
*/
export const listAuditLogsQueryForcedOnlyDefault = false;
export const listAuditLogsQueryLimitDefault = 50;
export const listAuditLogsQueryLimitMax = 200;
export const listAuditLogsQueryOffsetDefault = 0;
export const listAuditLogsQueryOffsetMin = 0;
export const ListAuditLogsQueryParams = zod.object({
action: zod.coerce
.string()
.optional()
.describe('Exact action name to filter by (e.g. \"group.force_delete\").'),
forcedOnly: zod.coerce
.boolean()
.default(listAuditLogsQueryForcedOnlyDefault)
.describe(
"When true, restrict results to forced deletions only — i.e. rows\nwith action `group.force_delete`, `user.force_delete`,\n`app.force_delete`, `service.force_delete`, or any\n`group.delete`\/`user.delete`\/`app.delete` row whose metadata\ncontains `force: true`. Overrides the `action` filter when set.\n",
),
from: zod
.date()
.optional()
.describe("Start date (inclusive, YYYY-MM-DD UTC)."),
to: zod.date().optional().describe("End date (inclusive, YYYY-MM-DD UTC)."),
targetType: zod.coerce
.string()
.optional()
.describe(
'Restrict results to entries whose `target_type` exactly matches\n(e.g. \"group\", \"app\", \"role\", \"user\", \"service\", \"settings\").\nCombines with `targetId` and the other filters.\n',
),
targetId: zod.coerce
.number()
.min(1)
.optional()
.describe(
"Restrict results to entries whose `target_id` exactly matches.\nTypically used together with `targetType` to focus on the history\nof a single entity.\n",
),
actorUserId: zod.coerce
.number()
.min(1)
.optional()
.describe(
"Restrict results to entries written by a specific acting user\n(matches `actor_user_id`). Combines with the other filters.\n",
),
limit: zod.coerce
.number()
.min(1)
.max(listAuditLogsQueryLimitMax)
.default(listAuditLogsQueryLimitDefault),
offset: zod.coerce
.number()
.min(listAuditLogsQueryOffsetMin)
.default(listAuditLogsQueryOffsetDefault),
});
export const ListAuditLogsResponse = zod.object({
entries: zod.array(
zod.object({
id: zod.number(),
action: zod.string(),
targetType: zod.string(),
targetId: zod.number().nullable(),
metadata: zod.record(zod.string(), zod.unknown()).nullable(),
createdAt: zod.coerce.date(),
actor: zod.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullish(),
displayNameEn: zod.string().nullish(),
avatarUrl: zod.string().nullish(),
}),
zod.null(),
]),
}),
),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
actions: zod
.array(zod.string())
.describe(
"All distinct action names present in the audit log (for building filter UI).",
),
});
/**
* Streams the filtered audit log as a CSV download.
Honors the same `action`, `forcedOnly`, `from`, `to`, `targetType`,
`targetId`, and `actorUserId` filters as the list endpoint, so an
export always matches whatever the admin is currently viewing.
Columns: action, actor_username, target_type, target_id, created_at, metadata.
Capped at 50,000 rows per export to bound response size.
* @summary Export filtered audit log as CSV (admin)
*/
export const exportAuditLogsCsvQueryForcedOnlyDefault = false;
export const ExportAuditLogsCsvQueryParams = zod.object({
action: zod.coerce
.string()
.optional()
.describe('Exact action name to filter by (e.g. \"group.force_delete\").'),
forcedOnly: zod.coerce
.boolean()
.default(exportAuditLogsCsvQueryForcedOnlyDefault)
.describe(
"When true, restrict the export to forced deletions only.\nSee the list endpoint for the exact set of matching rows.\nOverrides the `action` filter when set.\n",
),
from: zod
.date()
.optional()
.describe("Start date (inclusive, YYYY-MM-DD UTC)."),
to: zod.date().optional().describe("End date (inclusive, YYYY-MM-DD UTC)."),
targetType: zod.coerce
.string()
.optional()
.describe(
"Restrict the export to entries whose `target_type` exactly matches.\nCombines with `targetId` and the other filters.\n",
),
targetId: zod.coerce
.number()
.min(1)
.optional()
.describe(
"Restrict the export to entries whose `target_id` exactly matches.\n",
),
actorUserId: zod.coerce
.number()
.min(1)
.optional()
.describe(
"Restrict the export to entries written by a specific acting user.\n",
),
});
/**
* Returns the groups behind the inline "<n> groups" badge on the
admin Apps row, paginated. Admin only.
* @summary List groups granting access to an app
*/
export const GetAdminAppDependentGroupsParams = zod.object({
id: zod.coerce.number(),
});
export const getAdminAppDependentGroupsQueryLimitDefault = 50;
export const getAdminAppDependentGroupsQueryLimitMax = 200;
export const getAdminAppDependentGroupsQueryOffsetDefault = 0;
export const getAdminAppDependentGroupsQueryOffsetMin = 0;
export const GetAdminAppDependentGroupsQueryParams = zod.object({
limit: zod.coerce
.number()
.min(1)
.max(getAdminAppDependentGroupsQueryLimitMax)
.default(getAdminAppDependentGroupsQueryLimitDefault),
offset: zod.coerce
.number()
.min(getAdminAppDependentGroupsQueryOffsetMin)
.default(getAdminAppDependentGroupsQueryOffsetDefault),
});
export const GetAdminAppDependentGroupsResponse = zod.object({
items: zod.array(
zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullable(),
descriptionEn: zod.string().nullable(),
}),
),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
});
/**
* Returns the legacy permissions gating an app, with the role names
currently holding each permission, paginated. Admin only.
* @summary List permission restrictions on an app
*/
export const GetAdminAppDependentRestrictionsParams = zod.object({
id: zod.coerce.number(),
});
export const getAdminAppDependentRestrictionsQueryLimitDefault = 50;
export const getAdminAppDependentRestrictionsQueryLimitMax = 200;
export const getAdminAppDependentRestrictionsQueryOffsetDefault = 0;
export const getAdminAppDependentRestrictionsQueryOffsetMin = 0;
export const GetAdminAppDependentRestrictionsQueryParams = zod.object({
limit: zod.coerce
.number()
.min(1)
.max(getAdminAppDependentRestrictionsQueryLimitMax)
.default(getAdminAppDependentRestrictionsQueryLimitDefault),
offset: zod.coerce
.number()
.min(getAdminAppDependentRestrictionsQueryOffsetMin)
.default(getAdminAppDependentRestrictionsQueryOffsetDefault),
});
export const GetAdminAppDependentRestrictionsResponse = zod.object({
items: zod.array(
zod.object({
id: zod.number(),
name: zod.string(),
descriptionAr: zod.string().nullable(),
descriptionEn: zod.string().nullable(),
roles: zod
.array(zod.string())
.describe("Names of roles currently holding this permission."),
}),
),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
});
/**
* Returns every recorded open for the app newest-first, paginated.
Unlike /stats/admin/app-opens/by-app/{appId}, this endpoint does
not apply a date range so the badge total and the list match.
* @summary List recent opens for an app (no time filter)
*/
export const GetAdminAppDependentOpensParams = zod.object({
id: zod.coerce.number(),
});
export const getAdminAppDependentOpensQueryLimitDefault = 50;
export const getAdminAppDependentOpensQueryLimitMax = 200;
export const getAdminAppDependentOpensQueryOffsetDefault = 0;
export const getAdminAppDependentOpensQueryOffsetMin = 0;
export const GetAdminAppDependentOpensQueryParams = zod.object({
limit: zod.coerce
.number()
.min(1)
.max(getAdminAppDependentOpensQueryLimitMax)
.default(getAdminAppDependentOpensQueryLimitDefault),
offset: zod.coerce
.number()
.min(getAdminAppDependentOpensQueryOffsetMin)
.default(getAdminAppDependentOpensQueryOffsetDefault),
});
export const GetAdminAppDependentOpensResponse = zod.object({
items: zod.array(
zod.object({
id: zod.number(),
createdAt: zod.coerce.date(),
userId: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullable(),
displayNameEn: zod.string().nullable(),
avatarUrl: zod.string().nullable(),
}),
),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
});
/**
* Returns the service orders behind the inline "<n> orders" badge
on the admin Services row, paginated, newest-first. Admin only.
* @summary List orders placed against a service
*/
export const GetAdminServiceDependentOrdersParams = zod.object({
id: zod.coerce.number(),
});
export const getAdminServiceDependentOrdersQueryLimitDefault = 50;
export const getAdminServiceDependentOrdersQueryLimitMax = 200;
export const getAdminServiceDependentOrdersQueryOffsetDefault = 0;
export const getAdminServiceDependentOrdersQueryOffsetMin = 0;
export const GetAdminServiceDependentOrdersQueryParams = zod.object({
limit: zod.coerce
.number()
.min(1)
.max(getAdminServiceDependentOrdersQueryLimitMax)
.default(getAdminServiceDependentOrdersQueryLimitDefault),
offset: zod.coerce
.number()
.min(getAdminServiceDependentOrdersQueryOffsetMin)
.default(getAdminServiceDependentOrdersQueryOffsetDefault),
});
export const GetAdminServiceDependentOrdersResponse = zod.object({
items: zod.array(
zod.object({
id: zod.number(),
status: zod.string(),
notes: zod.string().nullable(),
createdAt: zod.coerce.date(),
userId: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullable(),
displayNameEn: zod.string().nullable(),
}),
),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
});
/**
* @summary Alias of GET /notes — list the caller's own notes
*/
export const listMyNotesAliasQueryArchivedDefault = false;
export const ListMyNotesAliasQueryParams = zod.object({
archived: zod.coerce.boolean().default(listMyNotesAliasQueryArchivedDefault),
});
export const ListMyNotesAliasResponseItem = zod
.object({
id: zod.number(),
userId: zod.number(),
title: zod.string(),
content: zod.string(),
color: zod.string(),
isPinned: zod.boolean(),
isArchived: zod.boolean(),
folderId: zod
.number()
.nullable()
.describe("Owning folder id, or null for Unfiled."),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
labelIds: zod.array(zod.number()),
})
.describe("A note owned by the caller (My Notes \/ Archive view).");
export const ListMyNotesAliasResponse = zod.array(ListMyNotesAliasResponseItem);
/**
* @summary List notes the caller has sent
*/
export const ListSentNotesResponseItem = zod.object({
id: zod.number(),
userId: zod.number(),
title: zod.string(),
content: zod.string(),
color: zod.string(),
isPinned: zod.boolean().optional(),
isArchived: zod.boolean().optional(),
folderId: zod
.number()
.nullish()
.describe("Owning folder id, or null for Unfiled."),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
recipients: zod.array(
zod.object({
id: zod.number(),
noteId: zod.number().optional(),
recipientUserId: zod.number(),
status: zod.enum(["unread", "read", "replied", "archived"]),
sentAt: zod.coerce.date(),
readAt: zod.coerce.date().nullable(),
archivedAt: zod.coerce.date().nullable(),
recipient: zod
.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullable(),
displayNameEn: zod.string().nullable(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean().optional(),
}),
zod.null(),
])
.optional(),
}),
),
replyCount: zod.number(),
});
export const ListSentNotesResponse = zod.array(ListSentNotesResponseItem);
/**
* @summary List notes sent to the caller
*/
export const listReceivedNotesQueryArchivedDefault = false;
export const ListReceivedNotesQueryParams = zod.object({
archived: zod.coerce.boolean().default(listReceivedNotesQueryArchivedDefault),
});
export const ListReceivedNotesResponseItem = zod.object({
id: zod.number(),
title: zod.string(),
content: zod.string(),
color: zod.string(),
folderId: zod
.number()
.nullish()
.describe("Owning folder id on the recipient's side, or null for Unfiled."),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
sender: zod
.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullable(),
displayNameEn: zod.string().nullable(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean().optional(),
}),
zod.null(),
])
.optional(),
recipientRowId: zod.number(),
status: zod.enum(["unread", "read", "replied", "archived"]),
sentAt: zod.coerce.date(),
readAt: zod.coerce.date().nullable(),
archivedAt: zod.coerce.date().nullable(),
});
export const ListReceivedNotesResponse = zod.array(
ListReceivedNotesResponseItem,
);
/**
* @summary Get a note's full thread (note + recipients + replies)
*/
export const GetNoteDetailParams = zod.object({
id: zod.coerce.number(),
});
export const GetNoteDetailResponse = zod.object({
id: zod.number(),
title: zod.string(),
content: zod.string(),
color: zod.string(),
createdAt: zod.coerce.date().nullish(),
updatedAt: zod.coerce.date().nullish(),
senderUserId: zod.number(),
sender: zod
.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullable(),
displayNameEn: zod.string().nullable(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean().optional(),
}),
zod.null(),
])
.optional(),
isOwner: zod.boolean(),
isAdmin: zod.boolean(),
myStatus: zod.union([
zod.enum(["unread", "read", "replied", "archived"]),
zod.null(),
]),
recipients: zod.array(
zod.object({
id: zod.number(),
noteId: zod.number().optional(),
recipientUserId: zod.number(),
status: zod.enum(["unread", "read", "replied", "archived"]),
sentAt: zod.coerce.date(),
readAt: zod.coerce.date().nullable(),
archivedAt: zod.coerce.date().nullable(),
recipient: zod
.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullable(),
displayNameEn: zod.string().nullable(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean().optional(),
}),
zod.null(),
])
.optional(),
}),
),
replies: zod.array(
zod.object({
id: zod.number(),
noteId: zod.number(),
senderUserId: zod.number(),
recipientUserId: zod.number(),
content: zod.string(),
createdAt: zod.coerce.date(),
sender: zod
.union([
zod.object({
id: zod.number(),
username: zod.string(),
displayNameAr: zod.string().nullable(),
displayNameEn: zod.string().nullable(),
avatarUrl: zod.string().nullish(),
isActive: zod.boolean().optional(),
}),
zod.null(),
])
.optional(),
}),
),
});
/**
* @summary Owner sends an existing note to a list of recipients
*/
export const SendNoteParams = zod.object({
id: zod.coerce.number(),
});
export const SendNoteBody = zod.object({
recipientUserIds: zod.array(zod.number()),
});
export const SendNoteResponse = zod.object({
success: zod.boolean(),
sent: zod.number(),
});
/**
* @summary Recipient marks their copy of a note read
*/
export const MarkNoteReadParams = zod.object({
id: zod.coerce.number(),
});
export const MarkNoteReadResponse = zod.object({
success: zod.boolean(),
});
/**
* @summary Recipient archives or unarchives their copy of a note
*/
export const ArchiveReceivedNoteParams = zod.object({
id: zod.coerce.number(),
});
export const ArchiveReceivedNoteBody = zod.object({
archived: zod.boolean(),
});
export const ArchiveReceivedNoteResponse = zod.object({
success: zod.boolean(),
});
/**
* @summary Sender or recipient adds a reply to a note's thread
*/
export const ReplyToNoteParams = zod.object({
id: zod.coerce.number(),
});
export const ReplyToNoteBody = zod.object({
content: zod.string(),
recipientUserId: zod
.number()
.optional()
.describe(
"Required when the note owner replies and there is more than one recipient.",
),
});
/**
* @summary List the caller's note folders with per-tab note counts
*/
export const listNoteFoldersQueryArchivedDefault = false;
export const ListNoteFoldersQueryParams = zod.object({
archived: zod.coerce
.boolean()
.default(listNoteFoldersQueryArchivedDefault)
.describe("Tab scope for noteCount (false = My Notes, true = Archive)"),
});
export const ListNoteFoldersResponseItem = zod.object({
id: zod.number(),
userId: zod.number(),
name: zod.string(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
noteCount: zod
.number()
.describe(
"Number of notes in this folder, scoped to the requested tab (active vs archived).",
),
});
export const ListNoteFoldersResponse = zod.array(ListNoteFoldersResponseItem);
/**
* @summary Create a new note folder owned by the caller
*/
export const createNoteFolderBodyNameMax = 80;
export const CreateNoteFolderBody = zod.object({
name: zod.string().min(1).max(createNoteFolderBodyNameMax),
});
/**
* @summary Rename a note folder
*/
export const UpdateNoteFolderParams = zod.object({
id: zod.coerce.number(),
});
export const updateNoteFolderQueryArchivedDefault = false;
export const UpdateNoteFolderQueryParams = zod.object({
archived: zod.coerce
.boolean()
.default(updateNoteFolderQueryArchivedDefault)
.describe(
"Tab scope for the returned noteCount (false = My Notes, true = Archive)",
),
});
export const updateNoteFolderBodyNameMax = 80;
export const UpdateNoteFolderBody = zod.object({
name: zod.string().min(1).max(updateNoteFolderBodyNameMax),
});
export const UpdateNoteFolderResponse = zod.object({
id: zod.number(),
userId: zod.number(),
name: zod.string(),
createdAt: zod.coerce.date(),
updatedAt: zod.coerce.date(),
noteCount: zod
.number()
.describe(
"Number of notes in this folder, scoped to the requested tab (active vs archived).",
),
});
/**
* @summary Delete a note folder. Notes inside are preserved (folderId set to null).
*/
export const DeleteNoteFolderParams = zod.object({
id: zod.coerce.number(),
});
export const DeleteNoteFolderResponse = zod.object({
success: zod.boolean(),
});
/**
* @summary List notes owned by a user
*/
export const GetAdminUserDependentNotesParams = zod.object({
id: zod.coerce.number(),
});
export const getAdminUserDependentNotesQueryLimitDefault = 50;
export const getAdminUserDependentNotesQueryLimitMax = 200;
export const getAdminUserDependentNotesQueryOffsetDefault = 0;
export const getAdminUserDependentNotesQueryOffsetMin = 0;
export const GetAdminUserDependentNotesQueryParams = zod.object({
limit: zod.coerce
.number()
.min(1)
.max(getAdminUserDependentNotesQueryLimitMax)
.default(getAdminUserDependentNotesQueryLimitDefault),
offset: zod.coerce
.number()
.min(getAdminUserDependentNotesQueryOffsetMin)
.default(getAdminUserDependentNotesQueryOffsetDefault),
});
export const GetAdminUserDependentNotesResponse = zod.object({
items: zod.array(
zod.object({
id: zod.number(),
title: zod.string(),
isPinned: zod.boolean(),
isArchived: zod.boolean(),
updatedAt: zod.coerce.date(),
}),
),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
});
/**
* @summary List service orders placed by a user
*/
export const GetAdminUserDependentOrdersParams = zod.object({
id: zod.coerce.number(),
});
export const getAdminUserDependentOrdersQueryLimitDefault = 50;
export const getAdminUserDependentOrdersQueryLimitMax = 200;
export const getAdminUserDependentOrdersQueryOffsetDefault = 0;
export const getAdminUserDependentOrdersQueryOffsetMin = 0;
export const GetAdminUserDependentOrdersQueryParams = zod.object({
limit: zod.coerce
.number()
.min(1)
.max(getAdminUserDependentOrdersQueryLimitMax)
.default(getAdminUserDependentOrdersQueryLimitDefault),
offset: zod.coerce
.number()
.min(getAdminUserDependentOrdersQueryOffsetMin)
.default(getAdminUserDependentOrdersQueryOffsetDefault),
});
export const GetAdminUserDependentOrdersResponse = zod.object({
items: zod.array(
zod.object({
id: zod.number(),
status: zod.string(),
notes: zod.string().nullable(),
createdAt: zod.coerce.date(),
serviceId: zod.number(),
serviceNameAr: zod.string(),
serviceNameEn: zod.string(),
}),
),
totalCount: zod.number(),
limit: zod.number(),
offset: zod.number(),
nextOffset: zod.number().nullable(),
});