Task #64: Service Orders — receiver page + admin role toggle
Backend (artifacts/api-server)
- Add admin-only role-toggle endpoints:
- POST /users/:id/roles { roleName } — idempotent, returns UserProfile
- DELETE /users/:id/roles/:roleName — idempotent, returns UserProfile
- Allow assigned receiver to cancel their own claimed order
(received/preparing) in PATCH /orders/:id/status. Owner & admin
rules unchanged.
- New backend test cases: receiver can cancel own assigned order;
another receiver gets 403.
API spec / codegen
- Add AddUserRoleBody schema and the two new role endpoints
under a new "roles" tag in lib/api-spec/openapi.yaml.
- Regenerated api-zod and api-client-react.
Frontend (artifacts/teaboy-os)
- New page src/pages/orders-incoming.tsx at /orders/incoming:
RBAC-gated (admin || order_receiver), shows "My active orders"
and "Awaiting receiver" sections, with claim, mark preparing,
mark completed and cancel buttons. Handles 409 already_claimed.
- Add Inbox button to home top bar, conditional on the same roles.
- Admin users table now has an "Order Receiver" Switch wired to
the new role-toggle hooks.
- Extend use-notifications-socket to invalidate the incoming-orders
query on order_incoming_changed and on notification_created with
type === "order".
- Bilingual locale keys (ar/en) for the new page and admin label.
Tests
- All 25 api-server tests pass (24 existing + 1 new receiver-cancel
case). All 3 teaboy-os e2e tests pass.
Follow-up filed: #67 (notify requester when receiver cancels).
This commit is contained in:
@@ -138,6 +138,11 @@ export interface UpdateUserBody {
|
||||
preferredLanguage?: string;
|
||||
}
|
||||
|
||||
export interface AddUserRoleBody {
|
||||
/** @minLength 1 */
|
||||
roleName: string;
|
||||
}
|
||||
|
||||
export interface App {
|
||||
id: number;
|
||||
slug: string;
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
|
||||
import type {
|
||||
AddParticipantsBody,
|
||||
AddUserRoleBody,
|
||||
AdminAppOpensByApp,
|
||||
AdminAppOpensByUser,
|
||||
AdminResetLinkResponse,
|
||||
@@ -4378,6 +4379,178 @@ export const useDeleteUser = <
|
||||
return useMutation(getDeleteUserMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Idempotently add a role to a user (admin)
|
||||
*/
|
||||
export const getAddUserRoleUrl = (id: number) => {
|
||||
return `/api/users/${id}/roles`;
|
||||
};
|
||||
|
||||
export const addUserRole = async (
|
||||
id: number,
|
||||
addUserRoleBody: AddUserRoleBody,
|
||||
options?: RequestInit,
|
||||
): Promise<UserProfile> => {
|
||||
return customFetch<UserProfile>(getAddUserRoleUrl(id), {
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(addUserRoleBody),
|
||||
});
|
||||
};
|
||||
|
||||
export const getAddUserRoleMutationOptions = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof addUserRole>>,
|
||||
TError,
|
||||
{ id: number; data: BodyType<AddUserRoleBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof addUserRole>>,
|
||||
TError,
|
||||
{ id: number; data: BodyType<AddUserRoleBody> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["addUserRole"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof addUserRole>>,
|
||||
{ id: number; data: BodyType<AddUserRoleBody> }
|
||||
> = (props) => {
|
||||
const { id, data } = props ?? {};
|
||||
|
||||
return addUserRole(id, data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type AddUserRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof addUserRole>>
|
||||
>;
|
||||
export type AddUserRoleMutationBody = BodyType<AddUserRoleBody>;
|
||||
export type AddUserRoleMutationError = ErrorType<ErrorResponse>;
|
||||
|
||||
/**
|
||||
* @summary Idempotently add a role to a user (admin)
|
||||
*/
|
||||
export const useAddUserRole = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof addUserRole>>,
|
||||
TError,
|
||||
{ id: number; data: BodyType<AddUserRoleBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof addUserRole>>,
|
||||
TError,
|
||||
{ id: number; data: BodyType<AddUserRoleBody> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getAddUserRoleMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Idempotently remove a role from a user (admin)
|
||||
*/
|
||||
export const getRemoveUserRoleUrl = (id: number, roleName: string) => {
|
||||
return `/api/users/${id}/roles/${roleName}`;
|
||||
};
|
||||
|
||||
export const removeUserRole = async (
|
||||
id: number,
|
||||
roleName: string,
|
||||
options?: RequestInit,
|
||||
): Promise<UserProfile> => {
|
||||
return customFetch<UserProfile>(getRemoveUserRoleUrl(id, roleName), {
|
||||
...options,
|
||||
method: "DELETE",
|
||||
});
|
||||
};
|
||||
|
||||
export const getRemoveUserRoleMutationOptions = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof removeUserRole>>,
|
||||
TError,
|
||||
{ id: number; roleName: string },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof removeUserRole>>,
|
||||
TError,
|
||||
{ id: number; roleName: string },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["removeUserRole"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof removeUserRole>>,
|
||||
{ id: number; roleName: string }
|
||||
> = (props) => {
|
||||
const { id, roleName } = props ?? {};
|
||||
|
||||
return removeUserRole(id, roleName, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type RemoveUserRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof removeUserRole>>
|
||||
>;
|
||||
|
||||
export type RemoveUserRoleMutationError = ErrorType<ErrorResponse>;
|
||||
|
||||
/**
|
||||
* @summary Idempotently remove a role from a user (admin)
|
||||
*/
|
||||
export const useRemoveUserRole = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof removeUserRole>>,
|
||||
TError,
|
||||
{ id: number; roleName: string },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof removeUserRole>>,
|
||||
TError,
|
||||
{ id: number; roleName: string },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getRemoveUserRoleMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get home screen stats
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user