mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 10:53:13 +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 { post } from "../../apiUtil";
|
||||
import { API_URL } from "../../constants";
|
||||
|
||||
export const getWorkOrders = async (params = {}) => {
|
||||
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) => {
|
||||
try {
|
||||
await workOrderApi.delete(id);
|
||||
|
|
|
|||
|
|
@ -126,10 +126,23 @@
|
|||
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 {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 2px solid #e5e5e5;
|
||||
}
|
||||
|
||||
|
|
@ -187,6 +200,37 @@
|
|||
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 {
|
||||
padding: 40px;
|
||||
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 { toast } from "react-toastify";
|
||||
import { getWorkOrderById } from "../api";
|
||||
import { getWorkOrderById, addComment } from "../api";
|
||||
import "./WorkOrderViewPage.css";
|
||||
|
||||
function formatDateTime(value) {
|
||||
|
|
@ -14,54 +14,109 @@ function formatDateTime(value) {
|
|||
}
|
||||
|
||||
function CommentTimeline({ comments }) {
|
||||
const bottomRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
bottomRef.current?.scrollIntoView();
|
||||
}, [comments?.length]);
|
||||
|
||||
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 (
|
||||
<div className="woTimeline">
|
||||
{comments.map((c) => (
|
||||
<div key={c.id} className="woTimelineItem">
|
||||
<div className="woTimelineHeader">
|
||||
<span className="woTimelineAuthor">{c.userName || c.commenter || "System"}</span>
|
||||
<span className="woTimelineDate">
|
||||
{c.createdDate ? formatDateTime(c.createdDate) : ""}
|
||||
</span>
|
||||
<div className="woTabScroll">
|
||||
<div className="woTimeline">
|
||||
{comments.map((c) => (
|
||||
<div key={c.id} className="woTimelineItem">
|
||||
<div className="woTimelineHeader">
|
||||
<span className="woTimelineAuthor">{c.userName || c.commenter || "System"}</span>
|
||||
<span className="woTimelineDate">
|
||||
{c.createdDate ? formatDateTime(c.createdDate) : ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="woTimelineBody">{c.commenttext}</div>
|
||||
</div>
|
||||
<div className="woTimelineBody">{c.commenttext}</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AuditTimeline({ entries }) {
|
||||
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 (
|
||||
<div className="woTimeline">
|
||||
{entries.map((e) => (
|
||||
<div key={e.id} className="woTimelineItem woAuditItem">
|
||||
<div className="woTimelineHeader">
|
||||
<span className="woTimelineAuthor">{e.userName || "System"}</span>
|
||||
<span className="woTimelineDate">
|
||||
{e.createdAt ? formatDateTime(e.createdAt) : ""}
|
||||
</span>
|
||||
<div className="woTabScroll">
|
||||
<div className="woTimeline">
|
||||
{entries.map((e) => (
|
||||
<div key={e.id} className="woTimelineItem woAuditItem">
|
||||
<div className="woTimelineHeader">
|
||||
<span className="woTimelineAuthor">{e.userName || "System"}</span>
|
||||
<span className="woTimelineDate">
|
||||
{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 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>
|
||||
);
|
||||
}
|
||||
|
||||
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() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -83,6 +138,24 @@ export default function WorkOrderViewPage() {
|
|||
load();
|
||||
}, [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 (!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 },
|
||||
];
|
||||
|
||||
const showInput = activeTab === "internal" || activeTab === "vendor";
|
||||
|
||||
return (
|
||||
<div className="woViewPage">
|
||||
<div className="panel panel-default panel-connected">
|
||||
|
|
@ -188,10 +263,19 @@ export default function WorkOrderViewPage() {
|
|||
</button>
|
||||
))}
|
||||
</div>
|
||||
{activeTab === "customer" && <CommentTimeline comments={customerComments} />}
|
||||
{activeTab === "vendor" && <CommentTimeline comments={vendorComments} />}
|
||||
{activeTab === "internal" && <CommentTimeline comments={internalComments} />}
|
||||
{activeTab === "audit" && <AuditTimeline entries={auditLog} />}
|
||||
<div className="woTabContainer">
|
||||
{activeTab === "customer" && <CommentTimeline comments={customerComments} />}
|
||||
{activeTab === "vendor" && <CommentTimeline comments={vendorComments} />}
|
||||
{activeTab === "internal" && <CommentTimeline comments={internalComments} />}
|
||||
{activeTab === "audit" && <AuditTimeline entries={auditLog} />}
|
||||
{showInput && (
|
||||
<CommentInput
|
||||
workOrderId={parseInt(id)}
|
||||
commentType={activeTab}
|
||||
onCommentAdded={handleCommentAdded}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue