diff --git a/app/src/components/DeviceListModalRepair.tsx b/app/src/components/DeviceListModalRepair.tsx new file mode 100644 index 00000000..cd9193f9 --- /dev/null +++ b/app/src/components/DeviceListModalRepair.tsx @@ -0,0 +1,158 @@ +/** + * Description: Modal แสดงรายการอุปกรณ์ย่อย (Device Childs) สำหรับ Ticket + * - แสดงตาราง: ลำดับ, รหัสอุปกรณ์, Serial Number, สถานะ + * - สถานะแสดงเป็น Badge แบบมีสี (ตั้งค่าเป็น กำลังซ่อม สำหรับงานแจ้งซ่อม) + * Input : DeviceListModalProps { isOpen, onClose, devices } + * Output : React Component (Modal) + * Author: Worrawat Namwat (Wave) 66160372 (Refactored for Repair) + */ +import { Icon } from "@iconify/react"; +import type { RepairTicketReportedDevice } from "../services/RepairService"; + +interface DeviceListModalProps { + isOpen: boolean; + onClose: () => void; + devices: RepairTicketReportedDevice[]; +} + +// Status display configuration (matching Figma) +const statusConfig: Record = { + READY: { + label: "พร้อมใช้งาน", + className: "border-[#73D13D] text-[#73D13D]", + }, + BORROWED: { + label: "กำลังใช้งาน", + className: "border-[#40A9FF] text-[#40A9FF]", + }, + REPAIRING: { + label: "กำลังซ่อม", + className: "border-[#FDBA74] text-[#C2410C]", + }, + DAMAGED: { + label: "ชำรุด", + className: "border-[#FCA5A5] text-[#B91C1C]", + }, +}; + +const DeviceListModalRepair = ({ + isOpen, + onClose, + devices, +}: DeviceListModalProps) => { + if (!isOpen) return null; + + /** + * Description: ตรวจสอบว่ามีอุปกรณ์ใดมี Serial Number หรือไม่ (สำหรับแสดง/ซ่อน column) + * Input : devices (RepairTicketReportedDevice[]) + * Output : boolean + * Author : Pakkapon Chomchoey (Tonnam) 66160080 + */ + const hasSerialNumber = devices.some( + (device) => device.serial_number && device.serial_number.trim() !== "", + ); + + /** + * Description: ดึง style (label, className) ตามสถานะอุปกรณ์ + * Input : status (string) - สถานะ (READY, BORROWED, DAMAGED, etc.) + * Output : { label: string, className: string } + * Author : Pakkapon Chomchoey (Tonnam) 66160080 + */ + const getStatusStyle = (status: string) => { + return ( + statusConfig[status] || { + label: status, + className: "border-[#1890FF] text-[#1890FF]", + } + ); + }; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal - Responsive sizing */} +
+ {/* Header */} +
+

รายการอุปกรณ์

