From c0e55752a358b20f9a8e6fe2c361c4adfa4437a5 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Fri, 1 May 2026 07:32:11 +0000 Subject: [PATCH] Task #183: clickable dependency counts on admin Apps/Services/Users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turned the inline dependency-count badges on each admin row into focusable, keyboard-accessible buttons that open a drill-in modal listing the actual rows behind the count. Backend (artifacts/api-server/src/routes): - apps.ts: GET /admin/apps/:id/dependents/{groups,restrictions,opens} - services.ts: GET /admin/services/:id/dependents/orders - users.ts: GET /admin/users/:id/dependents/{notes,orders,conversations,messages} All paginated (limit default 50, max 200; offset) returning {items, totalCount, limit, offset, nextOffset}. Restrictions joins the role names that include each required permission. OpenAPI: - Added 8 path operations + 16 schemas (Item + Page) and re-ran `pnpm --filter @workspace/api-spec run codegen`. Frontend (artifacts/tx-os/src/pages/admin.tsx): - New DependencyDrillIn component reuses DrillInShell (Escape + backdrop close, RTL/LTR safe) and the existing LoadMoreSection pagination pattern from AppOpensDrillIn. - Each count part in Apps, Services, and Users panels is now a ))} @@ -1418,7 +1481,23 @@ export default function AdminPage() { className="text-[11px] text-muted-foreground pt-0.5" data-testid={`service-counts-${service.id}`} > - {t("admin.services.counts.orders", { count: service.orderCount })} + )} @@ -1619,6 +1698,13 @@ export default function AdminPage() { )} + {dependencyTarget && ( + setDependencyTarget(null)} + /> + )} ); } @@ -2737,6 +2823,495 @@ function UserOpensDrillIn({ ); } +// ===== Dependency drill-in ===== +// +// Generic shell + per-kind list rendering that turns each " X" count +// badge on the admin Apps / Services / Users rows into a clickable link +// that reveals the actual rows behind the count. Reuses DrillInShell so +// Escape-to-close + RTL/LTR layout already work; the counts themselves +// are rendered as ))} @@ -3155,6 +3768,13 @@ function UsersPanel({ /> ); })()} + {dependencyTarget && ( + setDependencyTarget(null)} + /> + )} ); } diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts index 460bcbc9..d13ff98e 100644 --- a/lib/api-client-react/src/generated/api.schemas.ts +++ b/lib/api-client-react/src/generated/api.schemas.ts @@ -912,6 +912,167 @@ export interface AdminAppOpensByUser { opens: AppOpenByUserEntry[]; } +export interface AppDependentGroupItem { + id: number; + name: string; + /** @nullable */ + descriptionAr: string | null; + /** @nullable */ + descriptionEn: string | null; +} + +export interface AppDependentGroupsPage { + items: AppDependentGroupItem[]; + totalCount: number; + limit: number; + offset: number; + /** @nullable */ + nextOffset: number | null; +} + +export interface AppDependentRestrictionItem { + id: number; + name: string; + /** @nullable */ + descriptionAr: string | null; + /** @nullable */ + descriptionEn: string | null; + /** Names of roles currently holding this permission. */ + roles: string[]; +} + +export interface AppDependentRestrictionsPage { + items: AppDependentRestrictionItem[]; + totalCount: number; + limit: number; + offset: number; + /** @nullable */ + nextOffset: number | null; +} + +export interface AppDependentOpenItem { + id: number; + createdAt: string; + userId: number; + username: string; + /** @nullable */ + displayNameAr: string | null; + /** @nullable */ + displayNameEn: string | null; + /** @nullable */ + avatarUrl: string | null; +} + +export interface AppDependentOpensPage { + items: AppDependentOpenItem[]; + totalCount: number; + limit: number; + offset: number; + /** @nullable */ + nextOffset: number | null; +} + +export interface ServiceDependentOrderItem { + id: number; + status: string; + /** @nullable */ + notes: string | null; + createdAt: string; + userId: number; + username: string; + /** @nullable */ + displayNameAr: string | null; + /** @nullable */ + displayNameEn: string | null; +} + +export interface ServiceDependentOrdersPage { + items: ServiceDependentOrderItem[]; + totalCount: number; + limit: number; + offset: number; + /** @nullable */ + nextOffset: number | null; +} + +export interface UserDependentNoteItem { + id: number; + title: string; + isPinned: boolean; + isArchived: boolean; + updatedAt: string; +} + +export interface UserDependentNotesPage { + items: UserDependentNoteItem[]; + totalCount: number; + limit: number; + offset: number; + /** @nullable */ + nextOffset: number | null; +} + +export interface UserDependentOrderItem { + id: number; + status: string; + /** @nullable */ + notes: string | null; + createdAt: string; + serviceId: number; + serviceNameAr: string; + serviceNameEn: string; +} + +export interface UserDependentOrdersPage { + items: UserDependentOrderItem[]; + totalCount: number; + limit: number; + offset: number; + /** @nullable */ + nextOffset: number | null; +} + +export interface UserDependentConversationItem { + id: number; + /** @nullable */ + nameAr: string | null; + /** @nullable */ + nameEn: string | null; + isGroup: boolean; + createdAt: string; +} + +export interface UserDependentConversationsPage { + items: UserDependentConversationItem[]; + totalCount: number; + limit: number; + offset: number; + /** @nullable */ + nextOffset: number | null; +} + +export interface UserDependentMessageItem { + id: number; + content: string; + kind: string; + createdAt: string; + conversationId: number; + /** @nullable */ + conversationNameAr: string | null; + /** @nullable */ + conversationNameEn: string | null; + conversationIsGroup: boolean; +} + +export interface UserDependentMessagesPage { + items: UserDependentMessageItem[]; + totalCount: number; + limit: number; + offset: number; + /** @nullable */ + nextOffset: number | null; +} + export interface AuditLogActor { id: number; username: string; @@ -1327,3 +1488,99 @@ Combines with `targetId` and the other filters. */ actorUserId?: number; }; + +export type GetAdminAppDependentGroupsParams = { + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; +}; + +export type GetAdminAppDependentRestrictionsParams = { + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; +}; + +export type GetAdminAppDependentOpensParams = { + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; +}; + +export type GetAdminServiceDependentOrdersParams = { + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; +}; + +export type GetAdminUserDependentNotesParams = { + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; +}; + +export type GetAdminUserDependentOrdersParams = { + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; +}; + +export type GetAdminUserDependentConversationsParams = { + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; +}; + +export type GetAdminUserDependentMessagesParams = { + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; +}; diff --git a/lib/api-client-react/src/generated/api.ts b/lib/api-client-react/src/generated/api.ts index 96b6c408..e8755ece 100644 --- a/lib/api-client-react/src/generated/api.ts +++ b/lib/api-client-react/src/generated/api.ts @@ -26,6 +26,9 @@ import type { AdminStats, App, AppDeletionConflict, + AppDependentGroupsPage, + AppDependentOpensPage, + AppDependentRestrictionsPage, AppPermissionLink, AppPermissionsImpact, AppPermissionsImpactBody, @@ -48,9 +51,17 @@ import type { ExportAuditLogsCsvParams, ForgotPasswordBody, ForgotPasswordResponse, + GetAdminAppDependentGroupsParams, + GetAdminAppDependentOpensParams, + GetAdminAppDependentRestrictionsParams, GetAdminAppOpensByAppParams, GetAdminAppOpensByUserParams, + GetAdminServiceDependentOrdersParams, GetAdminStatsParams, + GetAdminUserDependentConversationsParams, + GetAdminUserDependentMessagesParams, + GetAdminUserDependentNotesParams, + GetAdminUserDependentOrdersParams, GetAppPermissionAuditParams, GetGroupPermissionAuditParams, GetRolePermissionAuditParams, @@ -84,6 +95,7 @@ import type { Service, ServiceCategory, ServiceDeletionConflict, + ServiceDependentOrdersPage, ServiceOrder, SuccessResponse, UpdateAppBody, @@ -100,6 +112,10 @@ import type { UpdateServiceOrderStatusBody, UpdateUserBody, UserDeletionConflict, + UserDependentConversationsPage, + UserDependentMessagesPage, + UserDependentNotesPage, + UserDependentOrdersPage, UserProfile, VerifyResetTokenBody, VerifyResetTokenResponse, @@ -7423,3 +7439,986 @@ export function useExportAuditLogsCsv< return { ...query, queryKey: queryOptions.queryKey }; } + +/** + * Returns the groups behind the inline " groups" badge on the +admin Apps row, paginated. Admin only. + + * @summary List groups granting access to an app + */ +export const getGetAdminAppDependentGroupsUrl = ( + id: number, + params?: GetAdminAppDependentGroupsParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/admin/apps/${id}/dependents/groups?${stringifiedParams}` + : `/api/admin/apps/${id}/dependents/groups`; +}; + +export const getAdminAppDependentGroups = async ( + id: number, + params?: GetAdminAppDependentGroupsParams, + options?: RequestInit, +): Promise => { + return customFetch( + getGetAdminAppDependentGroupsUrl(id, params), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetAdminAppDependentGroupsQueryKey = ( + id: number, + params?: GetAdminAppDependentGroupsParams, +) => { + return [ + `/api/admin/apps/${id}/dependents/groups`, + ...(params ? [params] : []), + ] as const; +}; + +export const getGetAdminAppDependentGroupsQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminAppDependentGroupsParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? getGetAdminAppDependentGroupsQueryKey(id, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + getAdminAppDependentGroups(id, params, { signal, ...requestOptions }); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetAdminAppDependentGroupsQueryResult = NonNullable< + Awaited> +>; +export type GetAdminAppDependentGroupsQueryError = ErrorType; + +/** + * @summary List groups granting access to an app + */ + +export function useGetAdminAppDependentGroups< + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminAppDependentGroupsParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetAdminAppDependentGroupsQueryOptions( + id, + params, + options, + ); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * 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 getGetAdminAppDependentRestrictionsUrl = ( + id: number, + params?: GetAdminAppDependentRestrictionsParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/admin/apps/${id}/dependents/restrictions?${stringifiedParams}` + : `/api/admin/apps/${id}/dependents/restrictions`; +}; + +export const getAdminAppDependentRestrictions = async ( + id: number, + params?: GetAdminAppDependentRestrictionsParams, + options?: RequestInit, +): Promise => { + return customFetch( + getGetAdminAppDependentRestrictionsUrl(id, params), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetAdminAppDependentRestrictionsQueryKey = ( + id: number, + params?: GetAdminAppDependentRestrictionsParams, +) => { + return [ + `/api/admin/apps/${id}/dependents/restrictions`, + ...(params ? [params] : []), + ] as const; +}; + +export const getGetAdminAppDependentRestrictionsQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminAppDependentRestrictionsParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetAdminAppDependentRestrictionsQueryKey(id, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + getAdminAppDependentRestrictions(id, params, { signal, ...requestOptions }); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetAdminAppDependentRestrictionsQueryResult = NonNullable< + Awaited> +>; +export type GetAdminAppDependentRestrictionsQueryError = + ErrorType; + +/** + * @summary List permission restrictions on an app + */ + +export function useGetAdminAppDependentRestrictions< + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminAppDependentRestrictionsParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetAdminAppDependentRestrictionsQueryOptions( + id, + params, + options, + ); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * 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 getGetAdminAppDependentOpensUrl = ( + id: number, + params?: GetAdminAppDependentOpensParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/admin/apps/${id}/dependents/opens?${stringifiedParams}` + : `/api/admin/apps/${id}/dependents/opens`; +}; + +export const getAdminAppDependentOpens = async ( + id: number, + params?: GetAdminAppDependentOpensParams, + options?: RequestInit, +): Promise => { + return customFetch( + getGetAdminAppDependentOpensUrl(id, params), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetAdminAppDependentOpensQueryKey = ( + id: number, + params?: GetAdminAppDependentOpensParams, +) => { + return [ + `/api/admin/apps/${id}/dependents/opens`, + ...(params ? [params] : []), + ] as const; +}; + +export const getGetAdminAppDependentOpensQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminAppDependentOpensParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? getGetAdminAppDependentOpensQueryKey(id, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + getAdminAppDependentOpens(id, params, { signal, ...requestOptions }); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetAdminAppDependentOpensQueryResult = NonNullable< + Awaited> +>; +export type GetAdminAppDependentOpensQueryError = ErrorType; + +/** + * @summary List recent opens for an app (no time filter) + */ + +export function useGetAdminAppDependentOpens< + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminAppDependentOpensParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetAdminAppDependentOpensQueryOptions( + id, + params, + options, + ); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Returns the service orders behind the inline " orders" badge +on the admin Services row, paginated, newest-first. Admin only. + + * @summary List orders placed against a service + */ +export const getGetAdminServiceDependentOrdersUrl = ( + id: number, + params?: GetAdminServiceDependentOrdersParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/admin/services/${id}/dependents/orders?${stringifiedParams}` + : `/api/admin/services/${id}/dependents/orders`; +}; + +export const getAdminServiceDependentOrders = async ( + id: number, + params?: GetAdminServiceDependentOrdersParams, + options?: RequestInit, +): Promise => { + return customFetch( + getGetAdminServiceDependentOrdersUrl(id, params), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetAdminServiceDependentOrdersQueryKey = ( + id: number, + params?: GetAdminServiceDependentOrdersParams, +) => { + return [ + `/api/admin/services/${id}/dependents/orders`, + ...(params ? [params] : []), + ] as const; +}; + +export const getGetAdminServiceDependentOrdersQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminServiceDependentOrdersParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetAdminServiceDependentOrdersQueryKey(id, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + getAdminServiceDependentOrders(id, params, { signal, ...requestOptions }); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetAdminServiceDependentOrdersQueryResult = NonNullable< + Awaited> +>; +export type GetAdminServiceDependentOrdersQueryError = ErrorType; + +/** + * @summary List orders placed against a service + */ + +export function useGetAdminServiceDependentOrders< + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminServiceDependentOrdersParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetAdminServiceDependentOrdersQueryOptions( + id, + params, + options, + ); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * @summary List notes owned by a user + */ +export const getGetAdminUserDependentNotesUrl = ( + id: number, + params?: GetAdminUserDependentNotesParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/admin/users/${id}/dependents/notes?${stringifiedParams}` + : `/api/admin/users/${id}/dependents/notes`; +}; + +export const getAdminUserDependentNotes = async ( + id: number, + params?: GetAdminUserDependentNotesParams, + options?: RequestInit, +): Promise => { + return customFetch( + getGetAdminUserDependentNotesUrl(id, params), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetAdminUserDependentNotesQueryKey = ( + id: number, + params?: GetAdminUserDependentNotesParams, +) => { + return [ + `/api/admin/users/${id}/dependents/notes`, + ...(params ? [params] : []), + ] as const; +}; + +export const getGetAdminUserDependentNotesQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminUserDependentNotesParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? getGetAdminUserDependentNotesQueryKey(id, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + getAdminUserDependentNotes(id, params, { signal, ...requestOptions }); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetAdminUserDependentNotesQueryResult = NonNullable< + Awaited> +>; +export type GetAdminUserDependentNotesQueryError = ErrorType; + +/** + * @summary List notes owned by a user + */ + +export function useGetAdminUserDependentNotes< + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminUserDependentNotesParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetAdminUserDependentNotesQueryOptions( + id, + params, + options, + ); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * @summary List service orders placed by a user + */ +export const getGetAdminUserDependentOrdersUrl = ( + id: number, + params?: GetAdminUserDependentOrdersParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/admin/users/${id}/dependents/orders?${stringifiedParams}` + : `/api/admin/users/${id}/dependents/orders`; +}; + +export const getAdminUserDependentOrders = async ( + id: number, + params?: GetAdminUserDependentOrdersParams, + options?: RequestInit, +): Promise => { + return customFetch( + getGetAdminUserDependentOrdersUrl(id, params), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetAdminUserDependentOrdersQueryKey = ( + id: number, + params?: GetAdminUserDependentOrdersParams, +) => { + return [ + `/api/admin/users/${id}/dependents/orders`, + ...(params ? [params] : []), + ] as const; +}; + +export const getGetAdminUserDependentOrdersQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminUserDependentOrdersParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetAdminUserDependentOrdersQueryKey(id, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + getAdminUserDependentOrders(id, params, { signal, ...requestOptions }); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetAdminUserDependentOrdersQueryResult = NonNullable< + Awaited> +>; +export type GetAdminUserDependentOrdersQueryError = ErrorType; + +/** + * @summary List service orders placed by a user + */ + +export function useGetAdminUserDependentOrders< + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminUserDependentOrdersParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetAdminUserDependentOrdersQueryOptions( + id, + params, + options, + ); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * @summary List conversations created by a user + */ +export const getGetAdminUserDependentConversationsUrl = ( + id: number, + params?: GetAdminUserDependentConversationsParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/admin/users/${id}/dependents/conversations?${stringifiedParams}` + : `/api/admin/users/${id}/dependents/conversations`; +}; + +export const getAdminUserDependentConversations = async ( + id: number, + params?: GetAdminUserDependentConversationsParams, + options?: RequestInit, +): Promise => { + return customFetch( + getGetAdminUserDependentConversationsUrl(id, params), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetAdminUserDependentConversationsQueryKey = ( + id: number, + params?: GetAdminUserDependentConversationsParams, +) => { + return [ + `/api/admin/users/${id}/dependents/conversations`, + ...(params ? [params] : []), + ] as const; +}; + +export const getGetAdminUserDependentConversationsQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminUserDependentConversationsParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetAdminUserDependentConversationsQueryKey(id, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + getAdminUserDependentConversations(id, params, { + signal, + ...requestOptions, + }); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetAdminUserDependentConversationsQueryResult = NonNullable< + Awaited> +>; +export type GetAdminUserDependentConversationsQueryError = + ErrorType; + +/** + * @summary List conversations created by a user + */ + +export function useGetAdminUserDependentConversations< + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminUserDependentConversationsParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetAdminUserDependentConversationsQueryOptions( + id, + params, + options, + ); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * @summary List messages sent by a user + */ +export const getGetAdminUserDependentMessagesUrl = ( + id: number, + params?: GetAdminUserDependentMessagesParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/admin/users/${id}/dependents/messages?${stringifiedParams}` + : `/api/admin/users/${id}/dependents/messages`; +}; + +export const getAdminUserDependentMessages = async ( + id: number, + params?: GetAdminUserDependentMessagesParams, + options?: RequestInit, +): Promise => { + return customFetch( + getGetAdminUserDependentMessagesUrl(id, params), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetAdminUserDependentMessagesQueryKey = ( + id: number, + params?: GetAdminUserDependentMessagesParams, +) => { + return [ + `/api/admin/users/${id}/dependents/messages`, + ...(params ? [params] : []), + ] as const; +}; + +export const getGetAdminUserDependentMessagesQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminUserDependentMessagesParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetAdminUserDependentMessagesQueryKey(id, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + getAdminUserDependentMessages(id, params, { signal, ...requestOptions }); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetAdminUserDependentMessagesQueryResult = NonNullable< + Awaited> +>; +export type GetAdminUserDependentMessagesQueryError = ErrorType; + +/** + * @summary List messages sent by a user + */ + +export function useGetAdminUserDependentMessages< + TData = Awaited>, + TError = ErrorType, +>( + id: number, + params?: GetAdminUserDependentMessagesParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetAdminUserDependentMessagesQueryOptions( + id, + params, + options, + ); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index c0788568..d6772286 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -2465,6 +2465,275 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" + /admin/apps/{id}/dependents/groups: + get: + operationId: getAdminAppDependentGroups + tags: [apps] + summary: List groups granting access to an app + description: | + Returns the groups behind the inline " groups" badge on the + admin Apps row, paginated. Admin only. + parameters: + - in: path + name: id + required: true + schema: { type: integer } + - in: query + name: limit + required: false + schema: { type: integer, minimum: 1, maximum: 200, default: 50 } + - in: query + name: offset + required: false + schema: { type: integer, minimum: 0, default: 0 } + responses: + "200": + description: Paged list of groups granting the app + content: + application/json: + schema: + $ref: "#/components/schemas/AppDependentGroupsPage" + "404": + description: App not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + /admin/apps/{id}/dependents/restrictions: + get: + operationId: getAdminAppDependentRestrictions + tags: [apps] + summary: List permission restrictions on an app + description: | + Returns the legacy permissions gating an app, with the role names + currently holding each permission, paginated. Admin only. + parameters: + - in: path + name: id + required: true + schema: { type: integer } + - in: query + name: limit + required: false + schema: { type: integer, minimum: 1, maximum: 200, default: 50 } + - in: query + name: offset + required: false + schema: { type: integer, minimum: 0, default: 0 } + responses: + "200": + description: Paged list of restrictions on the app + content: + application/json: + schema: + $ref: "#/components/schemas/AppDependentRestrictionsPage" + "404": + description: App not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + /admin/apps/{id}/dependents/opens: + get: + operationId: getAdminAppDependentOpens + tags: [apps] + summary: List recent opens for an app (no time filter) + description: | + 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. + parameters: + - in: path + name: id + required: true + schema: { type: integer } + - in: query + name: limit + required: false + schema: { type: integer, minimum: 1, maximum: 200, default: 50 } + - in: query + name: offset + required: false + schema: { type: integer, minimum: 0, default: 0 } + responses: + "200": + description: Paged list of opens + content: + application/json: + schema: + $ref: "#/components/schemas/AppDependentOpensPage" + "404": + description: App not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + /admin/services/{id}/dependents/orders: + get: + operationId: getAdminServiceDependentOrders + tags: [services] + summary: List orders placed against a service + description: | + Returns the service orders behind the inline " orders" badge + on the admin Services row, paginated, newest-first. Admin only. + parameters: + - in: path + name: id + required: true + schema: { type: integer } + - in: query + name: limit + required: false + schema: { type: integer, minimum: 1, maximum: 200, default: 50 } + - in: query + name: offset + required: false + schema: { type: integer, minimum: 0, default: 0 } + responses: + "200": + description: Paged list of orders for the service + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceDependentOrdersPage" + "404": + description: Service not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + /admin/users/{id}/dependents/notes: + get: + operationId: getAdminUserDependentNotes + tags: [users] + summary: List notes owned by a user + parameters: + - in: path + name: id + required: true + schema: { type: integer } + - in: query + name: limit + required: false + schema: { type: integer, minimum: 1, maximum: 200, default: 50 } + - in: query + name: offset + required: false + schema: { type: integer, minimum: 0, default: 0 } + responses: + "200": + description: Paged list of notes + content: + application/json: + schema: + $ref: "#/components/schemas/UserDependentNotesPage" + "404": + description: User not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + /admin/users/{id}/dependents/orders: + get: + operationId: getAdminUserDependentOrders + tags: [users] + summary: List service orders placed by a user + parameters: + - in: path + name: id + required: true + schema: { type: integer } + - in: query + name: limit + required: false + schema: { type: integer, minimum: 1, maximum: 200, default: 50 } + - in: query + name: offset + required: false + schema: { type: integer, minimum: 0, default: 0 } + responses: + "200": + description: Paged list of orders + content: + application/json: + schema: + $ref: "#/components/schemas/UserDependentOrdersPage" + "404": + description: User not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + /admin/users/{id}/dependents/conversations: + get: + operationId: getAdminUserDependentConversations + tags: [users] + summary: List conversations created by a user + parameters: + - in: path + name: id + required: true + schema: { type: integer } + - in: query + name: limit + required: false + schema: { type: integer, minimum: 1, maximum: 200, default: 50 } + - in: query + name: offset + required: false + schema: { type: integer, minimum: 0, default: 0 } + responses: + "200": + description: Paged list of conversations + content: + application/json: + schema: + $ref: "#/components/schemas/UserDependentConversationsPage" + "404": + description: User not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + /admin/users/{id}/dependents/messages: + get: + operationId: getAdminUserDependentMessages + tags: [users] + summary: List messages sent by a user + parameters: + - in: path + name: id + required: true + schema: { type: integer } + - in: query + name: limit + required: false + schema: { type: integer, minimum: 1, maximum: 200, default: 50 } + - in: query + name: offset + required: false + schema: { type: integer, minimum: 0, default: 0 } + responses: + "200": + description: Paged list of messages + content: + application/json: + schema: + $ref: "#/components/schemas/UserDependentMessagesPage" + "404": + description: User not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + components: schemas: HealthStatus: @@ -4098,6 +4367,208 @@ components: - nextOffset - opens + # ===== Dependent-item drill-ins ===== + # Each " X" badge on the admin Apps / Services / Users rows is now + # clickable; the click loads one of the schemas below. They share the + # same envelope (totalCount + limit/offset/nextOffset) so the frontend + # can use a single LoadMoreSection for them all. + + AppDependentGroupItem: + type: object + properties: + id: { type: integer } + name: { type: string } + descriptionAr: { type: ["string", "null"] } + descriptionEn: { type: ["string", "null"] } + required: [id, name, descriptionAr, descriptionEn] + + AppDependentGroupsPage: + type: object + properties: + items: + type: array + items: + $ref: "#/components/schemas/AppDependentGroupItem" + totalCount: { type: integer } + limit: { type: integer } + offset: { type: integer } + nextOffset: { type: ["integer", "null"] } + required: [items, totalCount, limit, offset, nextOffset] + + AppDependentRestrictionItem: + type: object + properties: + id: { type: integer } + name: { type: string } + descriptionAr: { type: ["string", "null"] } + descriptionEn: { type: ["string", "null"] } + roles: + type: array + description: Names of roles currently holding this permission. + items: { type: string } + required: [id, name, descriptionAr, descriptionEn, roles] + + AppDependentRestrictionsPage: + type: object + properties: + items: + type: array + items: + $ref: "#/components/schemas/AppDependentRestrictionItem" + totalCount: { type: integer } + limit: { type: integer } + offset: { type: integer } + nextOffset: { type: ["integer", "null"] } + required: [items, totalCount, limit, offset, nextOffset] + + AppDependentOpenItem: + type: object + properties: + id: { type: integer } + createdAt: { type: string, format: date-time } + userId: { type: integer } + username: { type: string } + displayNameAr: { type: ["string", "null"] } + displayNameEn: { type: ["string", "null"] } + avatarUrl: { type: ["string", "null"] } + required: [id, createdAt, userId, username, displayNameAr, displayNameEn, avatarUrl] + + AppDependentOpensPage: + type: object + properties: + items: + type: array + items: + $ref: "#/components/schemas/AppDependentOpenItem" + totalCount: { type: integer } + limit: { type: integer } + offset: { type: integer } + nextOffset: { type: ["integer", "null"] } + required: [items, totalCount, limit, offset, nextOffset] + + ServiceDependentOrderItem: + type: object + properties: + id: { type: integer } + status: { type: string } + notes: { type: ["string", "null"] } + createdAt: { type: string, format: date-time } + userId: { type: integer } + username: { type: string } + displayNameAr: { type: ["string", "null"] } + displayNameEn: { type: ["string", "null"] } + required: [id, status, notes, createdAt, userId, username, displayNameAr, displayNameEn] + + ServiceDependentOrdersPage: + type: object + properties: + items: + type: array + items: + $ref: "#/components/schemas/ServiceDependentOrderItem" + totalCount: { type: integer } + limit: { type: integer } + offset: { type: integer } + nextOffset: { type: ["integer", "null"] } + required: [items, totalCount, limit, offset, nextOffset] + + UserDependentNoteItem: + type: object + properties: + id: { type: integer } + title: { type: string } + isPinned: { type: boolean } + isArchived: { type: boolean } + updatedAt: { type: string, format: date-time } + required: [id, title, isPinned, isArchived, updatedAt] + + UserDependentNotesPage: + type: object + properties: + items: + type: array + items: + $ref: "#/components/schemas/UserDependentNoteItem" + totalCount: { type: integer } + limit: { type: integer } + offset: { type: integer } + nextOffset: { type: ["integer", "null"] } + required: [items, totalCount, limit, offset, nextOffset] + + UserDependentOrderItem: + type: object + properties: + id: { type: integer } + status: { type: string } + notes: { type: ["string", "null"] } + createdAt: { type: string, format: date-time } + serviceId: { type: integer } + serviceNameAr: { type: string } + serviceNameEn: { type: string } + required: [id, status, notes, createdAt, serviceId, serviceNameAr, serviceNameEn] + + UserDependentOrdersPage: + type: object + properties: + items: + type: array + items: + $ref: "#/components/schemas/UserDependentOrderItem" + totalCount: { type: integer } + limit: { type: integer } + offset: { type: integer } + nextOffset: { type: ["integer", "null"] } + required: [items, totalCount, limit, offset, nextOffset] + + UserDependentConversationItem: + type: object + properties: + id: { type: integer } + nameAr: { type: ["string", "null"] } + nameEn: { type: ["string", "null"] } + isGroup: { type: boolean } + createdAt: { type: string, format: date-time } + required: [id, nameAr, nameEn, isGroup, createdAt] + + UserDependentConversationsPage: + type: object + properties: + items: + type: array + items: + $ref: "#/components/schemas/UserDependentConversationItem" + totalCount: { type: integer } + limit: { type: integer } + offset: { type: integer } + nextOffset: { type: ["integer", "null"] } + required: [items, totalCount, limit, offset, nextOffset] + + UserDependentMessageItem: + type: object + properties: + id: { type: integer } + content: { type: string } + kind: { type: string } + createdAt: { type: string, format: date-time } + conversationId: { type: integer } + conversationNameAr: { type: ["string", "null"] } + conversationNameEn: { type: ["string", "null"] } + conversationIsGroup: { type: boolean } + required: [id, content, kind, createdAt, conversationId, conversationNameAr, conversationNameEn, conversationIsGroup] + + UserDependentMessagesPage: + type: object + properties: + items: + type: array + items: + $ref: "#/components/schemas/UserDependentMessageItem" + totalCount: { type: integer } + limit: { type: integer } + offset: { type: integer } + nextOffset: { type: ["integer", "null"] } + required: [items, totalCount, limit, offset, nextOffset] + TopUserItem: type: object properties: diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index 6b1f8bd2..5ea149cc 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -2884,3 +2884,355 @@ export const ExportAuditLogsCsvQueryParams = zod.object({ "Restrict the export to entries written by a specific acting user.\n", ), }); + +/** + * Returns the groups behind the inline " 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 " 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 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(), +}); + +/** + * @summary List conversations created by a user + */ +export const GetAdminUserDependentConversationsParams = zod.object({ + id: zod.coerce.number(), +}); + +export const getAdminUserDependentConversationsQueryLimitDefault = 50; +export const getAdminUserDependentConversationsQueryLimitMax = 200; + +export const getAdminUserDependentConversationsQueryOffsetDefault = 0; +export const getAdminUserDependentConversationsQueryOffsetMin = 0; + +export const GetAdminUserDependentConversationsQueryParams = zod.object({ + limit: zod.coerce + .number() + .min(1) + .max(getAdminUserDependentConversationsQueryLimitMax) + .default(getAdminUserDependentConversationsQueryLimitDefault), + offset: zod.coerce + .number() + .min(getAdminUserDependentConversationsQueryOffsetMin) + .default(getAdminUserDependentConversationsQueryOffsetDefault), +}); + +export const GetAdminUserDependentConversationsResponse = zod.object({ + items: zod.array( + zod.object({ + id: zod.number(), + nameAr: zod.string().nullable(), + nameEn: zod.string().nullable(), + isGroup: zod.boolean(), + createdAt: zod.coerce.date(), + }), + ), + totalCount: zod.number(), + limit: zod.number(), + offset: zod.number(), + nextOffset: zod.number().nullable(), +}); + +/** + * @summary List messages sent by a user + */ +export const GetAdminUserDependentMessagesParams = zod.object({ + id: zod.coerce.number(), +}); + +export const getAdminUserDependentMessagesQueryLimitDefault = 50; +export const getAdminUserDependentMessagesQueryLimitMax = 200; + +export const getAdminUserDependentMessagesQueryOffsetDefault = 0; +export const getAdminUserDependentMessagesQueryOffsetMin = 0; + +export const GetAdminUserDependentMessagesQueryParams = zod.object({ + limit: zod.coerce + .number() + .min(1) + .max(getAdminUserDependentMessagesQueryLimitMax) + .default(getAdminUserDependentMessagesQueryLimitDefault), + offset: zod.coerce + .number() + .min(getAdminUserDependentMessagesQueryOffsetMin) + .default(getAdminUserDependentMessagesQueryOffsetDefault), +}); + +export const GetAdminUserDependentMessagesResponse = zod.object({ + items: zod.array( + zod.object({ + id: zod.number(), + content: zod.string(), + kind: zod.string(), + createdAt: zod.coerce.date(), + conversationId: zod.number(), + conversationNameAr: zod.string().nullable(), + conversationNameEn: zod.string().nullable(), + conversationIsGroup: zod.boolean(), + }), + ), + totalCount: zod.number(), + limit: zod.number(), + offset: zod.number(), + nextOffset: zod.number().nullable(), +});