Fix date validations, datepicker overflow, and QC components
This commit is contained in:
@@ -62,6 +62,16 @@ import { isoToApiDate } from "../lib/jalali";
|
||||
|
||||
const quickValues = [0, 25, 50, 75, 100];
|
||||
|
||||
function parseCleanNumber(raw: string): number {
|
||||
if (!raw) return 0;
|
||||
const clean = raw
|
||||
.replace(/[۰-۹]/g, (d) => "۰۱۲۳۴۵۶۷۸۹".indexOf(d).toString())
|
||||
.replace(/[٠-٩]/g, (d) => "٠١٢٣٤٥٦٧٨٩".indexOf(d).toString())
|
||||
.replace(/[^0-9.]/g, "");
|
||||
const val = parseFloat(clean);
|
||||
return isNaN(val) ? 0 : Math.max(0, val);
|
||||
}
|
||||
|
||||
function SectionCard({ title, children }: { title: ReactNode; children: ReactNode }) {
|
||||
return (
|
||||
<Card className="p-4">
|
||||
@@ -71,10 +81,6 @@ function SectionCard({ title, children }: { title: ReactNode; children: ReactNod
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// صفحه جزئیات وظیفه
|
||||
// ----------------------------------------
|
||||
|
||||
export default function TaskDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -99,7 +105,6 @@ export default function TaskDetailPage() {
|
||||
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
/** ISO میلادی — ارسال با isoToApiDate */
|
||||
const [startDate, setStartDate] = useState<string | null>(null);
|
||||
const [dueDate, setDueDate] = useState<string | null>(null);
|
||||
|
||||
@@ -109,7 +114,14 @@ export default function TaskDetailPage() {
|
||||
const [consumedMaterials, setConsumedMaterials] = useState<MaterialConsumption[]>([]);
|
||||
const [progressNote, setProgressNote] = useState("");
|
||||
|
||||
// محاسبه مجموع مصارف ثبتشده قبلی از سوابق گزارشهای وظیفه
|
||||
const isDateRangeInvalid = Boolean(
|
||||
startDate &&
|
||||
dueDate &&
|
||||
isoToApiDate(startDate) &&
|
||||
isoToApiDate(dueDate) &&
|
||||
isoToApiDate(startDate)! > isoToApiDate(dueDate)!
|
||||
);
|
||||
|
||||
const pastConsumptionMap = useMemo(() => {
|
||||
const map = new Map<string, { total: number; unit: string }>();
|
||||
if (!task?.progress_logs) return map;
|
||||
@@ -148,7 +160,6 @@ export default function TaskDetailPage() {
|
||||
setConsumedHr([]);
|
||||
}
|
||||
|
||||
// یکپارچهسازی مصالح تخصیصیافته و مصارف قبلی
|
||||
const items: MaterialConsumption[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
@@ -158,6 +169,7 @@ export default function TaskDetailPage() {
|
||||
seen.add(key);
|
||||
const past = pastConsumptionMap.get(key)?.total || 0;
|
||||
const alloc = Number(m.count) || 0;
|
||||
const isOver = alloc > 0 && past > alloc;
|
||||
items.push({
|
||||
name: m.name,
|
||||
unit: m.unit || "",
|
||||
@@ -166,14 +178,13 @@ export default function TaskDetailPage() {
|
||||
previous_consumed: past,
|
||||
total_consumed: past,
|
||||
remaining_amount: Math.max(0, alloc - past),
|
||||
is_over_allocated: alloc > 0 ? past > alloc : past > 0,
|
||||
over_allocated_amount: Math.max(0, past - alloc),
|
||||
is_over_allocated: isOver,
|
||||
over_allocated_amount: isOver ? past - alloc : 0,
|
||||
note: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// مصالحی که در سوابق قبل مصرف شدهاند ولی در تخصیص اولیه نبودهاند
|
||||
pastConsumptionMap.forEach((val, key) => {
|
||||
if (!seen.has(key)) {
|
||||
items.push({
|
||||
@@ -184,8 +195,8 @@ export default function TaskDetailPage() {
|
||||
previous_consumed: val.total,
|
||||
total_consumed: val.total,
|
||||
remaining_amount: 0,
|
||||
is_over_allocated: val.total > 0,
|
||||
over_allocated_amount: val.total,
|
||||
is_over_allocated: false,
|
||||
over_allocated_amount: 0,
|
||||
note: "خارج از سقف مصوب",
|
||||
});
|
||||
}
|
||||
@@ -197,14 +208,12 @@ export default function TaskDetailPage() {
|
||||
|
||||
const invalidateTask = () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["task", taskIdParam] });
|
||||
// کلید QC با همان شناسهای است که صفحههای QC استفاده میکنند
|
||||
void queryClient.invalidateQueries({ queryKey: ["task-qc", currentTaskId] });
|
||||
void queryClient.invalidateQueries({ queryKey: ["today"] });
|
||||
void queryClient.invalidateQueries({ queryKey: ["tasks"] });
|
||||
};
|
||||
|
||||
const progressMutation = useMutation({
|
||||
// ثبت پیشرفت کار همراه با مصرف واقعی نیروی انسانی و مصالح
|
||||
mutationFn: (payload: TaskProgressPayload) => updateTaskProgress(currentTaskId, payload),
|
||||
onSuccess: () => {
|
||||
toast.success("پیشرفت کار و گزارش مصرف منابع با موفقیت ثبت شد");
|
||||
@@ -215,8 +224,7 @@ export default function TaskDetailPage() {
|
||||
});
|
||||
|
||||
const handleMaterialAmountChange = (idx: number, rawVal: string) => {
|
||||
const val = parseFloat(rawVal);
|
||||
const amt = Math.max(0, isNaN(val) ? 0 : val);
|
||||
const amt = parseCleanNumber(rawVal);
|
||||
setConsumedMaterials((prev) => {
|
||||
const next = [...prev];
|
||||
const item = { ...next[idx] };
|
||||
@@ -224,8 +232,8 @@ export default function TaskDetailPage() {
|
||||
const prevC = item.previous_consumed ?? 0;
|
||||
const tot = prevC + amt;
|
||||
const rem = Math.max(0, alloc - tot);
|
||||
const isOver = alloc > 0 ? tot > alloc : tot > 0;
|
||||
const overAmt = Math.max(0, tot - alloc);
|
||||
const isOver = alloc > 0 && tot > alloc;
|
||||
const overAmt = isOver ? tot - alloc : 0;
|
||||
|
||||
item.amount = amt;
|
||||
item.total_consumed = tot;
|
||||
@@ -250,7 +258,7 @@ export default function TaskDetailPage() {
|
||||
...prev,
|
||||
{
|
||||
name: "",
|
||||
unit: "",
|
||||
unit: "عدد",
|
||||
amount: 0,
|
||||
allocated_amount: 0,
|
||||
previous_consumed: 0,
|
||||
@@ -258,7 +266,7 @@ export default function TaskDetailPage() {
|
||||
remaining_amount: 0,
|
||||
is_over_allocated: false,
|
||||
over_allocated_amount: 0,
|
||||
note: "مصالح مازاد / خارج از برنامه",
|
||||
note: "مصالح خارج از برنامه",
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -273,7 +281,7 @@ export default function TaskDetailPage() {
|
||||
const start = isoToApiDate(startDate);
|
||||
const due = isoToApiDate(dueDate);
|
||||
if (start && due && start > due) {
|
||||
throw new ApiError("تاریخ شروع یک وظیفه نمیتواند بعد از تاریخ اتمام آن باشد", 400, "VALIDATION");
|
||||
throw new ApiError("تاریخ پایان نمیتواند کمتر از تاریخ شروع باشد", 400, "VALIDATION");
|
||||
}
|
||||
if (start) payload.start_date = start;
|
||||
if (due) payload.due_date = due;
|
||||
@@ -286,7 +294,6 @@ export default function TaskDetailPage() {
|
||||
onError: (err) => toast.error(err instanceof ApiError ? err.message : "ثبت تاریخها ناموفق بود"),
|
||||
});
|
||||
|
||||
// ۱. وضعیت و جهش ویرایش منابع تخصیصیافته به وظیفه
|
||||
const [showEditResourcesModal, setShowEditResourcesModal] = useState(false);
|
||||
const [editMaterials, setEditMaterials] = useState<MaterialItem[]>([]);
|
||||
|
||||
@@ -310,7 +317,6 @@ export default function TaskDetailPage() {
|
||||
toast.error(err instanceof ApiError ? err.message : "خطا در بهروزرسانی منابع وظیفه"),
|
||||
});
|
||||
|
||||
// ۲. وضعیت و جهشهای مدیریت روابط وابستگی (FS, SS, FF, SF)
|
||||
const [showAddRelationModal, setShowAddRelationModal] = useState(false);
|
||||
const [relTargetId, setRelTargetId] = useState("");
|
||||
const [relDirection, setRelDirection] = useState<"predecessor" | "successor">("predecessor");
|
||||
@@ -336,7 +342,7 @@ export default function TaskDetailPage() {
|
||||
}
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast.error(err?.message || "خطا در ایجاد رابطه وابستگی (احتمال وجود دور یا ورودی نامعتبر)");
|
||||
toast.error(err?.message || "خطا در ایجاد رابطه وابستگی");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -399,8 +405,6 @@ export default function TaskDetailPage() {
|
||||
const projectLabel = project?.name ?? "—";
|
||||
const overdue = isOverdue(task.due_date);
|
||||
|
||||
// مجوزهای نقش: ناظر پیشرفت را ثبت و بازرسی را اجرا میکند، اما ساختار QC و
|
||||
// تاریخهای برنامه و ویرایش تسک را تغییر نمیدهد (سرور هم مستقل همین را بررسی میکند).
|
||||
const isSupervisor = (user?.role as string) === "project_supervisor" || (user?.role as string) === "supervisor";
|
||||
const canDefineQc = !isSupervisor && hasPermission(user, Perm.qcDefine);
|
||||
const canExecuteQc = hasPermission(user, Perm.qcExecute) || isSupervisor;
|
||||
@@ -419,7 +423,6 @@ export default function TaskDetailPage() {
|
||||
const preStatus = qcSummaryStatusMeta(readiness?.pre?.status || "none");
|
||||
const execStatus = qcSummaryStatusMeta(readiness?.exec?.status || "none");
|
||||
|
||||
// پیشبینی قواعد سرور: شروع نیازمند تأیید «پیش از اجرا» و ۱۰۰٪ نیازمند تأیید همه بازرسیها
|
||||
const blockedByStart = progress > 0 && !readiness.can_start;
|
||||
const blockedByComplete = progress >= 100 && !readiness.can_complete;
|
||||
const progressBlocked = blockedByStart || blockedByComplete;
|
||||
@@ -451,7 +454,6 @@ export default function TaskDetailPage() {
|
||||
/>
|
||||
|
||||
<div className="space-y-3 px-4 pb-28 pt-4">
|
||||
{/* کارت اطلاعات اصلی */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-base font-bold leading-6 text-slate-800">{task.subject}</p>
|
||||
@@ -493,7 +495,7 @@ export default function TaskDetailPage() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* کارت یکپارچه پیشرفت، تخصیص و گزارش مصرف مصالح و نیروی انسانی */}
|
||||
{/* کارت پیشرفت و مدیریت یکپارچه مصالح */}
|
||||
<SectionCard
|
||||
title={
|
||||
<div className="flex items-center justify-between w-full">
|
||||
@@ -538,7 +540,6 @@ export default function TaskDetailPage() {
|
||||
className="mt-3 w-full accent-sky-600"
|
||||
aria-label="درصد پیشرفت"
|
||||
/>
|
||||
{/* dir=ltr فقط برای ترتیب چشمی دکمهها: ۰ تا ۱۰۰ از چپ به راست */}
|
||||
<div dir="ltr" className="mt-2 grid grid-cols-5 gap-1.5">
|
||||
{quickValues.map((v) => (
|
||||
<button
|
||||
@@ -572,7 +573,7 @@ export default function TaskDetailPage() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* ۱. بخش مصالح و تجهیزات: تخصیص، مصارف قبلی، مصرف جاری و مانده */}
|
||||
{/* بخش مصالح و تجهیزات مصرفی */}
|
||||
<div className="mt-4 rounded-xl border border-slate-200 bg-white p-3 space-y-3 shadow-2xs">
|
||||
<div className="flex items-center justify-between border-b border-slate-100 pb-2 gap-2">
|
||||
<span className="text-xs font-bold text-slate-900 flex items-center gap-1.5 min-w-0">
|
||||
@@ -582,9 +583,9 @@ export default function TaskDetailPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddCustomMaterial}
|
||||
className="shrink-0 text-[10px] font-semibold text-amber-800 hover:text-amber-950 bg-amber-50 border border-amber-200 rounded-lg px-2 py-1 flex items-center gap-1 shadow-2xs hover:bg-amber-100 transition-colors"
|
||||
className="shrink-0 text-[11px] font-semibold text-amber-800 hover:text-amber-950 bg-amber-50 border border-amber-200 rounded-lg px-2.5 py-1 flex items-center gap-1 shadow-2xs hover:bg-amber-100 transition-colors"
|
||||
>
|
||||
<Plus size={12} />
|
||||
<Plus size={13} />
|
||||
<span>افزودن مصالح خارج برنامه</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -594,15 +595,15 @@ export default function TaskDetailPage() {
|
||||
مصالح یا تجهیزاتی برای این وظیفه تعیین نشده است. میتوانید با دکمه بالا مصالح جدید اضافه کنید.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2.5">
|
||||
<div className="space-y-3">
|
||||
{consumedMaterials.map((item, idx) => {
|
||||
const alloc = item.allocated_amount || 0;
|
||||
const prevC = item.previous_consumed || 0;
|
||||
const currentC = item.amount || 0;
|
||||
const totC = prevC + currentC;
|
||||
const rem = Math.max(0, alloc - totC);
|
||||
const isOver = alloc > 0 ? totC > alloc : totC > 0;
|
||||
const overAmt = Math.max(0, totC - alloc);
|
||||
const isOver = alloc > 0 && totC > alloc;
|
||||
const overAmt = isOver ? totC - alloc : 0;
|
||||
const pctConsumed = alloc > 0 ? Math.min(100, Math.round((totC / alloc) * 100)) : 100;
|
||||
const specs =
|
||||
task?.materials?.find(
|
||||
@@ -612,40 +613,50 @@ export default function TaskDetailPage() {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`rounded-xl border p-2.5 space-y-2.5 bg-white shadow-2xs transition-all ${
|
||||
className={`rounded-xl border p-3 space-y-3 bg-white shadow-2xs transition-all ${
|
||||
isOver ? "border-rose-300 ring-1 ring-rose-200 bg-rose-50/10" : "border-slate-200"
|
||||
}`}
|
||||
>
|
||||
{/* ۱. سطر عنوان، واحد و مشخصات کالا */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
{alloc === 0 ? (
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<input
|
||||
value={item.name}
|
||||
onChange={(e) => handleMaterialFieldChange(idx, "name", e.target.value)}
|
||||
placeholder="نام مصالح..."
|
||||
className="h-7 w-36 rounded border border-amber-300 bg-amber-50/40 px-2 text-xs font-bold text-slate-800 focus:outline-none focus:ring-1 focus:ring-amber-500"
|
||||
/>
|
||||
<input
|
||||
value={item.unit}
|
||||
onChange={(e) => handleMaterialFieldChange(idx, "unit", e.target.value)}
|
||||
placeholder="واحد"
|
||||
className="h-7 w-16 rounded border border-amber-300 bg-amber-50/40 px-2 text-xs text-slate-700 focus:outline-none focus:ring-1 focus:ring-amber-500"
|
||||
/>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 w-full">
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold text-slate-500 block mb-0.5">
|
||||
نام مصالح یا کالا
|
||||
</label>
|
||||
<input
|
||||
value={item.name}
|
||||
onChange={(e) => handleMaterialFieldChange(idx, "name", e.target.value)}
|
||||
placeholder="مثلاً: آجر فشاری، سیمان..."
|
||||
className="h-8 w-full rounded-lg border border-amber-300 bg-amber-50/30 px-2.5 text-xs font-bold text-slate-800 focus:outline-none focus:ring-1 focus:ring-amber-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold text-slate-500 block mb-0.5">
|
||||
واحد سنجش
|
||||
</label>
|
||||
<input
|
||||
value={item.unit}
|
||||
onChange={(e) => handleMaterialFieldChange(idx, "unit", e.target.value)}
|
||||
placeholder="مثلاً: عدد، تن، پاکت..."
|
||||
className="h-8 w-full rounded-lg border border-amber-300 bg-amber-50/30 px-2.5 text-xs text-slate-800 focus:outline-none focus:ring-1 focus:ring-amber-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<span className="font-extrabold text-slate-900 text-[13px]">{item.name}</span>
|
||||
{item.unit && <span className="text-[11px] text-slate-400 font-medium">({item.unit})</span>}
|
||||
<span className="font-extrabold text-slate-900 text-sm">{item.name}</span>
|
||||
{item.unit && <span className="text-xs text-slate-500 font-medium">({item.unit})</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{specs.length > 0 && (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
<div className="mt-1.5 flex flex-wrap gap-1">
|
||||
{specs.map((s, si) => (
|
||||
<span
|
||||
key={si}
|
||||
className="rounded bg-slate-100 text-slate-600 px-1.5 py-0.5 text-[9.5px] border border-slate-200"
|
||||
className="rounded bg-slate-100 text-slate-600 px-1.5 py-0.5 text-[10px] border border-slate-200"
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
@@ -654,17 +665,21 @@ export default function TaskDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* نشانگر وضعیت سقف یا دکمه حذف */}
|
||||
<div className="shrink-0">
|
||||
<div className="shrink-0 pt-0.5">
|
||||
{alloc === 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveMaterialRow(idx)}
|
||||
className="text-slate-400 hover:text-rose-600 p-1 rounded-md hover:bg-rose-50 transition-colors"
|
||||
title="حذف ردیف"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="rounded-md px-2 py-0.5 text-[10px] font-bold border bg-amber-50 text-amber-700 border-amber-200">
|
||||
خارج از برنامه
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveMaterialRow(idx)}
|
||||
className="text-slate-400 hover:text-rose-600 p-1 rounded-md hover:bg-rose-50 transition-colors"
|
||||
title="حذف ردیف"
|
||||
>
|
||||
<Trash2 size={15} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
className={`rounded-md px-2 py-0.5 text-[10px] font-bold border ${
|
||||
@@ -673,74 +688,94 @@ export default function TaskDetailPage() {
|
||||
: "bg-emerald-50 text-emerald-700 border-emerald-200"
|
||||
}`}
|
||||
>
|
||||
{isOver ? "⚠️ مازاد" : "مجاز"}
|
||||
{isOver ? "⚠️ مازاد بر سقف" : "مجاز"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ۲. جدول ۳بخشی منظم حسابداری مصالح (مقدار کل مصوب، مصرف تاکنون، مانده قابل مصرف) */}
|
||||
<div className="grid grid-cols-3 divide-x divide-x-reverse divide-slate-200 rounded-lg bg-slate-50 border border-slate-200 py-1.5 text-center">
|
||||
<div className="grid grid-cols-3 divide-x divide-x-reverse divide-slate-200 rounded-lg bg-slate-50 border border-slate-200 py-2 text-center text-xs">
|
||||
<div className="px-1">
|
||||
<span className="text-slate-400 block text-[9.5px]">مقدار کل (سقف)</span>
|
||||
<span className="font-bold text-slate-800 text-[11.5px] mt-0.5 block">
|
||||
{alloc > 0 ? `${alloc} ${item.unit || ""}` : "نامحدود"}
|
||||
<span className="text-slate-400 block text-[10px]">سقف مصوب</span>
|
||||
<span className="font-bold text-slate-800 text-xs mt-0.5 block">
|
||||
{alloc > 0 ? (
|
||||
<>
|
||||
<span className="tabular-nums">{alloc}</span>{" "}
|
||||
<span className="text-slate-500 font-normal">{item.unit}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-slate-500 font-medium">بدون سقف</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
<span className="text-slate-400 block text-[9.5px]">مصرف تاکنون</span>
|
||||
<span className="font-bold text-slate-700 text-[11.5px] mt-0.5 block">
|
||||
{prevC} {item.unit || ""}
|
||||
<span className="text-slate-400 block text-[10px]">مصرف تاکنون</span>
|
||||
<span className="font-bold text-slate-700 text-xs mt-0.5 block">
|
||||
<span className="tabular-nums">{prevC}</span>{" "}
|
||||
<span className="text-slate-500 font-normal">{item.unit}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
<span className={`block text-[9.5px] ${isOver ? "text-rose-600 font-bold" : "text-emerald-700 font-bold"}`}>
|
||||
{isOver ? "مازاد سقف" : "مانده قابل مصرف"}
|
||||
<span
|
||||
className={`block text-[10px] font-bold ${
|
||||
isOver ? "text-rose-600" : alloc > 0 ? "text-emerald-700" : "text-sky-700"
|
||||
}`}
|
||||
>
|
||||
{isOver ? "مازاد سقف" : alloc > 0 ? "مانده مجاز" : "کل مصرفی"}
|
||||
</span>
|
||||
<span className={`font-black text-[11.5px] mt-0.5 block ${isOver ? "text-rose-600" : "text-emerald-700"}`}>
|
||||
{isOver ? `+${overAmt} ${item.unit || ""}` : `${rem} ${item.unit || ""}`}
|
||||
<span
|
||||
className={`font-black text-xs mt-0.5 block ${
|
||||
isOver ? "text-rose-600" : alloc > 0 ? "text-emerald-700" : "text-sky-700"
|
||||
}`}
|
||||
>
|
||||
{isOver ? (
|
||||
<span dir="ltr">+{overAmt} {item.unit}</span>
|
||||
) : alloc > 0 ? (
|
||||
<span>{rem} {item.unit}</span>
|
||||
) : (
|
||||
<span>{totC} {item.unit}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ۳. ورودی شاخص و مستقل «مصرف این گزارش *» */}
|
||||
<div className="space-y-1 rounded-lg bg-amber-50/70 border border-amber-200 p-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<label className="text-[11.5px] font-black text-amber-950 flex items-center gap-1.5 shrink-0">
|
||||
<div className="rounded-lg bg-amber-50/60 border border-amber-200 p-2.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<label className="text-xs font-bold text-amber-950 flex items-center gap-1.5 shrink-0">
|
||||
<span className="h-2 w-2 rounded-full bg-amber-500 animate-pulse" />
|
||||
<span>مصرف این گزارش *:</span>
|
||||
</label>
|
||||
<div className="flex items-center rounded-lg border-2 border-amber-400 bg-white shadow-xs focus-within:border-amber-600 focus-within:ring-2 focus-within:ring-amber-200">
|
||||
<div className="relative flex items-center w-36">
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
min={0}
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={item.amount === 0 ? "" : item.amount}
|
||||
onFocus={(e) => e.target.select()}
|
||||
onChange={(e) => handleMaterialAmountChange(idx, e.target.value)}
|
||||
placeholder="۰"
|
||||
className="h-8 w-24 bg-transparent px-2 text-center text-sm font-black text-slate-900 focus:outline-none"
|
||||
className="h-8 w-full rounded-lg border-2 border-amber-400 bg-white px-3 pe-12 text-center text-sm font-black text-slate-900 focus:border-amber-600 focus:outline-none focus:ring-2 focus:ring-amber-200"
|
||||
/>
|
||||
{item.unit && (
|
||||
<span className="shrink-0 bg-amber-100/90 border-s border-amber-300 px-2 py-1 text-[11px] font-bold text-amber-900 select-none">
|
||||
<span className="absolute left-2 text-[11px] font-bold text-slate-500 pointer-events-none">
|
||||
{item.unit}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentC > 0 && (
|
||||
<div className="pt-1 border-t border-amber-200/60 flex items-center justify-between text-[10px] text-amber-900">
|
||||
<span>مانده پس از ثبت این گزارش:</span>
|
||||
<b className={`font-black ${isOver ? "text-rose-600" : "text-emerald-700"}`}>
|
||||
{isOver ? `+${overAmt} ${item.unit} (مازاد سقف)` : `${rem} ${item.unit}`}
|
||||
<div className="mt-2 pt-1.5 border-t border-amber-200/60 flex items-center justify-between text-[11px] text-amber-900">
|
||||
<span>مجموع پس از ثبت:</span>
|
||||
<b className="font-black text-slate-800">
|
||||
{totC} {item.unit}
|
||||
</b>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ۴. نوار پیشرفت مصرف به سقف */}
|
||||
{alloc > 0 && (
|
||||
<div className="space-y-1 pt-0.5">
|
||||
<div className="flex items-center justify-between text-[9.5px] text-slate-500">
|
||||
<div className="flex items-center justify-between text-[10px] text-slate-500">
|
||||
<span>کل مصرف: <b className="text-slate-800">{totC}</b> از {alloc} {item.unit}</span>
|
||||
<span className="font-bold">
|
||||
{totC > alloc ? `اضافه مصرف (${Math.round((totC / alloc) * 100)}٪)` : `${pctConsumed}٪ از سقف`}
|
||||
@@ -757,14 +792,13 @@ export default function TaskDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ۵. هشدار اضافه مصرف جهت ثبت در سوابق و استعلام */}
|
||||
{isOver && (
|
||||
<p className="text-[10px] text-rose-700 font-medium flex items-center gap-1 bg-rose-50 p-1.5 rounded-lg border border-rose-200">
|
||||
<AlertTriangle size={12} className="shrink-0 text-rose-600" />
|
||||
<div className="flex items-start gap-1.5 rounded-lg border border-rose-200 bg-rose-50 p-2 text-[11px] text-rose-700">
|
||||
<AlertTriangle size={14} className="shrink-0 text-rose-600 mt-0.5" />
|
||||
<span>
|
||||
میزان مصرف ({totC} {item.unit}) از سقف تخصیص ({alloc}) فراتر رفته است. مازاد (+{overAmt} {item.unit}) در استعلامهای آتی قابل رهگیری است.
|
||||
میزان مصرف (<strong className="tabular-nums">{totC}</strong> {item.unit}) از سقف مصوب (<strong className="tabular-nums">{alloc}</strong> {item.unit}) بیشتر است. (<span dir="ltr" className="font-bold">+{overAmt}</span> {item.unit} مازاد)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -773,7 +807,7 @@ export default function TaskDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ۲. نیروی انسانی تحویلشده و حاضر در کارگاه */}
|
||||
{/* نیروی انسانی */}
|
||||
<div className="mt-3 rounded-xl border border-sky-200 bg-sky-50/50 p-3 space-y-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-sky-950 flex items-center gap-1">
|
||||
@@ -862,7 +896,6 @@ export default function TaskDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ۳. توضیحات و شرح گزارش روزانه کارگاه */}
|
||||
<div className="mt-3 pt-2 border-t border-slate-200">
|
||||
<label className="text-[10px] font-medium text-slate-600 block mb-1">
|
||||
توضیحات و گزارش روزانه کارگاه (اختیاری)
|
||||
@@ -903,7 +936,7 @@ export default function TaskDetailPage() {
|
||||
)}
|
||||
</SectionCard>
|
||||
|
||||
{/* کارت تاریخچه ثبت پیشرفت و مصرف منابع */}
|
||||
{/* کارت تاریخچه ثبت پیشرفت */}
|
||||
{task.progress_logs && task.progress_logs.length > 0 && (
|
||||
<SectionCard title={`تاریخچه گزارشهای پیشرفت و مصرف منابع (${task.progress_logs.length})`}>
|
||||
<div className="space-y-2.5">
|
||||
@@ -921,11 +954,10 @@ export default function TaskDetailPage() {
|
||||
<div className="text-[10px] text-slate-500 flex items-center gap-1">
|
||||
<Clock size={11} className="text-slate-400" />
|
||||
<span>{relativeTime(log.created_at)}</span>
|
||||
{log.logged_by && <span>• توسط {log.logged_by}</span>}
|
||||
{log.logged_by && <span>توسط {log.logged_by}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* نیروی کار حاضر در این گزارش */}
|
||||
{log.human_resources_used && log.human_resources_used.length > 0 && (
|
||||
<div>
|
||||
<span className="text-[10px] font-bold text-slate-600 block mb-1">نیروی کار حاضر:</span>
|
||||
@@ -944,7 +976,6 @@ export default function TaskDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* مصالح مصرفی در این گزارش */}
|
||||
{log.materials_used && log.materials_used.length > 0 && (
|
||||
<div>
|
||||
<span className="text-[10px] font-bold text-slate-600 block mb-1">مصالح مصرفشده:</span>
|
||||
@@ -972,7 +1003,6 @@ export default function TaskDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* توضیحات در صورت وجود */}
|
||||
{log.description && (
|
||||
<div className="rounded-lg bg-white p-2 border border-slate-100 text-[11px] text-slate-700 leading-relaxed">
|
||||
{log.description}
|
||||
@@ -984,7 +1014,7 @@ export default function TaskDetailPage() {
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{/* کارت تاریخها — برنامهریزیشده و واقعی */}
|
||||
{/* کارت تاریخهای وظیفه */}
|
||||
<SectionCard title="تاریخهای وظیفه">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
@@ -1001,9 +1031,17 @@ export default function TaskDetailPage() {
|
||||
<JalaliDatePicker value={dueDate} onChange={setDueDate} placeholder="۱۴۰۵/۰۸/۲۶" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isDateRangeInvalid && (
|
||||
<div className="mt-2.5 flex items-center gap-2 rounded-xl border border-rose-200 bg-rose-50 p-2.5 text-xs font-bold text-rose-600">
|
||||
<AlertTriangle size={15} className="shrink-0 text-rose-500" />
|
||||
<span>تاریخ پایان نمیتواند کمتر از تاریخ شروع باشد</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={datesMutation.isPending}
|
||||
disabled={datesMutation.isPending || isDateRangeInvalid}
|
||||
onClick={() => datesMutation.mutate()}
|
||||
className={`${btnSecondary} mt-2.5 w-full`}
|
||||
>
|
||||
@@ -1063,8 +1101,6 @@ export default function TaskDetailPage() {
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
|
||||
|
||||
{/* کارت ارتباطات */}
|
||||
<SectionCard
|
||||
title={
|
||||
@@ -1215,7 +1251,6 @@ export default function TaskDetailPage() {
|
||||
onUpdated={() => invalidateTask()}
|
||||
/>
|
||||
|
||||
{/* مودال ویرایش منابع تخصیصیافته به وظیفه */}
|
||||
{showEditResourcesModal && (
|
||||
<Modal
|
||||
open={showEditResourcesModal}
|
||||
@@ -1248,7 +1283,6 @@ export default function TaskDetailPage() {
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* مودال افزودن رابطه وابستگی جدید */}
|
||||
{showAddRelationModal && (
|
||||
<Modal
|
||||
open={showAddRelationModal}
|
||||
@@ -1352,4 +1386,4 @@ export default function TaskDetailPage() {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user