Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Eleazarguitar18/kantuta_pos_front/llms.txt

Use this file to discover all available pages before exploring further.

Kantuta POS enforces a two-tier role hierarchy: Administrador and Cajero. Every authenticated request carries a JWT that encodes the user’s role, and the ProtectedRoute component intercepts navigation at the router level before any protected view renders. Administrators can create and edit user accounts through the /administracion/usuarios module; cashiers have no access to any administration routes.

Roles and Capabilities

The system defines two roles stored in the role.nombre field of every Usuario object:
CapabilityAdministradorCajero
Access /administracion routes
Access /reportes routes
Product CRUD (/inventario/productos)
Category CRUD (/inventario/categorias)
Register & edit cash drawers (/cajas)
View product and category lists
Open / close own caja session
Operate the Point of Sale (/ventas/pos)
Mobile top-up operations (/recargas)
WhatsApp agent operations (/whatsapp)
Role names are matched case-insensitively and with leading/trailing whitespace trimmed. The route guard normalizes both the user’s role and the allowedRoles array to lowercase before comparing, so "Administrador", "administrador", and "ADMINISTRADOR" are all treated as the same role.

TypeScript Interfaces

The core data shapes for users and roles are defined across two files.
// src/modules/Administracion/Usuarios/interfaces/Usuario.ts

export interface Role {
  id: number;
  nombre: "admin" | "user" | string;
  descripcion?: string;
}

export interface Persona extends BaseEntityAudit {
  id: number;
  nombres: string;
  p_apellido: string;
  s_apellido?: string;
  fecha_nacimiento: string;
  genero: string;
}

export interface Usuario extends BaseEntityAudit {
  id: number;
  name: string;
  email: string;
  persona: Persona;
  role: Role;
}

User Registration

Creating a new account is a two-step form that groups fields into account credentials and personal data. Required fields are email, password, nombres, and p_apellido; all others are optional.
1

Navigate to the registration form

From the user list at /administracion/usuarios, click Nuevo Usuario. The app navigates to /administracion/usuarios/registrar.
2

Fill in account credentials

FieldTypeRequiredNotes
emailstringMust be a valid e-mail address
passwordstringPlain text — hashed server-side
namestringUsername / display handle (e.g. jdoe99)
id_rolenumberID of the role fetched from GET /auth/roles/list
3

Fill in personal data

FieldTypeRequiredNotes
nombresstringGiven names
p_apellidostringFirst (paternal) surname
s_apellidostringSecond (maternal) surname
fecha_nacimientostringISO date — YYYY-MM-DD
generostring"M" · "F" · "O"
4

Submit

Click Crear Usuario. On HTTP 200/201 the app shows a success alert and redirects back to the user list after 2 seconds. On error, an error alert is displayed for 5 seconds.
The registration form calls UsuariosService.registerUsuario(data), which posts to POST /usuario/register with a Bearer token attached:
// src/modules/Administracion/Usuarios/services/usuariosService.ts

const getHeaders = () => ({
  "Content-Type": "application/json",
  Authorization: `Bearer ${localStorage.getItem("access_token")}`,
});

export const UsuariosService = {
  async registerUsuario(data: RegisterUsuarioRequest) {
    return await axios.post(`${API_BASE_URL}/usuario/register`, data, {
      headers: getHeaders(),
    });
  },
  async updateUsuario(id: number, data: UpdateUsuarioRequest) {
    return await axios.patch(`${API_BASE_URL}/usuario/${id}`, data, {
      headers: getHeaders(),
    });
  },
  async updatePersona(data: UpdatePersonaRequest) {
    return await axios.put(`${API_BASE_URL}/persona`, data, {
      headers: getHeaders(),
    });
  },
  // ...
};

Route Map

RouteComponentAccess
/administracion/usuariosUsuariosMainAdministrador
/administracion/usuarios/registrarUsuariosRegisterAdministrador
/administracion/usuarios/editar/:idUserProfilesAdministrador

Route Guard: ProtectedRoute