+ +
+ + {/* Table content area */} +
+
+ {/* Table Header - Rounded */} +
+
ลำดับ
+ {!hasSerialNumber &&
} +
รหัสอุปกรณ์
+ {!hasSerialNumber &&
} + {hasSerialNumber && ( +
Serial Number
+ )} +
+ สถานะ +
+
+ + {/* Table Body */} +
+ {devices.map((device, index) => { + // อุปกรณ์ที่อยู่ใน List แจ้งซ่อม จะถูกกำหนดให้เป็นสถานะ "กำลังซ่อม" (REPAIRING) เสมอ + const statusStyle = getStatusStyle("REPAIRING"); + + return ( +
+
+ {index + 1} +
+ + {!hasSerialNumber &&
} + +
+ {device.asset_code || "-"} +
+ + {!hasSerialNumber &&
} + + {hasSerialNumber && ( +
+ {device.serial_number || "-"} +
+ )} + +
+ + {statusStyle.label} + +
+
+ ); + })} + + {devices.length === 0 && ( +
+ ไม่พบรายการรหัสอุปกรณ์ย่อย +
+ )} +
+
+
+
+
+ ); +}; + +export default DeviceListModalRepair; \ No newline at end of file diff --git a/app/src/components/RequestItemRepair.tsx b/app/src/components/RequestItemRepair.tsx new file mode 100644 index 00000000..ce44aafc --- /dev/null +++ b/app/src/components/RequestItemRepair.tsx @@ -0,0 +1,372 @@ +/** + * Description: Component สำหรับแสดงรายการคำร้องแจ้งซ่อมแต่ละรายการ (List Item) + * รองรับการกดขยาย (Expand) เพื่อดูรายละเอียดเพิ่มเติม และฟังก์ชันการกดรับงาน (Approve) + * Input : RequestItemRepairProps (ข้อมูล ticket, ฟังก์ชัน onExpand, onApprove, ฯลฯ) + * Output : React Component + * Author: Worrawat Namwat (Wave) 66160372 + */ + +import { useState, useEffect } from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faChevronDown } from "@fortawesome/free-solid-svg-icons"; +import { Icon } from "@iconify/react"; +import { useToast } from "./Toast"; +import { AlertDialog } from "./AlertDialog"; +import getImageUrl from "../services/GetImage"; +import { + type RepairTicketStatus, + type RepairTicketItem, + type RepairTicketDetail, +} from "../services/RepairService"; +import DeviceListModalRepair from "./DeviceListModalRepair"; + +interface RequestItemRepairProps { + ticket: RepairTicketItem; + ticketDetail?: RepairTicketDetail | null; + isLoadingDetail?: boolean; + onExpand: (ticketId: number, isExpandUI: boolean) => void; + onApprove?: (ticketId: number) => Promise; + currentUserId?: number; + currentUserName?: string; + isForceExpand?: boolean; + expandTrigger?: unknown; +} + +// Config สำหรับตั้งค่าสีและข้อความของแต่ละ Status +const statusConfig: Record< + RepairTicketStatus, + { label: string; className: string } +> = { + PENDING: { + label: "รออนุมัติ", + className: "bg-white text-[#FBBF24] border border-[#FBBF24]", + }, + IN_PROGRESS: { + label: "กำลังซ่อม", + className: "bg-white text-[#40A9FF] border border-[#40A9FF]", + }, + COMPLETED: { + label: "เสร็จสิ้น", + className: "bg-green-100 text-green-800 border border-green-200", + }, +}; + +/** + * Description: ฟังก์ชันสำหรับจัดรูปแบบวันที่ให้อ่านง่าย (DD/MM/YYYY) + * Input : dateStr (ข้อมูลวันที่รูปแบบ String หรือ null) + * Output : วันที่ที่จัดรูปแบบแล้ว หรือ "-" ถ้าไม่มีข้อมูล + * Author : Worrawat Namwat (Wave) 66160372 + */ +const formatDate = (dateStr: string | null): string => { + if (!dateStr) return "-"; + const date = new Date(dateStr); + return date.toLocaleDateString("th-TH", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); +}; + +/** + * Description: ฟังก์ชันสำหรับจัดรูปแบบเวลา (HH:MM) + * Input : dateStr (ข้อมูลเวลาในรูปแบบ String หรือ null) + * Output : เวลาที่จัดรูปแบบแล้ว หรือ "-" ถ้าไม่มีข้อมูล + * Author : Worrawat Namwat (Wave) 66160372 + */ +const formatTime = (dateStr: string | null): string => { + if (!dateStr) return "-"; + const date = new Date(dateStr); + return date.toLocaleTimeString("th-TH", { + hour: "2-digit", + minute: "2-digit", + }); +}; + +export default function RequestItemRepair({ + ticket, + ticketDetail, + isLoadingDetail, + onExpand, + onApprove, + isForceExpand, + currentUserId, + currentUserName, +}: RequestItemRepairProps) { + const [isExpanded, setIsExpanded] = useState(isForceExpand || false); + const [isDeviceModalOpen, setIsDeviceModalOpen] = useState(false); + const [isAlertOpen, setIsAlertOpen] = useState(false); + const { push } = useToast(); + + const [localApprover, setLocalApprover] = useState( + ticket.approver?.fullname || null + ); + const [localStatus, setLocalStatus] = useState(ticket.status); + + useEffect(() => { + setLocalStatus(ticket.status); + // ถ้าไม่ส่งมา (เป็น null) จะไม่เอา null ไปทับชื่อที่เพิ่งเซ็ตไว้ตอนกดรับงาน + if (ticket.approver?.fullname) { + setLocalApprover(ticket.approver.fullname); + } + }, [ticket.status, ticket.approver]); + + + /** + * Description: ฟังก์ชันสำหรับจัดการการกดขยาย (Expand) โดยจะสลับสถานะ isExpanded และเรียก onExpand เพื่อแจ้งพ่อแม่ให้โหลดข้อมูลรายละเอียดถ้ายังไม่มี + * Input : - ticketId (ID ของ ticket ที่จะขยาย) + * - isExpandUI (สถานะใหม่ของการขยาย ว่าขยายหรือไม่) + * Output : เปลี่ยนสถานะภายในและแจ้งพ่อแม่ผ่าน onExpand + * Author : Worrawat Namwat (Wave) 66160372 + */ + const handleExpandClick = () => { + const newExpandState = !isExpanded; + setIsExpanded(newExpandState); + onExpand(ticket.id, newExpandState); + }; + +/** + * Description: ฟังก์ชันสำหรับจัดการการกดปุ่มอนุมัติ โดยจะเปิด AlertDialog เพื่อยืนยันการรับงาน และถ้าผู้ใช้ยืนยัน จะเรียก onApprove เพื่ออัปเดตสถานะในระบบ และอัปเดตสถานะภายในของ component พร้อมแสดง Toast แจ้งผลลัพธ์ + * Input : - e (เหตุการณ์การคลิกปุ่มอนุมัติ) + * Output : เปิด AlertDialog และถ้าผู้ใช้ยืนยัน จะอัปเดตสถานะและแสดง Toast + * Author : Worrawat Namwat (Wave) 66160372 + */ + const handleApproveClick = (e: React.MouseEvent) => { + e.stopPropagation(); + setIsAlertOpen(true); + }; + + /** + * Description: ฟังก์ชันสำหรับจัดการการยืนยันรับงานจาก AlertDialog โดยจะเรียก onApprove เพื่ออัปเดตสถานะในระบบ และอัปเดตสถานะภายในของ component พร้อมแสดง Toast แจ้งผลลัพธ์ + * Input : - e (เหตุการณ์การคลิกปุ่มยืนยันใน AlertDialog) + * Output : ปิด AlertDialog และถ้าการอัปเดตสถานะสำเร็จ จะอัปเดตสถานะภายในและแสดง Toast แจ้งความสำเร็จ ถ้าล้มเหลว จะจับข้อผิดพลาดและแสดง Toast แจ้งความล้มเหลว + * Author : Worrawat Namwat (Wave) 66160372 + */ + const handleConfirmApprove = async (e?: React.MouseEvent) => { + if (e) { + e.preventDefault(); // ป้องกันไม่ให้ปุ่มมันเผลอรีเฟรชหน้าเว็บ + e.stopPropagation(); + } + + setIsAlertOpen(false); + + if (onApprove) { + try { + await onApprove(ticket.id); + + setLocalApprover(currentUserName || "ผู้รับเรื่อง"); + setLocalStatus("IN_PROGRESS"); + + push({ + message: "รับคำร้องสำเร็จ!", + tone: "success", + duration: 3000, + }); + + } catch (error) { + console.error("เซฟลง Database ไม่สำเร็จ:", error); + push({ + message: "เกิดข้อผิดพลาดในการรับงาน", + tone: "danger", + duration: 3000, + }); + } + } + }; + + const currentStatus = statusConfig[localStatus]; + const startDate = ticket.dates.created; + const deviceImage = ticket.device_info.image; + + return ( +
+
+ {/* Device Name & Asset Code */} +
+ + {ticket.device_info.name} + + + รหัส : {ticket.device_info.asset_code || "-"} + +
+ + {/* Quantity */} +
{ticket.device_info.quantity} ชิ้น
+ + {/* Category */} +
+ {ticket.device_info.category || "-"} +
+ + {/* Requester & Emp Code */} +
+ + {ticket.requester.fullname} + + + {ticket.requester.emp_code || "-"} + +
+ + {/* Date & Time */} +
+ {formatDate(startDate ?? null)} + เวลา : {formatTime(startDate ?? null)} +
+ + {/* Status */} +
+ + {currentStatus.label} + +
+ + {/* ช่องจัดการ (ปุ่มอนุมัติ หรือ ผู้ที่อนุมัติ) */} +
+ {localApprover ? ( +
+ + + + {localApprover} + +
+ ) : localStatus === "PENDING" ? ( + + ) : ( + - + )} +
+ + {/* Expand Icon */} +
+ +
+
+ + {/* Expanded Details */} +
+ {isLoadingDetail ? ( +
+ + กำลังโหลดข้อมูล... +
+ ) : ( + <> +
ข้อมูลการแจ้งซ่อมอุปกรณ์
+
+ {/* Image Column */} +
+
+ {deviceImage ? ( + {ticket.device_info.name} + ) : ( + + )} +
+ +
+ + {/* Info Columns */} +
+
+
+ ผู้ส่งคำร้อง + {ticket.requester.fullname} ({ticket.requester.emp_code || "-"}) +
+
+ ชื่ออุปกรณ์ + {ticket.device_info.name} +
+
+ หมวดหมู่ + {ticket.device_info.category || "-"} +
+
+ แผนก/ฝ่ายย่อย + {ticket.requester.department || "-"} / {ticket.requester.section || "-"} +
+
+ รหัสอุปกรณ์ที่แจ้งซ่อม +
+ {ticket.device_info.reported_devices && ticket.device_info.reported_devices.length > 0 ? ( + <> + {ticket.device_info.reported_devices.slice(0, 8).map((device, index) => ( + + {device.asset_code || "-"} + + ))} + {ticket.device_info.reported_devices.length > 8 && ( + + )} + + ) : ( + + {ticket.device_info.asset_code || "-"} + + )} +
+
+
+ จำนวน + {ticket.device_info.quantity} ชิ้น +
+
+ +
+
+ สถานที่รับอุปกรณ์ + {ticket.device_info.location || "-"} +
+
+ หัวข้อแจ้งซ่อม + {ticket.problem.title || "-"} +
+
+ รายละเอียดแจ้งซ่อม + {ticket.problem.description || "-"} +
+
+
+
+ + )} +
+ + {isDeviceModalOpen && ( + setIsDeviceModalOpen(false)} devices={ticketDetail?.devices || []} /> + )} + +
+ ); +} \ No newline at end of file diff --git a/app/src/pages/App.tsx b/app/src/pages/App.tsx index 185e5b18..fa685cc3 100644 --- a/app/src/pages/App.tsx +++ b/app/src/pages/App.tsx @@ -31,6 +31,7 @@ import Profile from "./Profile"; import NotFound from "./NotFound"; import { Settings } from "./Setting"; import History from "./History"; +import RequestsIssues from "./RequestRepair"; function App() { const ADMIN_ONLY: Role[] = ["ADMIN"]; @@ -94,7 +95,10 @@ function App() { ); const technicalRoutes = ( - <>{/*} />*/} + <> + {/*} />*/} + } /> + ); // route หน้า dashboard @@ -173,6 +177,9 @@ function App() { path="/request-borrow-ticket/:id?" element={} /> + + } /> + } diff --git a/app/src/pages/RequestRepair.tsx b/app/src/pages/RequestRepair.tsx new file mode 100644 index 00000000..f5b7556f --- /dev/null +++ b/app/src/pages/RequestRepair.tsx @@ -0,0 +1,417 @@ +import { useState, useEffect, useCallback } from "react"; +import SearchFilter from "../components/SearchFilter"; +import Dropdown from "../components/DropDown"; +import Pagination from "../components/Pagination"; +import { Icon } from "@iconify/react"; +import { AlertDialog } from "../components/AlertDialog"; +import { useNavigate } from "react-router-dom"; +import RequestItemRepair from "../components/RequestItemRepair"; +import { + repairTicketsService, + type RepairTicketStatus, +} from "../services/RepairService"; +import type { + RepairTicketItem, + GetRepairTicketsQuery +} from "../services/RepairService"; +import { useUserStore } from "../stores/userStore"; + +type SortField = + | "device_name" + | "quantity" + | "category" + | "requester" + | "request_date" + | "status"; + +type SortDirection = "asc" | "desc"; + +const statusOptions = [ + { id: "all", label: "ทั้งหมด", value: "ALL" }, + { id: "pending", label: "รอดำเนินการ", value: "PENDING" }, + { + id: "in_progress", + label: "กำลังดำเนินการ", + value: "IN_PROGRESS", + }, + { id: "completed", label: "เสร็จสิ้น", value: "COMPLETED" }, +]; + +/** + * Description: หน้าจัดการคำร้องแจ้งซ่อม รองรับการค้นหา กรองสถานะ แบ่งหน้า และอนุมัติรับงาน + * Input : - + * Output : React Component + * Author : Worrawat Namwat (Wave) 66160372 + */ +const RequestsRepair = () => { + const navigate = useNavigate(); + + const dataUser = localStorage.getItem("User") || sessionStorage.getItem("User"); + const user = dataUser ? JSON.parse(dataUser) : null; + const fetchUserFromServer = useUserStore((state) => state.fetchUserFromServer); + + useEffect(() => { + if (!user) { + fetchUserFromServer(); + } +}, [user, fetchUserFromServer]); + + const loggedInUserName = user?.us_firstname + ? `${user.us_firstname} ${user?.us_lastname || ""}`.trim() + : "ผู้รับเรื่อง"; + + /** + * Description: ฟังก์ชันสำหรับอนุมัติรับงานแจ้งซ่อม พร้อมตรวจสอบสิทธิ์ผู้ใช้ (Session) + * Input : ticketId (รหัสคำร้องแจ้งซ่อม) + * Output : Promise + * Author : Worrawat Namwat (Wave) 66160372 + */ + const handleApproveAction = async (ticketId: number): Promise => { + try { + if (!user?.us_id) { + throw new Error("User not found"); + } + + await repairTicketsService.approveTicket(ticketId, user.us_id); + + await fetchTickets(); + } catch (error) { + console.error(error); + } +}; + // --- States --- + const [activeTabKey, setActiveTabKey] = useState("all"); + const [searchFilter, setSearchFilter] = useState({ search: "" }); + const [statusFilter, setStatusFilter] = useState(statusOptions[0]); + + // Data State + const [tickets, setTickets] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Accordion State สำหรับเปิดดูทีละ 1 รายการ + const [expandedId, setExpandedId] = useState(null); + + // Sorting + const [sortField, setSortField] = useState("request_date"); + const [sortDirection, setSortDirection] = useState("desc"); + + // Pagination + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + + // Confirm Dialog State + const [confirmOpen, setConfirmOpen] = useState(false); + const [confirmData, setConfirmData] = useState<{ + title: string; + description: string; + onConfirm: () => Promise; + tone: "success" | "warning" | "danger"; + }>({ + title: "", + description: "", + onConfirm: async () => {}, + tone: "success", + }); + + /** + * Description: ฟังก์ชันสำหรับดึงข้อมูลคำร้องแจ้งซ่อมจาก API ตามเงื่อนไข (Pagination, Filter) + * Input : - (ใช้ข้อมูลจาก State ภายใน Component) + * Output : Promise + * Author : Worrawat Namwat (Wave) 66160372 + */ + const fetchTickets = useCallback(async () => { + setLoading(true); + setError(null); + try { + const queryParams: GetRepairTicketsQuery = { + page: page, + limit: 10, + }; + + if (searchFilter.search) { + queryParams.search = searchFilter.search; + } + + if (statusFilter.value !== "ALL") { + queryParams.status = statusFilter.value; + } + + const response = await repairTicketsService.getRepairTickets(queryParams); + + // response.data จาก Axios มักจะมีรูปแบบซ้อนกัน ขึ้นอยู่กับ Interface ของ API Service + setTickets(response.data.data || response.data); + setTotalPages(response.data.pagination?.totalPages || 1); + } catch (err: unknown) { + const errorResponse = err as { response?: { data?: { message?: string } } }; + setError(errorResponse?.response?.data?.message || "เกิดข้อผิดพลาดในการดึงข้อมูล"); + } finally { + setLoading(false); + } + }, [page, searchFilter.search, statusFilter.value, activeTabKey]); + + useEffect(() => { + setPage(1); + }, [searchFilter.search, statusFilter.value, activeTabKey]); + + useEffect(() => { + fetchTickets(); + }, [fetchTickets]); + + /** + * Description: ฟังก์ชันสำหรับจัดการการเรียงลำดับคอลัมน์ (Sorting) + * Input : field (ชื่อฟิลด์ที่ต้องการจัดเรียง) + * Output : - + * Author : Worrawat Namwat (Wave) 66160372 + */ + const handleSort = (field: SortField) => { + if (sortField === field) { + setSortDirection((prev) => (prev === "asc" ? "desc" : "asc")); + } else { + setSortField(field); + setSortDirection("desc"); + } + }; + + /** + * Description: ฟังก์ชันดึงไอคอนสำหรับการเรียงลำดับ (Sort Icon) + * Input : field (ชื่อฟิลด์ปัจจุบันของคอลัมน์) + * Output : string (ชื่อไอคอน) + * Author : Worrawat Namwat (Wave) 66160372 + */ + const getSortIcon = (field: SortField) => { + if (sortField === field) { + return sortDirection === "asc" ? "bx:sort-down" : "bx:sort-up"; + } + return "bx:sort-down"; + }; + + /** + * Description: ฟังก์ชันจัดการการขยายดูรายละเอียด Ticket (Accordion) + * Input : ticketId (รหัสคำร้อง), isExpanded (สถานะการกาง) + * Output : - + * Author : Worrawat Namwat (Wave) 66160372 + */ + const handleExpand = (ticketId: number, isExpanded: boolean) => { + setExpandedId(isExpanded ? ticketId : null); + }; + + return ( +
+
+
+ คำร้อง +
+ + {/* Page Title */} +
+

