From 01c3d606943238095a740d4b068259d5dfc4c277 Mon Sep 17 00:00:00 2001 From: icamarillo Date: Tue, 3 Feb 2026 13:54:05 -0700 Subject: [PATCH] Release version 1.3.1: Updated backend models and endpoints --- backend/app/api/v1/endpoints/tickets.py | 136 ++++- .../app/api/v1/endpoints/tickets.py.backup | 451 ++++++++++++++++ backend/app/main.py | 3 +- backend/app/models/comment.py | 69 +++ backend/app/models/ticket.py | 6 + backend/app/models/user.py | 4 +- .../src/routes/tickets/+page.svelte | 481 ++++++++++++++++++ temp_response.txt | Bin 0 -> 4312 bytes 8 files changed, 1138 insertions(+), 12 deletions(-) create mode 100644 backend/app/api/v1/endpoints/tickets.py.backup create mode 100644 backend/app/models/comment.py create mode 100644 frontend-internal/src/routes/tickets/+page.svelte create mode 100644 temp_response.txt diff --git a/backend/app/api/v1/endpoints/tickets.py b/backend/app/api/v1/endpoints/tickets.py index e4a0fc3..b08aef8 100644 --- a/backend/app/api/v1/endpoints/tickets.py +++ b/backend/app/api/v1/endpoints/tickets.py @@ -14,6 +14,7 @@ from app.models.ticket import Ticket, TicketStatus, TicketPriority from app.models.user import User from app.models.category import Category # ✅ CORREGIDO: Era TicketCategory from app.models.system import System +from app.models.comment import TicketComment import uuid router = APIRouter() @@ -40,6 +41,7 @@ class TicketResponse(BaseModel): id: str ticket_number: str subject: str + title: str description: str status: str priority: str @@ -124,6 +126,7 @@ async def create_ticket( "id": str(db_ticket.id), "ticket_number": db_ticket.ticket_number, "subject": db_ticket.subject, + "title": db_ticket.subject, "description": db_ticket.description, "status": db_ticket.status.value, "priority": db_ticket.priority.value, @@ -186,6 +189,7 @@ async def get_tickets( "id": str(t.id), "ticket_number": t.ticket_number, "subject": t.subject, + "title": t.subject, "description": t.description, "status": t.status.value, "priority": t.priority.value, @@ -237,6 +241,7 @@ async def get_ticket( "id": str(ticket.id), "ticket_number": ticket.ticket_number, "subject": ticket.subject, + "title": ticket.subject, "description": ticket.description, "status": ticket.status.value, "priority": ticket.priority.value, @@ -305,6 +310,7 @@ async def update_ticket( "id": str(db_ticket.id), "ticket_number": db_ticket.ticket_number, "subject": db_ticket.subject, + "title": db_ticket.subject, "description": db_ticket.description, "status": db_ticket.status.value, "priority": db_ticket.priority.value, @@ -369,6 +375,7 @@ async def close_ticket( "id": str(db_ticket.id), "ticket_number": db_ticket.ticket_number, "subject": db_ticket.subject, + "title": db_ticket.subject, "description": db_ticket.description, "status": db_ticket.status.value, "priority": db_ticket.priority.value, @@ -388,11 +395,29 @@ async def close_ticket( ) -# =================================== -# COMMENT ENDPOINTS (placeholder) +## =================================== +# COMMENT ENDPOINTS # =================================== -@router.get("/{ticket_id}/comments") +class CommentCreate(BaseModel): + content: str + is_internal: bool = False + +class CommentResponse(BaseModel): + id: str + ticket_id: str + author_id: str + author_name: str + content: str + is_internal: bool + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +@router.get("/{ticket_id}/comments", response_model=List[CommentResponse]) async def get_ticket_comments( ticket_id: str, db: AsyncSession = Depends(get_db), @@ -401,23 +426,116 @@ async def get_ticket_comments( """ Obtener comentarios de un ticket """ - return [] + try: + ticket_uuid = uuid.UUID(ticket_id) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid ticket ID format" + ) + + # Verificar que el ticket existe y el usuario tiene acceso + ticket_query = select(Ticket).where( + Ticket.id == ticket_uuid, + Ticket.tenant_id == current_user.tenant_id + ) + ticket_result = await db.execute(ticket_query) + ticket = ticket_result.scalars().first() + + if not ticket: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Ticket {ticket_id} not found" + ) + + # Obtener comentarios + comments_query = select(TicketComment, User).join( + User, TicketComment.author_id == User.id + ).where( + TicketComment.ticket_id == ticket_uuid + ).order_by(TicketComment.created_at.asc()) + + result = await db.execute(comments_query) + comments_with_users = result.all() + + # Formatear respuesta + return [ + { + "id": str(comment.id), + "ticket_id": str(comment.ticket_id), + "author_id": str(comment.author_id), + "author_name": f"{user.first_name} {user.last_name}", + "content": comment.content, + "is_internal": comment.is_internal, + "created_at": comment.created_at, + "updated_at": comment.updated_at + } + for comment, user in comments_with_users + ] -@router.post("/{ticket_id}/comments", status_code=status.HTTP_201_CREATED) +@router.post("/{ticket_id}/comments", response_model=CommentResponse, status_code=status.HTTP_201_CREATED) async def create_comment( ticket_id: str, + comment: CommentCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Agregar un comentario a un ticket """ - raise HTTPException( - status_code=status.HTTP_501_NOT_IMPLEMENTED, - detail="Comments not yet implemented" + try: + ticket_uuid = uuid.UUID(ticket_id) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid ticket ID format" + ) + + # Verificar que el ticket existe y el usuario tiene acceso + ticket_query = select(Ticket).where( + Ticket.id == ticket_uuid, + Ticket.tenant_id == current_user.tenant_id ) - + ticket_result = await db.execute(ticket_query) + ticket_obj = ticket_result.scalars().first() + + if not ticket_obj: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Ticket {ticket_id} not found" + ) + + # Crear comentario + new_comment = TicketComment( + id=uuid.uuid4(), + ticket_id=ticket_uuid, + author_id=current_user.id, + content=comment.content, + is_internal=comment.is_internal, + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + + db.add(new_comment) + + # Actualizar el ticket updated_at + ticket_obj.updated_at = datetime.utcnow() + + await db.commit() + await db.refresh(new_comment) + + # Retornar con el nombre del autor + return { + "id": str(new_comment.id), + "ticket_id": str(new_comment.ticket_id), + "author_id": str(new_comment.author_id), + "author_name": f"{current_user.first_name} {current_user.last_name}", + "content": new_comment.content, + "is_internal": new_comment.is_internal, + "created_at": new_comment.created_at, + "updated_at": new_comment.updated_at + } # =================================== # ATTACHMENT ENDPOINTS (placeholder) diff --git a/backend/app/api/v1/endpoints/tickets.py.backup b/backend/app/api/v1/endpoints/tickets.py.backup new file mode 100644 index 0000000..e4a0fc3 --- /dev/null +++ b/backend/app/api/v1/endpoints/tickets.py.backup @@ -0,0 +1,451 @@ +""" +Tickets endpoints - ServiceManagerWeb +""" + +from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func +from typing import List, Optional +from datetime import datetime +from app.core.database import get_db +from app.api.deps import get_current_user +from app.models.ticket import Ticket, TicketStatus, TicketPriority +from app.models.user import User +from app.models.category import Category # ✅ CORREGIDO: Era TicketCategory +from app.models.system import System +import uuid + +router = APIRouter() + +# =================================== +# SCHEMAS +# =================================== + +class TicketCreate(BaseModel): + subject: str + description: str + category_id: Optional[str] = None + affected_system_id: Optional[str] = None # ✅ CORREGIDO: Era system_id + priority: str = "MEDIUM" + +class TicketUpdate(BaseModel): + subject: Optional[str] = None + description: Optional[str] = None + status: Optional[str] = None + priority: Optional[str] = None + assigned_to: Optional[str] = None + +class TicketResponse(BaseModel): + id: str + ticket_number: str + subject: str + description: str + status: str + priority: str + category_id: Optional[str] = None + affected_system_id: Optional[str] = None # ✅ CORREGIDO: Era system_id + created_by: str + assigned_to: Optional[str] = None + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + +class TicketCloseRequest(BaseModel): + resolution: Optional[str] = None + + +# =================================== +# TICKET ENDPOINTS +# =================================== + +@router.post("/", response_model=TicketResponse, status_code=status.HTTP_201_CREATED) +async def create_ticket( + ticket: TicketCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Crear un nuevo ticket + """ + try: + # Generar número de ticket único + result = await db.execute( + select(func.count(Ticket.id)).where(Ticket.tenant_id == current_user.tenant_id) + ) + count = result.scalar() or 0 + ticket_number = f"TK-{count + 1:06d}" + + # Convertir IDs de string a UUID si son proporcionados + category_uuid = uuid.UUID(ticket.category_id) if ticket.category_id else None + system_uuid = uuid.UUID(ticket.affected_system_id) if ticket.affected_system_id else None # ✅ CORREGIDO + + # ✅ CORREGIDO: Validar en la tabla correcta con el nombre correcto del modelo + if category_uuid: + category = await db.get(Category, category_uuid) # ✅ Category, no TicketCategory + if not category: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"La categoría con ID {ticket.category_id} no existe." + ) + + # Validar si el system_id existe en la tabla affected_systems + if system_uuid: + system = await db.get(System, system_uuid) + if not system: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"El sistema con ID {ticket.affected_system_id} no existe." + ) + + db_ticket = Ticket( + id=uuid.uuid4(), + tenant_id=current_user.tenant_id, + ticket_number=ticket_number, + subject=ticket.subject, + description=ticket.description, + category_id=category_uuid, + affected_system_id=system_uuid, # ✅ CORREGIDO: Nombre correcto del campo + priority=TicketPriority[ticket.priority.upper()], + created_by=current_user.id, + status=TicketStatus.NEW, + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + + db.add(db_ticket) + await db.commit() + await db.refresh(db_ticket) + + # ✅ CORREGIDO: Usar affected_system_id en respuesta + return { + "id": str(db_ticket.id), + "ticket_number": db_ticket.ticket_number, + "subject": db_ticket.subject, + "description": db_ticket.description, + "status": db_ticket.status.value, + "priority": db_ticket.priority.value, + "category_id": str(db_ticket.category_id) if db_ticket.category_id else None, + "affected_system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORREGIDO + "created_by": str(db_ticket.created_by), + "assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None, + "created_at": db_ticket.created_at, + "updated_at": db_ticket.updated_at + } + + except ValueError as e: + await db.rollback() + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid UUID format: {str(e)}" + ) + except Exception as e: + await db.rollback() + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Error creating ticket: {str(e)}" + ) + + +@router.get("/", response_model=List[TicketResponse]) +async def get_tickets( + skip: int = 0, + limit: int = 100, + status_filter: Optional[str] = None, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Obtener tickets del usuario actual + """ + query = select(Ticket).where( + Ticket.tenant_id == current_user.tenant_id, + Ticket.created_by == current_user.id + ) + + if status_filter: + try: + status_enum = TicketStatus[status_filter.upper()] + query = query.where(Ticket.status == status_enum) + except KeyError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid status: {status_filter}" + ) + + query = query.order_by(Ticket.created_at.desc()).offset(skip).limit(limit) + + result = await db.execute(query) + tickets = result.scalars().all() + + # ✅ CORREGIDO: Usar affected_system_id + return [ + { + "id": str(t.id), + "ticket_number": t.ticket_number, + "subject": t.subject, + "description": t.description, + "status": t.status.value, + "priority": t.priority.value, + "category_id": str(t.category_id) if t.category_id else None, + "affected_system_id": str(t.affected_system_id) if t.affected_system_id else None, # ✅ CORREGIDO + "created_by": str(t.created_by), + "assigned_to": str(t.assigned_to) if t.assigned_to else None, + "created_at": t.created_at, + "updated_at": t.updated_at + } + for t in tickets + ] + + +@router.get("/{ticket_id}", response_model=TicketResponse) +async def get_ticket( + ticket_id: str, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Obtener un ticket específico + """ + try: + ticket_uuid = uuid.UUID(ticket_id) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid ticket ID format" + ) + + query = select(Ticket).where( + Ticket.id == ticket_uuid, + Ticket.tenant_id == current_user.tenant_id, + Ticket.created_by == current_user.id + ) + + result = await db.execute(query) + ticket = result.scalars().first() + + if not ticket: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Ticket {ticket_id} not found" + ) + + # ✅ CORREGIDO: Usar affected_system_id + return { + "id": str(ticket.id), + "ticket_number": ticket.ticket_number, + "subject": ticket.subject, + "description": ticket.description, + "status": ticket.status.value, + "priority": ticket.priority.value, + "category_id": str(ticket.category_id) if ticket.category_id else None, + "affected_system_id": str(ticket.affected_system_id) if ticket.affected_system_id else None, # ✅ CORREGIDO + "created_by": str(ticket.created_by), + "assigned_to": str(ticket.assigned_to) if ticket.assigned_to else None, + "created_at": ticket.created_at, + "updated_at": ticket.updated_at + } + + +@router.patch("/{ticket_id}", response_model=TicketResponse) +async def update_ticket( + ticket_id: str, + ticket_update: TicketUpdate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Actualizar un ticket + """ + try: + ticket_uuid = uuid.UUID(ticket_id) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid ticket ID format" + ) + + query = select(Ticket).where( + Ticket.id == ticket_uuid, + Ticket.tenant_id == current_user.tenant_id, + Ticket.created_by == current_user.id + ) + + result = await db.execute(query) + db_ticket = result.scalars().first() + + if not db_ticket: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Ticket {ticket_id} not found" + ) + + try: + update_data = ticket_update.dict(exclude_unset=True) + + for field, value in update_data.items(): + if field == "status" and value: + setattr(db_ticket, field, TicketStatus[value.upper()]) + elif field == "priority" and value: + setattr(db_ticket, field, TicketPriority[value.upper()]) + elif field == "assigned_to" and value: + setattr(db_ticket, field, uuid.UUID(value)) + else: + setattr(db_ticket, field, value) + + db_ticket.updated_at = datetime.utcnow() + + await db.commit() + await db.refresh(db_ticket) + + # ✅ CORREGIDO: Usar affected_system_id + return { + "id": str(db_ticket.id), + "ticket_number": db_ticket.ticket_number, + "subject": db_ticket.subject, + "description": db_ticket.description, + "status": db_ticket.status.value, + "priority": db_ticket.priority.value, + "category_id": str(db_ticket.category_id) if db_ticket.category_id else None, + "affected_system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORREGIDO + "created_by": str(db_ticket.created_by), + "assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None, + "created_at": db_ticket.created_at, + "updated_at": db_ticket.updated_at + } + + except Exception as e: + await db.rollback() + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Error updating ticket: {str(e)}" + ) + + +@router.patch("/{ticket_id}/close", response_model=TicketResponse) +async def close_ticket( + ticket_id: str, + close_request: TicketCloseRequest, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Cerrar un ticket + """ + try: + ticket_uuid = uuid.UUID(ticket_id) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid ticket ID format" + ) + + query = select(Ticket).where( + Ticket.id == ticket_uuid, + Ticket.tenant_id == current_user.tenant_id, + Ticket.created_by == current_user.id + ) + + result = await db.execute(query) + db_ticket = result.scalars().first() + + if not db_ticket: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Ticket {ticket_id} not found" + ) + + try: + db_ticket.status = TicketStatus.CLOSED + db_ticket.updated_at = datetime.utcnow() + + await db.commit() + await db.refresh(db_ticket) + + # ✅ CORREGIDO: Usar affected_system_id + return { + "id": str(db_ticket.id), + "ticket_number": db_ticket.ticket_number, + "subject": db_ticket.subject, + "description": db_ticket.description, + "status": db_ticket.status.value, + "priority": db_ticket.priority.value, + "category_id": str(db_ticket.category_id) if db_ticket.category_id else None, + "affected_system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORREGIDO + "created_by": str(db_ticket.created_by), + "assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None, + "created_at": db_ticket.created_at, + "updated_at": db_ticket.updated_at + } + + except Exception as e: + await db.rollback() + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Error closing ticket: {str(e)}" + ) + + +# =================================== +# COMMENT ENDPOINTS (placeholder) +# =================================== + +@router.get("/{ticket_id}/comments") +async def get_ticket_comments( + ticket_id: str, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Obtener comentarios de un ticket + """ + return [] + + +@router.post("/{ticket_id}/comments", status_code=status.HTTP_201_CREATED) +async def create_comment( + ticket_id: str, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Agregar un comentario a un ticket + """ + raise HTTPException( + status_code=status.HTTP_501_NOT_IMPLEMENTED, + detail="Comments not yet implemented" + ) + + +# =================================== +# ATTACHMENT ENDPOINTS (placeholder) +# =================================== + +@router.get("/{ticket_id}/attachments") +async def get_ticket_attachments( + ticket_id: str, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Obtener adjuntos de un ticket + """ + return [] + + +@router.post("/{ticket_id}/attachments", status_code=status.HTTP_201_CREATED) +async def upload_attachment( + ticket_id: str, + file: UploadFile = File(...), + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Subir un archivo adjunto a un ticket + """ + raise HTTPException( + status_code=status.HTTP_501_NOT_IMPLEMENTED, + detail="File uploads not yet implemented" + ) \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index f00588d..8169f72 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -21,6 +21,7 @@ from app.models.system import System from app.models.category import Category from app.models.user import User from app.models.ticket import Ticket +from app.models.comment import TicketComment from app.core.logging import setup_logging from app.api.v1.router import api_router @@ -198,4 +199,4 @@ if __name__ == "__main__": host="0.0.0.0", port=8000, reload=settings.ENVIRONMENT == "development" - ) \ No newline at end of file + ) diff --git a/backend/app/models/comment.py b/backend/app/models/comment.py new file mode 100644 index 0000000..da49d3a --- /dev/null +++ b/backend/app/models/comment.py @@ -0,0 +1,69 @@ +""" +Comment Model - ServiceManagerWeb + +Modelo para comentarios en tickets +""" + +from sqlalchemy import Column, String, Text, Boolean, ForeignKey, DateTime +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column, relationship +from datetime import datetime +import uuid + +from app.core.database import Base + + +class TicketComment(Base): + """Comentarios en tickets.""" + + __tablename__ = "ticket_comments" + + # Columnas + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + primary_key=True, + default=uuid.uuid4 + ) + + ticket_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + ForeignKey("tickets.id", ondelete="CASCADE"), + nullable=False, + index=True + ) + + author_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + ForeignKey("users.id"), + nullable=False, + index=True + ) + + content: Mapped[str] = mapped_column(Text, nullable=False) + + is_internal: Mapped[bool] = mapped_column( + Boolean, + default=False, + nullable=False + ) + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=datetime.utcnow, + nullable=False, + index=True + ) + + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=datetime.utcnow, + onupdate=datetime.utcnow, + nullable=False + ) + + # Relationships + ticket: Mapped["Ticket"] = relationship("Ticket", back_populates="comments") + author: Mapped["User"] = relationship("User") + + def __repr__(self) -> str: + return f"" \ No newline at end of file diff --git a/backend/app/models/ticket.py b/backend/app/models/ticket.py index a9ba19d..2a4bbc0 100644 --- a/backend/app/models/ticket.py +++ b/backend/app/models/ticket.py @@ -119,6 +119,12 @@ class Ticket(Base): foreign_keys=[assigned_to], back_populates="assigned_tickets" ) + + comments: Mapped[list["TicketComment"]] = relationship( + "TicketComment", + back_populates="ticket", + cascade="all, delete-orphan" + ) # ✅ AÑADIDOS: Constraints según schema.sql __table_args__ = ( diff --git a/backend/app/models/user.py b/backend/app/models/user.py index c492222..3d97dd0 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -48,7 +48,7 @@ class User(Base): # Autenticación password_hash: Mapped[str] = mapped_column(String(255), nullable=False) - role: Mapped[UserRole] = mapped_column(ENUM(UserRole), nullable=False) + role: Mapped[UserRole] = mapped_column(ENUM(UserRole, name="user_role_enum"), nullable=False) # 2FA (opcional para staff interno) totp_secret: Mapped[Optional[str]] = mapped_column(String(32)) @@ -132,4 +132,4 @@ class User(Base): def requires_2fa(self) -> bool: """Check if 2FA is required for this user.""" # 2FA opcional para staff interno, no requerido para clientes - return self.is_staff \ No newline at end of file + return self.is_staff diff --git a/frontend-internal/src/routes/tickets/+page.svelte b/frontend-internal/src/routes/tickets/+page.svelte new file mode 100644 index 0000000..f9eb1fe --- /dev/null +++ b/frontend-internal/src/routes/tickets/+page.svelte @@ -0,0 +1,481 @@ + + +
+
+
+