Every sensitive sub-tree is wrapped in a <ProtectedRoute allowedRoles={[...]} /> outlet. The component:
  1. Redirects unauthenticated visitors to /signin.
  2. Extracts role.nombre (or role.name as a fallback) from the stored user object.
  3. Normalizes both sides to lowercase and checks inclusion against allowedRoles.
  4. Redirects unauthorized users to / without rendering the target view.
// src/router/ProtectedRoute.tsx

export default function ProtectedRoute({ allowedRoles }: { allowedRoles?: string[] }) {
  const { isAuthenticated, user } = useAuth();

  if (!isAuthenticated) {
    return <Navigate to="/signin" replace />;
  }

  if (allowedRoles && user) {
    const userRaw = user as any;
    const roleObject = userRaw.role || userRaw.Role;
    let roleName = "";

    if (roleObject) {
      roleName =
        typeof roleObject === "object"
          ? roleObject.nombre || roleObject.name || ""
          : roleObject;
    }

    if (!roleName && userRaw.roleName) roleName = userRaw.roleName;

    const normalizedRole = roleName.toLowerCase().trim();
    const normalizedAllowed = allowedRoles.map((r) => r.toLowerCase().trim());

    if (!normalizedAllowed.includes(normalizedRole)) {
      return <Navigate to="/" replace />;
    }
  }

  return <Outlet />;
}
Usage in the router for the /administracion subtree:
<Route path="/administracion">
  <Route path="usuarios" element={<ProtectedRoute allowedRoles={['Administrador']} />}>
    <Route index element={<UsuariosMain />} />
    <Route path="registrar" element={<UsuariosRegister />} />
    <Route path="editar/:id" element={<UserProfiles />} />
  </Route>
</Route>

JWT Token Storage

After a successful login, AuthContext.loginStorage() persists three keys to localStorage:
KeyValue
access_tokenShort-lived JWT used in Authorization: Bearer headers
refresh_tokenLong-lived token for silent re-authentication
userFull User object serialized as a JSON string
On page load, the context re-hydrates from localStorage and immediately validates the access_token expiry claim (payload.exp). If the token has already expired, all three keys are removed and the user is treated as unauthenticated.
// src/context/auth/AuthContext.tsx (excerpt)

const loginStorage = (newToken: string, refreshToken: string, newUser: User) => {
  setToken(newToken);
  setUser(newUser);
  localStorage.setItem("access_token", newToken);
  localStorage.setItem("refresh_token", refreshToken);
  localStorage.setItem("user", JSON.stringify(newUser));
};

Inactivity Auto-Logout

Sessions expire automatically after 45 minutes of inactivity. The timer resets on any mousemove, keydown, scroll, or click event. If no interaction is detected for the full 45 minutes, logoutStorage() is called unconditionally — all tokens are cleared from localStorage and the browser is redirected to /signin. Any unsaved work in open forms (such as an in-progress sale or purchase entry) will be lost. Users should save drafts frequently or keep the interface active.
The timeout is implemented as a useRef-backed setTimeout in AuthContextProvider:
// src/context/auth/AuthContext.tsx (excerpt)

useEffect(() => {
  const resetTimer = () => {
    if (timeoutRef.current) clearTimeout(timeoutRef.current as any);
    timeoutRef.current = setTimeout(() => {
      logoutStorage();
    }, 45 * 60 * 1000); // 45 minutes
  };

  resetTimer();

  const events = ["mousemove", "keydown", "scroll", "click"];
  const handleActivity = () => {
    resetTimer();
  };

  events.forEach((evt) => window.addEventListener(evt, handleActivity));

  return () => {
    if (timeoutRef.current) clearTimeout(timeoutRef.current as any);
    events.forEach((evt) => window.removeEventListener(evt, handleActivity));
  };
}, []);

Editing an Existing User

To modify an existing account, click Ver/Editar Perfil on any row in the user list. The app navigates to /administracion/usuarios/editar/:id and passes the full Usuario object as location.state. Account updates are sent via PATCH /usuario/:id and personal data changes via PUT /persona.
The estado flag (active/inactive) on a Usuario is controlled through UpdateUsuarioRequest. Setting estado: false effectively disables the account — the user can no longer log in but their record and associated sales history are preserved.

Build docs developers (and LLMs) love