+ จัดการคำร้องแจ้งซ่อม +

+
+ +
+ setActiveTabKey("all")} + > + ทั้งหมด + + + setActiveTabKey("mine")} + > + ของฉัน + +
+ + {/* Filters */} +
+
+ +
+ +
+
+
+ + {/* Table Header */} +
+
+ อุปกรณ์ + +
+
+ จำนวน + +
+
+ หมวดหมู่ + +
+
+ ชื่อผู้ร้องขอ + +
+
+ วันที่ร้องขอ + +
+
+ สถานะ + +
+
จัดการ
+
+
+ +
+ {loading && ( +
+ + กำลังโหลดข้อมูล... +
+ )} + + {error && !loading && ( +
+ {error} + +
+ )} + + {!loading && !error && tickets.length === 0 && ( +
+ ไม่พบข้อมูลคำร้องแจ้งซ่อม +
+ )} + + {!loading && !error && tickets.length > 0 && ( +
+ {tickets.map((ticket) => ( + + ))} +
+ )} + + {!loading && !error && tickets.length > 0 && ( +
+ +
+ )} +
+
+ + +
+ ); +}; + +/** + * Description: Component ปุ่ม Tab สำหรับสลับมุมมอง (เช่น ทั้งหมด / ของฉัน) + * Input : { isActive: boolean, onClick: function, children: ReactNode } + * Output : React Component (ปุ่ม Tab) + * Author : Worrawat Namwat (Wave) 66160372 + */ +function TabButton({ + isActive, + onClick, + children, +}: { + isActive: boolean; + onClick: () => void; + children: React.ReactNode; +}) { + return ( + + ); +} + +export default RequestsRepair; \ No newline at end of file diff --git a/app/src/pages/Requests.tsx b/app/src/pages/Requests.tsx index aa9d4be7..8aaf9e28 100644 --- a/app/src/pages/Requests.tsx +++ b/app/src/pages/Requests.tsx @@ -489,6 +489,7 @@ const Requests = () => { setRejectLoading(false); } }; + return (
diff --git a/app/src/services/RepairService.ts b/app/src/services/RepairService.ts new file mode 100644 index 00000000..6797a667 --- /dev/null +++ b/app/src/services/RepairService.ts @@ -0,0 +1,119 @@ +import api from "../api/axios"; + +export type RepairTicketStatus = + | "PENDING" + | "IN_PROGRESS" + | "COMPLETED"; + +export interface RepairTicketReportedDevice { + asset_code: string | null; + serial_number: string | null; +} + +export interface RepairTicketDeviceInfo { + name: string; + asset_code: string | null; + category: string | null; + quantity: number; + location: string | null; + image: string | null; + reported_devices: RepairTicketReportedDevice[]; +} + +export interface RepairTicketProblem { + title: string; + description: string; +} + +export interface RepairTicketRequester { + user_id: number; + emp_code: string | null; + fullname: string; + department: string | null; + section: string | null; +} + +export interface RepairTicketItem { + id: number; + ticket_no: string; + status: RepairTicketStatus; + dates: { + created: string; + updated: string | null; + }; + device_info: RepairTicketDeviceInfo; + problem: RepairTicketProblem; + requester: RepairTicketRequester; + approver: { fullname: string } | null; +} + +export interface RepairTicketDeviceDetail { + id?: number; + name: string; + asset_code?: string; + serial_number?: string; +} + +export interface RepairTicketDetail { + devices: RepairTicketDeviceDetail[]; +} + +export interface RepairTicketsPagination { + page: number; + limit: number; + totalItems: number; + totalPages: number; +} + +export interface GetRepairTicketsResponse { + success: boolean; + data: { + data: RepairTicketItem[]; + pagination: RepairTicketsPagination; + }; +} + +export interface GetRepairTicketsQuery { + page?: number; + limit?: number; + search?: string; + status?: RepairTicketStatus; + start_date?: string; + end_date?: string; +} +export interface ApproveRepairTicketResponse { + message: string; +} + +export const repairTicketsService = { + /** + * Description: ดึงรายการคำร้องแจ้งซ่อมทั้งหมด พร้อมระบบค้นหา กรองสถานะ และแบ่งหน้า + * Endpoint : GET /repair-tickets + * Input : params (GetRepairTicketsQuery) + * Output : Promise + * Author : Worrawat Namwat (Wave) 66160372 + */ +getRepairTickets: async ( + params?: GetRepairTicketsQuery, +): Promise => { + const response = await api.get("/repair-tickets", { params }); + return response.data; +}, + + /** + * Description: อนุมัติใบแจ้งซ่อม โดยเปลี่ยนสถานะเป็น IN_PROGRESS และบันทึกผู้รับเรื่อง + * Endpoint : PATCH /repair-tickets/:id/approve + * Input : ticketId (number), userId (number) + * Output : Promise + * Author : Worrawat Namwat (Wave) 66160372 + * */ + approveTicket: async ( + ticketId: number, + userId: number, + ): Promise => { + const response = await api.patch(`/repair-tickets/${ticketId}/approve`, { + user_id: userId, + }); + return response.data; + }, +}; diff --git a/package-lock.json b/package-lock.json index 27028aeb..0bd86657 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3732,6 +3732,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=10" } @@ -3749,6 +3750,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=10" } @@ -3766,6 +3768,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -3783,6 +3786,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -3800,6 +3804,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -3817,6 +3822,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -3834,6 +3840,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -3851,6 +3858,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -3868,6 +3876,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -3885,6 +3894,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } diff --git a/server/src/infrastructure/database/prisma/migrations/20260211025210_init/migration.sql b/server/src/infrastructure/database/prisma/migrations/20260211025210_init/migration.sql new file mode 100644 index 00000000..722317b6 --- /dev/null +++ b/server/src/infrastructure/database/prisma/migrations/20260211025210_init/migration.sql @@ -0,0 +1,784 @@ +-- CreateEnum +CREATE TYPE "US_ROLE" AS ENUM ('ADMIN', 'HOD', 'HOS', 'TECHNICAL', 'STAFF', 'EMPLOYEE'); + +-- CreateEnum +CREATE TYPE "DEVICE_CHILD_STATUS" AS ENUM ('READY', 'BORROWED', 'REPAIRING', 'DAMAGED', 'LOST'); + +-- CreateEnum +CREATE TYPE "BRT_STATUS" AS ENUM ('PENDING', 'APPROVED', 'IN_USE', 'OVERDUE', 'COMPLETED', 'REJECTED'); + +-- CreateEnum +CREATE TYPE "BRTS_STATUS" AS ENUM ('PENDING', 'APPROVED', 'REJECTED'); + +-- CreateEnum +CREATE TYPE "TI_STATUS" AS ENUM ('PENDING', 'IN_PROGRESS', 'COMPLETED'); + +-- CreateEnum +CREATE TYPE "TI_RESULT" AS ENUM ('SUCCESS', 'FAILED', 'IN_PROGRESS'); + +-- CreateEnum +CREATE TYPE "DA_STATUS" AS ENUM ('ACTIVE', 'COMPLETED'); + +-- CreateEnum +CREATE TYPE "BASE_EVENT" AS ENUM ('DEVICE_CREATED', 'APPROVAL_REQUESTED', 'NOTIFICATION_FULFILLED', 'NOTIFICATION_RESOLVED', 'TICKET_CREATED', 'TICKET_APPROVED', 'TICKET_REJECTED', 'TICKET_STAGE_PASSED', 'TICKET_RETURNED', 'TICKET_DUE_SOON', 'TICKET_OVERDUE', 'ISSUE_REPORTED', 'ISSUE_ASSIGNED', 'ISSUE_RESOLVED', 'ISSUE_MARK_DAMAGED'); + +-- CreateEnum +CREATE TYPE "NR_STATUS" AS ENUM ('UNREAD', 'READ', 'DISMISSED'); + +-- CreateEnum +CREATE TYPE "NR_EVENT" AS ENUM ('APPROVAL_REQUESTED', 'REQUEST_FULFILLED', 'REQUEST_RESOLVED', 'YOUR_TICKET_APPROVED', 'YOUR_TICKET_STAGE_APPROVED', 'YOUR_TICKET_REJECTED', 'YOUR_TICKET_IN_USE', 'YOUR_TICKET_RETURNED', 'DUE_SOON_REMINDER', 'OVERDUE_ALERT', 'ISSUE_NEW_FOR_TECH', 'ISSUE_ASSIGNED_TO_YOU', 'ISSUE_RESOLVED_FOR_REPORTER'); + +-- CreateEnum +CREATE TYPE "CM_ROLE" AS ENUM ('user', 'assistant', 'system', 'tool', 'admin'); + +-- CreateEnum +CREATE TYPE "CM_STATUS" AS ENUM ('ok', 'error', 'blocked'); + +-- CreateEnum +CREATE TYPE "LDC_ACTION" AS ENUM ('BORROWED', 'RETURNED', 'CHANGED', 'RESOLVED', 'MARK_DAMAGED'); + +-- CreateEnum +CREATE TYPE "LBR_ACTION" AS ENUM ('CREATED', 'UPDATED', 'APPROVED', 'REJECTED', 'RETURNED', 'MARK_DAMAGED', 'MARK_LOST'); + +-- CreateEnum +CREATE TYPE "LI_ACTION" AS ENUM ('REPORTED', 'ASSIGNED', 'UPDATED', 'RESOLVED', 'MARK_DAMAGED', 'CANCELLED'); + +-- CreateTable +CREATE TABLE "departments" ( + "dept_id" SERIAL NOT NULL, + "dept_name" VARCHAR(200) NOT NULL, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "departments_pkey" PRIMARY KEY ("dept_id") +); + +-- CreateTable +CREATE TABLE "sections" ( + "sec_id" SERIAL NOT NULL, + "sec_name" VARCHAR(50) NOT NULL, + "sec_dept_id" INTEGER NOT NULL, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "sections_pkey" PRIMARY KEY ("sec_id") +); + +-- CreateTable +CREATE TABLE "users" ( + "us_id" SERIAL NOT NULL, + "us_emp_code" VARCHAR(100), + "us_firstname" VARCHAR(150) NOT NULL, + "us_lastname" VARCHAR(150) NOT NULL, + "us_username" VARCHAR(150) NOT NULL, + "us_password" VARCHAR(255) NOT NULL, + "us_email" VARCHAR(150) NOT NULL, + "us_phone" VARCHAR(20) NOT NULL, + "us_role" "US_ROLE" NOT NULL, + "us_images" VARCHAR(200), + "us_dept_id" INTEGER, + "us_sec_id" INTEGER, + "us_is_active" BOOLEAN NOT NULL DEFAULT true, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "users_pkey" PRIMARY KEY ("us_id") +); + +-- CreateTable +CREATE TABLE "devices" ( + "de_id" SERIAL NOT NULL, + "de_serial_number" VARCHAR(100) NOT NULL, + "de_name" VARCHAR(200) NOT NULL, + "de_description" VARCHAR(250), + "de_location" VARCHAR(200) NOT NULL, + "de_max_borrow_days" INTEGER NOT NULL, + "de_images" VARCHAR(200), + "de_af_id" INTEGER NOT NULL, + "de_ca_id" INTEGER NOT NULL, + "de_us_id" INTEGER NOT NULL, + "de_sec_id" INTEGER NOT NULL, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "devices_pkey" PRIMARY KEY ("de_id") +); + +-- CreateTable +CREATE TABLE "device_childs" ( + "dec_id" SERIAL NOT NULL, + "dec_serial_number" VARCHAR(250), + "dec_asset_code" VARCHAR(200) NOT NULL, + "dec_has_serial_number" BOOLEAN NOT NULL DEFAULT false, + "dec_status" "DEVICE_CHILD_STATUS" NOT NULL, + "dec_de_id" INTEGER NOT NULL, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "device_childs_pkey" PRIMARY KEY ("dec_id") +); + +-- CreateTable +CREATE TABLE "carts" ( + "ct_id" SERIAL NOT NULL, + "ct_quantity" INTEGER NOT NULL DEFAULT 1, + "ct_us_id" INTEGER NOT NULL, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "carts_pkey" PRIMARY KEY ("ct_id") +); + +-- CreateTable +CREATE TABLE "cart_items" ( + "cti_id" SERIAL NOT NULL, + "cti_us_name" VARCHAR(255), + "cti_phone" VARCHAR(20), + "cti_note" VARCHAR(255), + "cti_usage_location" VARCHAR(255), + "cti_quantity" INTEGER NOT NULL DEFAULT 1, + "cti_start_date" TIMESTAMPTZ(6), + "cti_end_date" TIMESTAMPTZ(6), + "cti_ct_id" INTEGER NOT NULL, + "cti_de_id" INTEGER NOT NULL, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "cart_items_pkey" PRIMARY KEY ("cti_id") +); + +-- CreateTable +CREATE TABLE "cart_device_childs" ( + "cdc_id" SERIAL NOT NULL, + "cdc_cti_id" INTEGER NOT NULL, + "cdc_dec_id" INTEGER NOT NULL, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + "reserved_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "cart_device_childs_pkey" PRIMARY KEY ("cdc_id") +); + +-- CreateTable +CREATE TABLE "refresh_tokens" ( + "rt_id" SERIAL NOT NULL, + "rt_us_id" INTEGER NOT NULL, + "rt_token_hash" VARCHAR(255) NOT NULL, + "rt_revoked_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "refresh_tokens_pkey" PRIMARY KEY ("rt_id") +); + +-- CreateTable +CREATE TABLE "accessories" ( + "acc_id" SERIAL NOT NULL, + "acc_name" VARCHAR(100) NOT NULL, + "acc_quantity" INTEGER NOT NULL, + "acc_de_id" INTEGER, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "accessories_pkey" PRIMARY KEY ("acc_id") +); + +-- CreateTable +CREATE TABLE "categories" ( + "ca_id" SERIAL NOT NULL, + "ca_name" VARCHAR(100) NOT NULL, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "categories_pkey" PRIMARY KEY ("ca_id") +); + +-- CreateTable +CREATE TABLE "approval_flows" ( + "af_id" SERIAL NOT NULL, + "af_name" VARCHAR(100) NOT NULL, + "af_is_active" BOOLEAN NOT NULL DEFAULT true, + "af_us_id" INTEGER NOT NULL, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "approval_flows_pkey" PRIMARY KEY ("af_id") +); + +-- CreateTable +CREATE TABLE "approval_flow_steps" ( + "afs_id" SERIAL NOT NULL, + "afs_step_approve" INTEGER NOT NULL, + "afs_dept_id" INTEGER, + "afs_sec_id" INTEGER, + "afs_role" "US_ROLE" NOT NULL, + "afs_af_id" INTEGER NOT NULL, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "approval_flow_steps_pkey" PRIMARY KEY ("afs_id") +); + +-- CreateTable +CREATE TABLE "device_availabilities" ( + "da_id" SERIAL NOT NULL, + "da_dec_id" INTEGER NOT NULL, + "da_brt_id" INTEGER NOT NULL, + "da_start" TIMESTAMPTZ(6) NOT NULL, + "da_end" TIMESTAMPTZ(6) NOT NULL, + "da_status" "DA_STATUS" NOT NULL DEFAULT 'ACTIVE', + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "device_availabilities_pkey" PRIMARY KEY ("da_id") +); + +-- CreateTable +CREATE TABLE "borrow_return_tickets" ( + "brt_id" SERIAL NOT NULL, + "brt_status" "BRT_STATUS" NOT NULL, + "brt_usage_location" VARCHAR(255) NOT NULL, + "brt_borrow_purpose" VARCHAR(255) NOT NULL, + "brt_start_date" TIMESTAMPTZ(6) NOT NULL, + "brt_end_date" TIMESTAMPTZ(6) NOT NULL, + "brt_quantity" INTEGER NOT NULL DEFAULT 1, + "brt_current_stage" INTEGER, + "brt_reject_reason" VARCHAR(255), + "brt_pickup_location" VARCHAR(255), + "brt_pickup_datetime" TIMESTAMPTZ(6), + "brt_return_location" VARCHAR(255), + "brt_return_datetime" TIMESTAMPTZ(6), + "brt_af_id" INTEGER, + "brt_user_id" INTEGER NOT NULL, + "brt_staff_id" INTEGER, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "borrow_return_tickets_pkey" PRIMARY KEY ("brt_id") +); + +-- CreateTable +CREATE TABLE "borrow_return_ticket_stages" ( + "brts_id" SERIAL NOT NULL, + "brts_status" "BRTS_STATUS" NOT NULL, + "brts_name" VARCHAR(191) NOT NULL, + "brts_step_approve" INTEGER NOT NULL, + "brts_role" "US_ROLE" NOT NULL, + "brts_dept_id" INTEGER, + "brts_sec_id" INTEGER, + "brts_dept_name" VARCHAR(200), + "brts_sec_name" VARCHAR(200), + "brts_brt_id" INTEGER NOT NULL, + "brts_us_id" INTEGER, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "borrow_return_ticket_stages_pkey" PRIMARY KEY ("brts_id") +); + +-- CreateTable +CREATE TABLE "ticket_devices" ( + "td_id" SERIAL NOT NULL, + "td_brt_id" INTEGER NOT NULL, + "td_dec_id" INTEGER NOT NULL, + "td_origin_cti_id" INTEGER, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "ticket_devices_pkey" PRIMARY KEY ("td_id") +); + +-- CreateTable +CREATE TABLE "ticket_issues" ( + "ti_id" SERIAL NOT NULL, + "ti_de_id" INTEGER NOT NULL, + "ti_brt_id" INTEGER, + "ti_title" VARCHAR(200) NOT NULL, + "ti_description" TEXT NOT NULL, + "ti_reported_by" INTEGER NOT NULL, + "ti_assigned_to" INTEGER, + "ti_status" "TI_STATUS" NOT NULL, + "ti_result" "TI_RESULT" NOT NULL, + "ti_damaged_reason" VARCHAR(255), + "ti_resolved_note" TEXT, + "receive_at" TIMESTAMPTZ(6), + "success_at" TIMESTAMPTZ(6), + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "ticket_issues_pkey" PRIMARY KEY ("ti_id") +); + +-- CreateTable +CREATE TABLE "issue_attachments" ( + "iatt_id" SERIAL NOT NULL, + "iatt_path_url" VARCHAR(255) NOT NULL, + "iatt_ti_id" INTEGER NOT NULL, + "uploaded_by" INTEGER NOT NULL, + "deleted_at" TIMESTAMPTZ(6), + "uploaded_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "issue_attachments_pkey" PRIMARY KEY ("iatt_id") +); + +-- CreateTable +CREATE TABLE "notifications" ( + "n_id" SERIAL NOT NULL, + "n_title" VARCHAR(200) NOT NULL, + "n_message" TEXT NOT NULL, + "n_data" JSONB, + "n_target_route" VARCHAR(255), + "n_base_event" "BASE_EVENT", + "n_brt_id" INTEGER, + "n_brts_id" INTEGER, + "n_ti_id" INTEGER, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "send_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "notifications_pkey" PRIMARY KEY ("n_id") +); + +-- CreateTable +CREATE TABLE "notification_recipients" ( + "nr_id" SERIAL NOT NULL, + "nr_status" "NR_STATUS" NOT NULL, + "nr_event" "NR_EVENT" NOT NULL, + "nr_n_id" INTEGER NOT NULL, + "nr_us_id" INTEGER NOT NULL, + "read_at" TIMESTAMPTZ(6), + "dismissed_at" TIMESTAMPTZ(6), + + CONSTRAINT "notification_recipients_pkey" PRIMARY KEY ("nr_id") +); + +-- CreateTable +CREATE TABLE "log_borrow_returns" ( + "lbr_id" SERIAL NOT NULL, + "lbr_action" "LBR_ACTION" NOT NULL, + "lbr_old_status" VARCHAR(50), + "lbr_new_status" VARCHAR(50), + "lbr_note" TEXT, + "lbr_actor_id" INTEGER, + "lbr_brt_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "log_borrow_returns_pkey" PRIMARY KEY ("lbr_id") +); + +-- CreateTable +CREATE TABLE "log_issues" ( + "li_id" SERIAL NOT NULL, + "li_action" "LI_ACTION" NOT NULL, + "li_old_status" VARCHAR(50), + "li_new_status" VARCHAR(50), + "li_note" TEXT, + "li_actor_id" INTEGER, + "li_ti_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "log_issues_pkey" PRIMARY KEY ("li_id") +); + +-- CreateTable +CREATE TABLE "log_device_childs" ( + "ldc_id" SERIAL NOT NULL, + "ldc_action" "LDC_ACTION" NOT NULL, + "ldc_old_status" VARCHAR(50), + "ldc_new_status" VARCHAR(50), + "ldc_note" TEXT, + "ldc_actor_id" INTEGER, + "ldc_ti_id" INTEGER, + "ldc_brt_id" INTEGER, + "ldc_dec_id" INTEGER, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "log_device_childs_pkey" PRIMARY KEY ("ldc_id") +); + +-- CreateTable +CREATE TABLE "chat_rooms" ( + "cr_id" SERIAL NOT NULL, + "cr_us_id" INTEGER NOT NULL, + "cr_title" VARCHAR(200), + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + "last_msg_at" TIMESTAMPTZ(6), + + CONSTRAINT "chat_rooms_pkey" PRIMARY KEY ("cr_id") +); + +-- CreateTable +CREATE TABLE "chat_messages" ( + "cm_id" SERIAL NOT NULL, + "cm_role" "CM_ROLE" NOT NULL, + "cm_content" TEXT NOT NULL, + "cm_content_json" JSONB, + "cm_status" "CM_STATUS" NOT NULL DEFAULT 'ok', + "cm_parent_id" INTEGER, + "cm_cr_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "chat_messages_pkey" PRIMARY KEY ("cm_id") +); + +-- CreateTable +CREATE TABLE "chat_attachments" ( + "catt_id" SERIAL NOT NULL, + "catt_cm_id" INTEGER NOT NULL, + "catt_file_path" VARCHAR(500) NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "chat_attachments_pkey" PRIMARY KEY ("catt_id") +); + +-- CreateTable +CREATE TABLE "issue_devices" ( + "id_id" SERIAL NOT NULL, + "id_ti_id" INTEGER NOT NULL, + "id_dec_id" INTEGER NOT NULL, + "deleted_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "issue_devices_pkey" PRIMARY KEY ("id_id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "departments_dept_name_key" ON "departments"("dept_name"); + +-- CreateIndex +CREATE UNIQUE INDEX "sections_sec_name_key" ON "sections"("sec_name"); + +-- CreateIndex +CREATE INDEX "idx_sections_dept" ON "sections"("sec_dept_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_us_emp_code_key" ON "users"("us_emp_code"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_us_username_key" ON "users"("us_username"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_us_email_key" ON "users"("us_email"); + +-- CreateIndex +CREATE INDEX "idx_users_dept_sec" ON "users"("us_dept_id", "us_sec_id"); + +-- CreateIndex +CREATE INDEX "idx_users_active" ON "users"("us_is_active"); + +-- CreateIndex +CREATE UNIQUE INDEX "devices_de_serial_number_key" ON "devices"("de_serial_number"); + +-- CreateIndex +CREATE INDEX "idx_devices_category" ON "devices"("de_ca_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "device_childs_dec_serial_number_key" ON "device_childs"("dec_serial_number"); + +-- CreateIndex +CREATE UNIQUE INDEX "device_childs_dec_asset_code_key" ON "device_childs"("dec_asset_code"); + +-- CreateIndex +CREATE INDEX "idx_child_device" ON "device_childs"("dec_de_id"); + +-- CreateIndex +CREATE INDEX "idx_child_status" ON "device_childs"("dec_status"); + +-- CreateIndex +CREATE INDEX "idx_cart_user_status" ON "carts"("ct_us_id"); + +-- CreateIndex +CREATE INDEX "idx_ct_item_cart" ON "cart_items"("cti_ct_id"); + +-- CreateIndex +CREATE INDEX "idx_ct_item_device" ON "cart_items"("cti_de_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "uq_ct_item_device" ON "cart_items"("cti_ct_id", "cti_de_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "refresh_tokens_rt_token_hash_key" ON "refresh_tokens"("rt_token_hash"); + +-- CreateIndex +CREATE INDEX "idx_rt_user_exp" ON "refresh_tokens"("rt_us_id"); + +-- CreateIndex +CREATE INDEX "idx_rt_revoked" ON "refresh_tokens"("rt_revoked_at"); + +-- CreateIndex +CREATE INDEX "accessories_acc_de_id_idx" ON "accessories"("acc_de_id"); + +-- CreateIndex +CREATE INDEX "idx_steps_flow_step" ON "approval_flow_steps"("afs_af_id", "afs_step_approve"); + +-- CreateIndex +CREATE INDEX "idx_avail_device_period" ON "device_availabilities"("da_dec_id", "da_start", "da_end"); + +-- CreateIndex +CREATE INDEX "idx_avail_status" ON "device_availabilities"("da_status"); + +-- CreateIndex +CREATE INDEX "idx_brt_user_status_created" ON "borrow_return_tickets"("brt_user_id", "brt_staff_id", "brt_status", "created_at"); + +-- CreateIndex +CREATE INDEX "idx_brt_flow" ON "borrow_return_tickets"("brt_af_id"); + +-- CreateIndex +CREATE INDEX "idx_brt_start_end" ON "borrow_return_tickets"("brt_start_date", "brt_end_date"); + +-- CreateIndex +CREATE INDEX "idx_brts_brt_step" ON "borrow_return_ticket_stages"("brts_id", "brts_step_approve"); + +-- CreateIndex +CREATE INDEX "idx_brts_status" ON "borrow_return_ticket_stages"("brts_status"); + +-- CreateIndex +CREATE INDEX "idx_brts_pending_lookup" ON "borrow_return_ticket_stages"("brts_status", "brts_role", "brts_dept_id", "brts_sec_id"); + +-- CreateIndex +CREATE INDEX "idx_ticket_device_brt" ON "ticket_devices"("td_brt_id"); + +-- CreateIndex +CREATE INDEX "idx_ticket_device_child" ON "ticket_devices"("td_dec_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "uq_ticket_device" ON "ticket_devices"("td_brt_id", "td_dec_id"); + +-- CreateIndex +CREATE INDEX "idx_issue_device_status" ON "ticket_issues"("ti_de_id", "ti_status", "created_at"); + +-- CreateIndex +CREATE INDEX "idx_issue_assignee" ON "ticket_issues"("ti_assigned_to", "ti_status"); + +-- CreateIndex +CREATE INDEX "idx_issue_attachments_issue" ON "issue_attachments"("iatt_ti_id"); + +-- CreateIndex +CREATE INDEX "idx_notif_created" ON "notifications"("created_at"); + +-- CreateIndex +CREATE INDEX "idx_notif_brt" ON "notifications"("n_brt_id"); + +-- CreateIndex +CREATE INDEX "idx_notif_brts" ON "notifications"("n_brts_id"); + +-- CreateIndex +CREATE INDEX "idx_notif_issue" ON "notifications"("n_ti_id"); + +-- CreateIndex +CREATE INDEX "idx_notifrec_user_status" ON "notification_recipients"("nr_us_id", "nr_status", "nr_n_id"); + +-- CreateIndex +CREATE INDEX "idx_log_brt" ON "log_borrow_returns"("lbr_brt_id", "created_at"); + +-- CreateIndex +CREATE INDEX "idx_log_issue" ON "log_issues"("li_ti_id", "created_at"); + +-- CreateIndex +CREATE INDEX "idx_ldc_log_issue" ON "log_device_childs"("ldc_ti_id", "created_at"); + +-- CreateIndex +CREATE INDEX "idx_log_device_history" ON "log_device_childs"("ldc_dec_id", "created_at"); + +-- CreateIndex +CREATE INDEX "chat_rooms_cr_us_id_last_msg_at_idx" ON "chat_rooms"("cr_us_id", "last_msg_at"); + +-- CreateIndex +CREATE INDEX "chat_rooms_last_msg_at_idx" ON "chat_rooms"("last_msg_at"); + +-- CreateIndex +CREATE INDEX "chat_messages_cm_cr_id_created_at_idx" ON "chat_messages"("cm_cr_id", "created_at"); + +-- CreateIndex +CREATE INDEX "chat_messages_cm_role_idx" ON "chat_messages"("cm_role"); + +-- CreateIndex +CREATE INDEX "chat_attachments_catt_cm_id_idx" ON "chat_attachments"("catt_cm_id"); + +-- CreateIndex +CREATE INDEX "issue_devices_id_ti_id_idx" ON "issue_devices"("id_ti_id"); + +-- CreateIndex +CREATE INDEX "issue_devices_id_dec_id_idx" ON "issue_devices"("id_dec_id"); + +-- AddForeignKey +ALTER TABLE "sections" ADD CONSTRAINT "sections_sec_dept_id_fkey" FOREIGN KEY ("sec_dept_id") REFERENCES "departments"("dept_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "users" ADD CONSTRAINT "users_us_dept_id_fkey" FOREIGN KEY ("us_dept_id") REFERENCES "departments"("dept_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "users" ADD CONSTRAINT "users_us_sec_id_fkey" FOREIGN KEY ("us_sec_id") REFERENCES "sections"("sec_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "devices" ADD CONSTRAINT "devices_de_af_id_fkey" FOREIGN KEY ("de_af_id") REFERENCES "approval_flows"("af_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "devices" ADD CONSTRAINT "devices_de_ca_id_fkey" FOREIGN KEY ("de_ca_id") REFERENCES "categories"("ca_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "devices" ADD CONSTRAINT "devices_de_us_id_fkey" FOREIGN KEY ("de_us_id") REFERENCES "users"("us_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "devices" ADD CONSTRAINT "devices_de_sec_id_fkey" FOREIGN KEY ("de_sec_id") REFERENCES "sections"("sec_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "device_childs" ADD CONSTRAINT "device_childs_dec_de_id_fkey" FOREIGN KEY ("dec_de_id") REFERENCES "devices"("de_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "carts" ADD CONSTRAINT "carts_ct_us_id_fkey" FOREIGN KEY ("ct_us_id") REFERENCES "users"("us_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "cart_items" ADD CONSTRAINT "cart_items_cti_ct_id_fkey" FOREIGN KEY ("cti_ct_id") REFERENCES "carts"("ct_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "cart_items" ADD CONSTRAINT "cart_items_cti_de_id_fkey" FOREIGN KEY ("cti_de_id") REFERENCES "devices"("de_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "cart_device_childs" ADD CONSTRAINT "cart_device_childs_cdc_cti_id_fkey" FOREIGN KEY ("cdc_cti_id") REFERENCES "cart_items"("cti_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "cart_device_childs" ADD CONSTRAINT "cart_device_childs_cdc_dec_id_fkey" FOREIGN KEY ("cdc_dec_id") REFERENCES "device_childs"("dec_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_rt_us_id_fkey" FOREIGN KEY ("rt_us_id") REFERENCES "users"("us_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "accessories" ADD CONSTRAINT "accessories_acc_de_id_fkey" FOREIGN KEY ("acc_de_id") REFERENCES "devices"("de_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "approval_flows" ADD CONSTRAINT "approval_flows_af_us_id_fkey" FOREIGN KEY ("af_us_id") REFERENCES "users"("us_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "approval_flow_steps" ADD CONSTRAINT "approval_flow_steps_afs_af_id_fkey" FOREIGN KEY ("afs_af_id") REFERENCES "approval_flows"("af_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "approval_flow_steps" ADD CONSTRAINT "approval_flow_steps_afs_dept_id_fkey" FOREIGN KEY ("afs_dept_id") REFERENCES "departments"("dept_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "approval_flow_steps" ADD CONSTRAINT "approval_flow_steps_afs_sec_id_fkey" FOREIGN KEY ("afs_sec_id") REFERENCES "sections"("sec_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "device_availabilities" ADD CONSTRAINT "device_availabilities_da_dec_id_fkey" FOREIGN KEY ("da_dec_id") REFERENCES "device_childs"("dec_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "device_availabilities" ADD CONSTRAINT "device_availabilities_da_brt_id_fkey" FOREIGN KEY ("da_brt_id") REFERENCES "borrow_return_tickets"("brt_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "borrow_return_tickets" ADD CONSTRAINT "borrow_return_tickets_brt_af_id_fkey" FOREIGN KEY ("brt_af_id") REFERENCES "approval_flows"("af_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "borrow_return_tickets" ADD CONSTRAINT "borrow_return_tickets_brt_user_id_fkey" FOREIGN KEY ("brt_user_id") REFERENCES "users"("us_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "borrow_return_tickets" ADD CONSTRAINT "borrow_return_tickets_brt_staff_id_fkey" FOREIGN KEY ("brt_staff_id") REFERENCES "users"("us_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "borrow_return_ticket_stages" ADD CONSTRAINT "borrow_return_ticket_stages_brts_brt_id_fkey" FOREIGN KEY ("brts_brt_id") REFERENCES "borrow_return_tickets"("brt_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "borrow_return_ticket_stages" ADD CONSTRAINT "borrow_return_ticket_stages_brts_us_id_fkey" FOREIGN KEY ("brts_us_id") REFERENCES "users"("us_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "borrow_return_ticket_stages" ADD CONSTRAINT "borrow_return_ticket_stages_brts_dept_id_fkey" FOREIGN KEY ("brts_dept_id") REFERENCES "departments"("dept_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "borrow_return_ticket_stages" ADD CONSTRAINT "borrow_return_ticket_stages_brts_sec_id_fkey" FOREIGN KEY ("brts_sec_id") REFERENCES "sections"("sec_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ticket_devices" ADD CONSTRAINT "ticket_devices_td_brt_id_fkey" FOREIGN KEY ("td_brt_id") REFERENCES "borrow_return_tickets"("brt_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ticket_devices" ADD CONSTRAINT "ticket_devices_td_dec_id_fkey" FOREIGN KEY ("td_dec_id") REFERENCES "device_childs"("dec_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ticket_issues" ADD CONSTRAINT "ticket_issues_ti_de_id_fkey" FOREIGN KEY ("ti_de_id") REFERENCES "devices"("de_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ticket_issues" ADD CONSTRAINT "ticket_issues_ti_brt_id_fkey" FOREIGN KEY ("ti_brt_id") REFERENCES "borrow_return_tickets"("brt_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ticket_issues" ADD CONSTRAINT "ticket_issues_ti_reported_by_fkey" FOREIGN KEY ("ti_reported_by") REFERENCES "users"("us_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ticket_issues" ADD CONSTRAINT "ticket_issues_ti_assigned_to_fkey" FOREIGN KEY ("ti_assigned_to") REFERENCES "users"("us_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "issue_attachments" ADD CONSTRAINT "issue_attachments_iatt_ti_id_fkey" FOREIGN KEY ("iatt_ti_id") REFERENCES "ticket_issues"("ti_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "issue_attachments" ADD CONSTRAINT "issue_attachments_uploaded_by_fkey" FOREIGN KEY ("uploaded_by") REFERENCES "users"("us_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "notifications" ADD CONSTRAINT "notifications_n_brt_id_fkey" FOREIGN KEY ("n_brt_id") REFERENCES "borrow_return_tickets"("brt_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "notifications" ADD CONSTRAINT "notifications_n_brts_id_fkey" FOREIGN KEY ("n_brts_id") REFERENCES "borrow_return_ticket_stages"("brts_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "notifications" ADD CONSTRAINT "notifications_n_ti_id_fkey" FOREIGN KEY ("n_ti_id") REFERENCES "ticket_issues"("ti_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "notification_recipients" ADD CONSTRAINT "notification_recipients_nr_n_id_fkey" FOREIGN KEY ("nr_n_id") REFERENCES "notifications"("n_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "notification_recipients" ADD CONSTRAINT "notification_recipients_nr_us_id_fkey" FOREIGN KEY ("nr_us_id") REFERENCES "users"("us_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "log_borrow_returns" ADD CONSTRAINT "log_borrow_returns_lbr_brt_id_fkey" FOREIGN KEY ("lbr_brt_id") REFERENCES "borrow_return_tickets"("brt_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "log_borrow_returns" ADD CONSTRAINT "log_borrow_returns_lbr_actor_id_fkey" FOREIGN KEY ("lbr_actor_id") REFERENCES "users"("us_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "log_issues" ADD CONSTRAINT "log_issues_li_ti_id_fkey" FOREIGN KEY ("li_ti_id") REFERENCES "ticket_issues"("ti_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "log_issues" ADD CONSTRAINT "log_issues_li_actor_id_fkey" FOREIGN KEY ("li_actor_id") REFERENCES "users"("us_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "log_device_childs" ADD CONSTRAINT "log_device_childs_ldc_actor_id_fkey" FOREIGN KEY ("ldc_actor_id") REFERENCES "users"("us_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "log_device_childs" ADD CONSTRAINT "log_device_childs_ldc_ti_id_fkey" FOREIGN KEY ("ldc_ti_id") REFERENCES "ticket_issues"("ti_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "log_device_childs" ADD CONSTRAINT "log_device_childs_ldc_brt_id_fkey" FOREIGN KEY ("ldc_brt_id") REFERENCES "borrow_return_tickets"("brt_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "log_device_childs" ADD CONSTRAINT "log_device_childs_ldc_dec_id_fkey" FOREIGN KEY ("ldc_dec_id") REFERENCES "device_childs"("dec_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "chat_rooms" ADD CONSTRAINT "chat_rooms_cr_us_id_fkey" FOREIGN KEY ("cr_us_id") REFERENCES "users"("us_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "chat_messages" ADD CONSTRAINT "chat_messages_cm_cr_id_fkey" FOREIGN KEY ("cm_cr_id") REFERENCES "chat_rooms"("cr_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "chat_messages" ADD CONSTRAINT "chat_messages_cm_parent_id_fkey" FOREIGN KEY ("cm_parent_id") REFERENCES "chat_messages"("cm_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "chat_attachments" ADD CONSTRAINT "chat_attachments_catt_cm_id_fkey" FOREIGN KEY ("catt_cm_id") REFERENCES "chat_messages"("cm_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "issue_devices" ADD CONSTRAINT "issue_devices_id_ti_id_fkey" FOREIGN KEY ("id_ti_id") REFERENCES "ticket_issues"("ti_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "issue_devices" ADD CONSTRAINT "issue_devices_id_dec_id_fkey" FOREIGN KEY ("id_dec_id") REFERENCES "device_childs"("dec_id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/server/src/modules/repair/index.ts b/server/src/modules/repair/index.ts new file mode 100644 index 00000000..c2fe3314 --- /dev/null +++ b/server/src/modules/repair/index.ts @@ -0,0 +1,3 @@ +export { default as repairTicketsRouter } from "./repair-tickets.routes.js"; +export { repairTicketsService } from "./repair-tickets.service.js"; +export * as repairTicketsSchema from "./repair-tickets.schema.js"; \ No newline at end of file diff --git a/server/src/modules/repair/repair-tickets.controller.ts b/server/src/modules/repair/repair-tickets.controller.ts new file mode 100644 index 00000000..847d018b --- /dev/null +++ b/server/src/modules/repair/repair-tickets.controller.ts @@ -0,0 +1,67 @@ +import { NextFunction, Request, Response } from "express"; +import { repairTicketsService } from "./repair-tickets.service.js"; +import { approveRepairTicketBodySchema } from "./repair-tickets.schema.js"; +import { BaseResponse } from "../../core/base.response.js"; +import { BaseController } from "../../core/base.controller.js"; +import { + getRepairTicketsQuerySchema, + RepairTicketsResponse +} from "./repair-tickets.schema.js"; +import { ticket_issues } from "@prisma/client"; + +export type PaginatedBaseResponse = BaseResponse & { + pagination: P; +}; + +export class RepairTicketsController extends BaseController { + constructor() { + super(); + } + + /** + * Description: ดึงรายการแจ้งซ่อมทั้งหมด พร้อมรองรับระบบตรวจสอบ Query (Zod) และแบ่งหน้า (Pagination) + * Input : req, res, next + * Output : Promise + * Author : Worrawat Namwat (Wave) 66160372 + */ + async getRepairTickets( + req: Request, + res: Response, + next: NextFunction + ): Promise> { + const query = getRepairTicketsQuerySchema.parse(req.query); + const result = await repairTicketsService.getRepairTickets(query); + + return { + success: true, + data: result.data, + pagination: result.pagination + }; + } + + /** + * Description: อนุมัติรับเรื่องคำร้องแจ้งซ่อม โดยมีการตรวจสอบ Body ผ่าน Zod แบบ safeParse + * Input : req (params: id, body: user_id), res, next + * Output : Promise + * Author : Worrawat Namwat (Wave) 66160372 + */ + async approveTicket( + req: Request, + res: Response, + next: NextFunction + ): Promise> { + + const ticketId = Number(req.params.id); + + // Zod ตรวจสอบข้อมูล ถ้าไม่ผ่านระบบ Router จะจับโยนเป็น 400 ให้อัตโนมัติ + const body = approveRepairTicketBodySchema.parse(req.body); + + // เรียกใช้ Service อัปเดตข้อมูลใน Database + const result = await repairTicketsService.approveTicket(ticketId, body.user_id); + return { + success: true, + message: "รับคำร้องแจ้งซ่อมสำเร็จ", + data: result + }; + } +} \ No newline at end of file diff --git a/server/src/modules/repair/repair-tickets.routes.ts b/server/src/modules/repair/repair-tickets.routes.ts new file mode 100644 index 00000000..8935c539 --- /dev/null +++ b/server/src/modules/repair/repair-tickets.routes.ts @@ -0,0 +1,43 @@ +import { Router } from "../../core/router.js"; +import { approveRepairTicketBodySchema } from "./repair-tickets.schema.js"; +import { RepairTicketsController } from "./repair-tickets.controller.js"; +import { + getRepairTicketsResponseSchema, + getRepairTicketsQuerySchema +} from "./repair-tickets.schema.js"; + +const controller = new RepairTicketsController(); +const router = new Router(undefined, '/repair-tickets'); + +/** + * Route: GET /repair-tickets + * Description: API ดึงรายการแจ้งซ่อมทั้งหมด + * Input : Query (page, limit, search, status, dates) + * Output : { status, data: [], pagination } + * Author : Worrawat Namwat (Wave) 66160372 + */ +router.getDoc("/", { + tag: "Repair-Tickets", + summary: "ดึงรายการแจ้งซ่อม", + description: "ดึงข้อมูล Ticket Issues พร้อมฟิลเตอร์ สถานะ, วันที่ และค้นหา", + query: getRepairTicketsQuerySchema, + res: getRepairTicketsResponseSchema, + auth: true +}, controller.getRepairTickets); + +/** + * Route: PATCH /repair-tickets/:id/approve + * Description: อนุมัติใบแจ้งซ่อม + * Input : Path Param (id), Body (user_id) + * Output : { success: boolean, message: string } + * Author : Worrawat Namwat (Wave) 66160372 + */ +router.patchDoc("/:id/approve", { + tag: "Repair-Tickets", + summary: "อนุมัติใบแจ้งซ่อม", + description: "เปลี่ยนสถานะใบแจ้งซ่อมเป็นกำลังดำเนินการ และบันทึกผู้รับเรื่อง", + body: approveRepairTicketBodySchema, + auth: true +}, controller.approveTicket); + +export default router.instance; \ No newline at end of file diff --git a/server/src/modules/repair/repair-tickets.schema.ts b/server/src/modules/repair/repair-tickets.schema.ts new file mode 100644 index 00000000..ba3cea30 --- /dev/null +++ b/server/src/modules/repair/repair-tickets.schema.ts @@ -0,0 +1,71 @@ +import { z } from "zod"; +import { TI_STATUS } from "@prisma/client"; + +// Schema สำหรับข้อมูลภายใน (Data Item) +export const repairTicketItemSchema = z.object({ + id: z.number().openapi({ description: "ID ของ Ticket" }), + ticket_no: z.string().openapi({ description: "เลขที่ใบแจ้งซ่อม (TI-xxxxx)" }), + status: z.nativeEnum(TI_STATUS).openapi({ description: "สถานะงานซ่อม" }), + dates: z.object({ + created: z.string().openapi({ description: "วันที่แจ้ง" }), + updated: z.string().nullable().openapi({ description: "อัปเดตล่าสุด" }), + }).openapi({ description: "ข้อมูลวันที่" }), + device_info: z.object({ + name: z.string().openapi({ description: "ชื่ออุปกรณ์" }), + asset_code: z.string().nullable().openapi({ description: "รหัสทรัพย์สิน" }), + category: z.string().nullable().openapi({ description: "หมวดหมู่อุปกรณ์" }), + quantity: z.number().openapi({ description: "จำนวนอุปกรณ์" }), + location: z.string().nullable().openapi({ description: "สถานที่ตั้งอุปกรณ์/รับอุปกรณ์" }), + image: z.string().nullable().openapi({ description: "รูปภาพอุปกรณ์" }), + reported_devices: z.array(z.object({ + asset_code: z.string().nullable(), + serial_number: z.string().nullable(), + })).openapi({ description: "รายการอุปกรณ์ลูกที่แจ้งซ่อม" }), + }).openapi({ description: "ข้อมูลอุปกรณ์" }), + problem: z.object({ + title: z.string().openapi({ description: "หัวข้ออาการเสีย" }), + description: z.string().openapi({ description: "รายละเอียดเพิ่มเติม" }), + }).openapi({ description: "ข้อมูลปัญหา" }), + requester: z.object({ + user_id: z.number().openapi({ description: "ID ของผู้แจ้ง" }), + emp_code: z.string().nullable().openapi({ description: "รหัสพนักงาน" }), + fullname: z.string().openapi({ description: "ชื่อผู้แจ้ง" }), + department: z.string().nullable().openapi({ description: "แผนก" }), + section: z.string().nullable().openapi({ description: "ฝ่ายย่อย" }), + }).openapi({ description: "ผู้แจ้ง" }), + approver: z.object({ + fullname: z.string().openapi({ description: "ชื่อผู้อนุมัติ/ผู้รับงาน" }), + }).nullable().openapi({ description: "ข้อมูลผู้อนุมัติ" }), +}); + +// Schema สำหรับ Query String (Search & Filter) +export const getRepairTicketsQuerySchema = z.object({ + page: z.coerce.number().min(1).default(1), + limit: z.coerce.number().min(1).default(10), + search: z.string().optional(), + status: z.nativeEnum(TI_STATUS).optional(), + start_date: z.string().optional(), + end_date: z.string().optional(), +}); + +// Schema สำหรับ Response รวม +export const getRepairTicketsResponseSchema = z.object({ + status: z.string().default("success"), + data: z.array(repairTicketItemSchema), + pagination: z.object({ + page: z.number(), + limit: z.number(), + totalItems: z.number(), + totalPages: z.number(), + }), +}); + +// Schema สำหรับ Body ตอนกดอนุมัติ +export const approveRepairTicketBodySchema = z.object({ + user_id: z.coerce.number().openapi({ description: "ID ของผู้ที่กดอนุมัติ/รับเรื่อง" }), +}); + +export type GetRepairTicketsQuery = z.infer; +export type RepairTicketItem = z.infer; +export type ApproveRepairTicketBody = z.infer; +export type RepairTicketsResponse = z.infer; \ No newline at end of file diff --git a/server/src/modules/repair/repair-tickets.service.ts b/server/src/modules/repair/repair-tickets.service.ts new file mode 100644 index 00000000..13b41517 --- /dev/null +++ b/server/src/modules/repair/repair-tickets.service.ts @@ -0,0 +1,162 @@ +import { prisma } from "../../infrastructure/database/client.js"; +import { Prisma, ticket_issues, TI_STATUS } from "@prisma/client"; +import { GetRepairTicketsQuery, RepairTicketItem } from "./repair-tickets.schema.js"; + +export const repairTicketsService = { + async getRepairTickets(query: GetRepairTicketsQuery) { + const { page, limit, search, status, start_date, end_date } = query; + const skip = (page - 1) * limit; + + const whereCondition: Prisma.ticket_issuesWhereInput = { + deleted_at: null, + ...(status && { ti_status: status }), + ...(start_date && end_date && { + created_at: { + gte: new Date(start_date), + lte: new Date(new Date(end_date).setHours(23, 59, 59)), + } + }), + ...(search && { + OR: [ + { ti_title: { contains: search, mode: 'insensitive' } }, + { device: { de_name: { contains: search, mode: 'insensitive' } } }, + { reporter: { us_firstname: { contains: search, mode: 'insensitive' } } }, + { reporter: { us_emp_code: { contains: search, mode: 'insensitive' } } }, + ] + }) + }; + + const [totalItems, tickets] = await Promise.all([ + prisma.ticket_issues.count({ where: whereCondition }), + prisma.ticket_issues.findMany({ + where: whereCondition, + skip, + take: limit, + orderBy: { created_at: 'desc' }, + include: { + device: { + include: { category: true } + }, + reporter: { + include: { + department: true, + section: true + } + }, + issue_devices: { + include: { device_child: true } + }, + assignee: true, + } + }) + ]); + + type TicketType = typeof tickets[number]; + type IssueDeviceType = NonNullable[number]; + + const formattedData: RepairTicketItem[] = tickets.map((t: TicketType) => { + const childDevice = t.issue_devices?.[0]?.device_child; + const assetCode = childDevice?.dec_asset_code + || childDevice?.dec_serial_number + || t.device?.de_serial_number + || null; + + // หาจำนวนอุปกรณ์ (ถ้ามีการผูก issue_devices ไว้หลายชิ้นก็ใช้ค่านั้น ถ้าไม่มีก็ตีเป็น 1) + const quantity = t.issue_devices && t.issue_devices.length > 0 ? t.issue_devices.length : 1; + + const rawDept = t.reporter?.department?.dept_name || ""; + const rawSection = t.reporter?.section?.sec_name || ""; + + // ตัดคำ "แผนก " ออกจาก rawDept + const cleanDept = rawDept.replace(/^แผนก\s*/, ""); + + // "แผนก มีเดีย ฝ่ายย่อย " ให้เหลือแค่ตัวอักษรท้ายจาก rawSection + const cleanSection = rawSection.replace(/^.*ฝ่ายย่อย\s*/i, "").trim(); + + const reportedDevices = t.issue_devices?.map((id: IssueDeviceType) => ({ + asset_code: id.device_child?.dec_asset_code || id.device_child?.dec_serial_number || "-", + serial_number: id.device_child?.dec_serial_number || null + })) || []; + + return { + id: t.ti_id, + ticket_no: `TI-${t.ti_id.toString().padStart(5, '0')}`, + status: t.ti_status, + dates: { + created: t.created_at.toISOString(), + updated: t.updated_at ? t.updated_at.toISOString() : null, + }, + device_info: { + name: t.device?.de_name || "ไม่ระบุอุปกรณ์", + asset_code: assetCode !== "-" ? assetCode : null, + category: t.device?.category?.ca_name || "ไม่ระบุหมวดหมู่", + quantity: quantity, + location: t.device?.de_location || "-", + image: t.device?.de_images || null, + reported_devices: reportedDevices, + }, + problem: { + title: t.ti_title || "ไม่ระบุหัวข้อ", + description: t.ti_description || "-", + }, + requester: { + user_id: t.reporter?.us_id || 0, + emp_code: t.reporter?.us_emp_code || "-", + fullname: t.reporter ? `${t.reporter.us_firstname} ${t.reporter.us_lastname}` : "ไม่ระบุชื่อ", + department: cleanDept || "-", + section: cleanSection || "-", + }, + approver: t.assignee ? { + fullname: `${t.assignee.us_firstname} ${t.assignee.us_lastname}`, + } : null, + }; + }); + + return { + status: "success", + data: formattedData, + pagination: { + page, + limit, + totalItems: totalItems, + totalPages: Math.ceil(totalItems / limit) || 1, + } + }; + }, + + async approveTicket(ticketId: number, approverId: number): Promise { + + console.log("approveTicket called:", { ticketId, approverId }); + + // ตรวจสอบ ticket มีจริงไหม + const existingTicket = await prisma.ticket_issues.findUnique({ + where: { ti_id: ticketId } + }); + + if (!existingTicket) { + throw new Error("ไม่พบ ticket นี้"); + } + + // ตรวจสอบ user มีจริงไหม + const existingUser = await prisma.users.findUnique({ + where: { us_id: approverId } + }); + + if (!existingUser) { + throw new Error("ไม่พบ user นี้"); + } + + // update + const updatedTicket = await prisma.ticket_issues.update({ + where: { ti_id: ticketId }, + data: { + ti_status: TI_STATUS.IN_PROGRESS, + ti_assigned_to: approverId + } + }); + + console.log("update success"); + + return updatedTicket; + } +}; \ No newline at end of file diff --git a/server/src/routes.ts b/server/src/routes.ts index dcea55fd..acb47e32 100644 --- a/server/src/routes.ts +++ b/server/src/routes.ts @@ -17,6 +17,7 @@ import { homeRouter } from "./modules/home/index.js"; import { borrowRouter } from "./modules/borrows/index.js"; import { usersRouter } from "./modules/users/index.js"; import { historyApprovalRouter } from "./modules/history-approval/index.js"; +import { repairTicketsRouter } from "./modules/repair/index.js"; import { historyIssueRouter } from "./modules/history-issue/index.js"; /** @@ -91,6 +92,8 @@ export function routes(app: Express) { api.use("/history-borrow", authMiddleware, historyBorrowRouter); + api.use("/repair-tickets", authMiddleware, repairTicketsRouter); + // ผูก router ทั้งหมดไว้ใต้ /api/v1 app.use("/api/v1", api); }