Tickets de Soporte

+

Gestión de tickets del sistema de mesa de ayuda.

+
+
+ +
+
+ + +
+
+
+ + +
+ +
+ + +
+ +
+ +
+
+
+ + +
+
+
+
+ + + + + + + + + + + + + + + {#if isLoading} + + {:else if tickets.length === 0} + + {:else} + {#each tickets as ticket} + + + + + + + + + + + {/each} + {/if} + +
TicketAsuntoEstadoPrioridadCategoríaAsignado aCreado + Acciones +
Cargando...
No hay tickets registrados
+ {ticket.ticket_number || ticket.id.substring(0, 8)} + +
{ticket.subject}
+
{ticket.description}
+
+ + {getStatusBadge(ticket.status).label} + + + + {getPriorityBadge(ticket.priority).label} + + + {getCategoryName(ticket.category_id)} + + {getUserName(ticket.assigned_to)} + + {formatDate(ticket.created_at)} + + + +
+
+
+
+
+
+ + + showModal = false}> +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+
+
+ + + showEditModal = false}> +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+

Asunto: {selectedTicket?.subject}

+

Creado por: {getUserName(selectedTicket?.created_by)}

+
+ +
+ + +
+
+
\ No newline at end of file diff --git a/temp_response.txt b/temp_response.txt new file mode 100644 index 0000000000000000000000000000000000000000..8360213bb7f4579e2899968a173e175d22541ceb GIT binary patch literal 4312 zcmeH~ZEw>^5Xa~BN%tM1zP?oPQ{wa;DpjctdKKh!(jiniw$t{m(2|Er2=Uc{-+$K0 z1!`K-A|%Ag@_IeHJ3Biw|C!kj@1NUS+p;sOXm@yfR$F94OKd_fu`d0A<(AS*t#6sr z#x|xOK`WqY&4jk(uVN%}Zok284(HPTWqfYO_R1FalJVN6&2A(69Ui^alR?+I88gTB z4>L>Ico+MfN@>~`tPKX zw8NX&He)gW^1Y$`-R!-!P-9@K;*R1?9LYLBS&Pb2HE*kD3T_0-Z zI+ng=sc`n66*CE1Sz=~@PJ6D`{GOO;xEcFf3XLT%|58Dgt3c9A5qWbP6vJA~LS z%%lxn77Xah84Z@)6f?=nTd(Jd;OYPQwc_gSSPAIR+}oeP?K)C+?Xmq~&pZQ!Iqf+@ zvzA%pCbZiEY*ONbdmj}!8x z;9rWwA(tsSl&%H&qecg=frz;iG!gOkeI!_9^DNc(ES1y7XmlHneZGoTy?Y`~IZ<4d z`;@0MI4b|OBTlG_v1rArKCRGIxlBTwP$i8h;}78?Uy*0TE~mVv_IFufZHl<~2E?hh zTEJi4*-PN4d7$`h(F)rU1Bs>{BF`~y3r{yMU!AbX#7k0|i0 zncW@(zCLHFzK|^o+Ptyx7z@kmRm-3N@R#>(i8@g(_+u%S9dRb7z%YooxHuZ~!$ zN>DX>*rROrRqPQ>KBjt6y__+jP}NYucjErqj=S$)t8O99x2%fq7D*E9vx9X8UhfL; zLVkMhNHTz{s@H8e5BWx`_;kJAjhK_+8;TX-7AGq!R>-52se?y}sv1}kN3l=-SJRV) zqu|E;~m&8_q;u7ens$Tg`(@Zdj#ij#~V3j!_6`5bJOi^L=}7HQ6jWLvG5vr xtxLGQ+Wr5#UAT9CyStq#c416+JNaYCZ@S^hE0x_ex@p$jgzGzdVZYih_7UZefB66a literal 0 HcmV?d00001