mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
Add comment input on Internal and Vendor tabs
- Add CommentInput component with textarea and send button - Enter to send, Shift+Enter for new line - New comments append to the list immediately without reload - Fixed 400px height for tab content with scrollable overflow - Comment input bar pinned at bottom of tab container - Add addComment API function calling AddCommentJson endpoint - Auto-scroll to newest comment
This commit is contained in:
parent
dc8c8b68f3
commit
d3a4bcc984
3 changed files with 174 additions and 36 deletions
|
|
@ -1,4 +1,6 @@
|
||||||
import { workOrderApi } from "../../services/api";
|
import { workOrderApi } from "../../services/api";
|
||||||
|
import { post } from "../../apiUtil";
|
||||||
|
import { API_URL } from "../../constants";
|
||||||
|
|
||||||
export const getWorkOrders = async (params = {}) => {
|
export const getWorkOrders = async (params = {}) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -68,6 +70,14 @@ export const editWorkOrder = async (workOrder) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const addComment = async (workOrderId, text, commentType = "internal") => {
|
||||||
|
return post(`${API_URL}/WorkOrder/AddCommentJson`, {
|
||||||
|
WorkorderId: workOrderId,
|
||||||
|
Text: text,
|
||||||
|
CommentType: commentType,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteWorkOrder = async (id) => {
|
export const deleteWorkOrder = async (id) => {
|
||||||
try {
|
try {
|
||||||
await workOrderApi.delete(id);
|
await workOrderApi.delete(id);
|
||||||
|
|
|
||||||
|
|
@ -126,10 +126,23 @@
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.woTabContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 400px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 0 0 6px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woTabScroll {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.woCommentTabs {
|
.woCommentTabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
margin-bottom: 16px;
|
|
||||||
border-bottom: 2px solid #e5e5e5;
|
border-bottom: 2px solid #e5e5e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,6 +200,37 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.woCommentInput {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
background: #f8f9fa;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woCommentTextarea {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: inherit;
|
||||||
|
resize: none;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woCommentTextarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woCommentSend {
|
||||||
|
align-self: flex-end;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { useParams, useNavigate } from "react-router";
|
import { useParams, useNavigate } from "react-router";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { getWorkOrderById } from "../api";
|
import { getWorkOrderById, addComment } from "../api";
|
||||||
import "./WorkOrderViewPage.css";
|
import "./WorkOrderViewPage.css";
|
||||||
|
|
||||||
function formatDateTime(value) {
|
function formatDateTime(value) {
|
||||||
|
|
@ -14,54 +14,109 @@ function formatDateTime(value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommentTimeline({ comments }) {
|
function CommentTimeline({ comments }) {
|
||||||
|
const bottomRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
bottomRef.current?.scrollIntoView();
|
||||||
|
}, [comments?.length]);
|
||||||
|
|
||||||
if (!comments || comments.length === 0) {
|
if (!comments || comments.length === 0) {
|
||||||
return <p className="woNoComments">No comments yet.</p>;
|
return <div className="woTabScroll"><p className="woNoComments">No comments yet.</p></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woTimeline">
|
<div className="woTabScroll">
|
||||||
{comments.map((c) => (
|
<div className="woTimeline">
|
||||||
<div key={c.id} className="woTimelineItem">
|
{comments.map((c) => (
|
||||||
<div className="woTimelineHeader">
|
<div key={c.id} className="woTimelineItem">
|
||||||
<span className="woTimelineAuthor">{c.userName || c.commenter || "System"}</span>
|
<div className="woTimelineHeader">
|
||||||
<span className="woTimelineDate">
|
<span className="woTimelineAuthor">{c.userName || c.commenter || "System"}</span>
|
||||||
{c.createdDate ? formatDateTime(c.createdDate) : ""}
|
<span className="woTimelineDate">
|
||||||
</span>
|
{c.createdDate ? formatDateTime(c.createdDate) : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="woTimelineBody">{c.commenttext}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="woTimelineBody">{c.commenttext}</div>
|
))}
|
||||||
</div>
|
<div ref={bottomRef} />
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuditTimeline({ entries }) {
|
function AuditTimeline({ entries }) {
|
||||||
if (!entries || entries.length === 0) {
|
if (!entries || entries.length === 0) {
|
||||||
return <p className="woNoComments">No changes recorded.</p>;
|
return <div className="woTabScroll"><p className="woNoComments">No changes recorded.</p></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woTimeline">
|
<div className="woTabScroll">
|
||||||
{entries.map((e) => (
|
<div className="woTimeline">
|
||||||
<div key={e.id} className="woTimelineItem woAuditItem">
|
{entries.map((e) => (
|
||||||
<div className="woTimelineHeader">
|
<div key={e.id} className="woTimelineItem woAuditItem">
|
||||||
<span className="woTimelineAuthor">{e.userName || "System"}</span>
|
<div className="woTimelineHeader">
|
||||||
<span className="woTimelineDate">
|
<span className="woTimelineAuthor">{e.userName || "System"}</span>
|
||||||
{e.createdAt ? formatDateTime(e.createdAt) : ""}
|
<span className="woTimelineDate">
|
||||||
</span>
|
{e.createdAt ? formatDateTime(e.createdAt) : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="woTimelineBody">
|
||||||
|
<span className="woAuditField">{e.fieldName}</span>
|
||||||
|
{e.oldValue && <span className="woAuditOld">{e.oldValue}</span>}
|
||||||
|
<span className="woAuditArrow">→</span>
|
||||||
|
<span className="woAuditNew">{e.newValue}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="woTimelineBody">
|
))}
|
||||||
<span className="woAuditField">{e.fieldName}</span>
|
</div>
|
||||||
{e.oldValue && <span className="woAuditOld">{e.oldValue}</span>}
|
|
||||||
<span className="woAuditArrow">→</span>
|
|
||||||
<span className="woAuditNew">{e.newValue}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CommentInput({ workOrderId, commentType, onCommentAdded }) {
|
||||||
|
const [text, setText] = useState("");
|
||||||
|
const [sending, setSending] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!text.trim()) return;
|
||||||
|
setSending(true);
|
||||||
|
try {
|
||||||
|
const newComment = await addComment(workOrderId, text.trim(), commentType);
|
||||||
|
setText("");
|
||||||
|
if (onCommentAdded) onCommentAdded(newComment);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error("Failed to add comment");
|
||||||
|
} finally {
|
||||||
|
setSending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="woCommentInput" onSubmit={handleSubmit}>
|
||||||
|
<textarea
|
||||||
|
className="woCommentTextarea"
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Type a comment... (Enter to send, Shift+Enter for new line)"
|
||||||
|
rows={2}
|
||||||
|
disabled={sending}
|
||||||
|
/>
|
||||||
|
<button className="btn btn-primary woCommentSend" type="submit" disabled={sending || !text.trim()}>
|
||||||
|
{sending ? "Sending..." : "Send"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function WorkOrderViewPage() {
|
export default function WorkOrderViewPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -83,6 +138,24 @@ export default function WorkOrderViewPage() {
|
||||||
load();
|
load();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
const handleCommentAdded = (newComment) => {
|
||||||
|
setWo((prev) => ({
|
||||||
|
...prev,
|
||||||
|
comments: [
|
||||||
|
...prev.comments,
|
||||||
|
{
|
||||||
|
id: newComment.id,
|
||||||
|
createdDate: newComment.createdDate,
|
||||||
|
commenttext: newComment.commenttext,
|
||||||
|
commentType: newComment.commentType,
|
||||||
|
userName: newComment.userName,
|
||||||
|
commenter: null,
|
||||||
|
recordType: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) return <div className="woViewPage"><div className="loading">Loading...</div></div>;
|
if (loading) return <div className="woViewPage"><div className="loading">Loading...</div></div>;
|
||||||
if (!wo) return <div className="woViewPage"><div className="loading">Work order not found.</div></div>;
|
if (!wo) return <div className="woViewPage"><div className="loading">Work order not found.</div></div>;
|
||||||
|
|
||||||
|
|
@ -101,6 +174,8 @@ export default function WorkOrderViewPage() {
|
||||||
{ key: "audit", label: "Audit Log", count: auditLog.length },
|
{ key: "audit", label: "Audit Log", count: auditLog.length },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const showInput = activeTab === "internal" || activeTab === "vendor";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woViewPage">
|
<div className="woViewPage">
|
||||||
<div className="panel panel-default panel-connected">
|
<div className="panel panel-default panel-connected">
|
||||||
|
|
@ -188,10 +263,19 @@ export default function WorkOrderViewPage() {
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{activeTab === "customer" && <CommentTimeline comments={customerComments} />}
|
<div className="woTabContainer">
|
||||||
{activeTab === "vendor" && <CommentTimeline comments={vendorComments} />}
|
{activeTab === "customer" && <CommentTimeline comments={customerComments} />}
|
||||||
{activeTab === "internal" && <CommentTimeline comments={internalComments} />}
|
{activeTab === "vendor" && <CommentTimeline comments={vendorComments} />}
|
||||||
{activeTab === "audit" && <AuditTimeline entries={auditLog} />}
|
{activeTab === "internal" && <CommentTimeline comments={internalComments} />}
|
||||||
|
{activeTab === "audit" && <AuditTimeline entries={auditLog} />}
|
||||||
|
{showInput && (
|
||||||
|
<CommentInput
|
||||||
|
workOrderId={parseInt(id)}
|
||||||
|
commentType={activeTab}
|
||||||
|
onCommentAdded={handleCommentAdded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue