Files
ERP/pm/frontend/src/components/ResourceSectionInputs.tsx
T
MMD 2747f9d4bb chore(pm): vendor the Aramesh/PM platform under pm/ (squash of Aramesh/PM @ 91f3271)
The PM panel (PmPanelPage -> iframe /pm/) needs the project-management
platform served at /pm/ on the ERP domain. Source: org repo Aramesh/PM,
branch feature/project-management-platform. Update via:
git subtree pull --prefix=pm aramesh-pm feature/project-management-platform --squash
2026-10-07 15:14:20 +03:30

195 lines
7.4 KiB
TypeScript

import { useState } from "react";
import { Plus, Trash2, Layers, X } from "lucide-react";
import type { HumanResourceItem, MaterialItem } from "../lib/api";
interface ResourceSectionInputsProps {
humanResources?: HumanResourceItem[];
setHumanResources?: (hrs: HumanResourceItem[]) => void;
materials: MaterialItem[];
setMaterials: (mats: MaterialItem[]) => void;
}
function TagInput({
tags,
onChange,
placeholder = "افزودن مشخصه...",
}: {
tags: string[];
onChange: (tags: string[]) => void;
placeholder?: string;
}) {
const [val, setVal] = useState("");
const addTag = () => {
const trimmed = val.trim();
if (trimmed && !tags.includes(trimmed)) {
onChange([...tags, trimmed]);
setVal("");
}
};
const removeTag = (index: number) => {
onChange(tags.filter((_, i) => i !== index));
};
return (
<div className="space-y-1.5">
<div className="flex gap-1">
<input
value={val}
onChange={(e) => setVal(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
addTag();
}
}}
placeholder={placeholder}
className="h-8 flex-1 rounded-lg border border-slate-300 bg-white px-2.5 text-xs text-slate-800 placeholder:text-slate-400 focus:border-sky-500 focus:outline-none focus:ring-1 focus:ring-sky-200"
/>
<button
type="button"
onClick={addTag}
className="inline-flex h-8 items-center justify-center rounded-lg bg-slate-100 px-2.5 text-xs font-semibold text-slate-700 hover:bg-slate-200 transition-colors"
>
<Plus size={14} />
</button>
</div>
{tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{tags.map((tag, i) => (
<span
key={i}
className="inline-flex items-center gap-1 rounded-md bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-700 border border-slate-200"
>
{tag}
<button
type="button"
onClick={() => removeTag(i)}
className="text-slate-400 hover:text-rose-600 transition-colors"
aria-label="حذف مشخصه"
>
<X size={12} />
</button>
</span>
))}
</div>
)}
</div>
);
}
export function ResourceSectionInputs({
materials,
setMaterials,
}: ResourceSectionInputsProps) {
// --- Materials handlers ---
const addMaterial = () => {
setMaterials([...materials, { name: "", unit: "", specifications: [], count: 1 }]);
};
const updateMaterial = (index: number, field: keyof MaterialItem, value: any) => {
const next = [...materials];
next[index] = { ...next[index], [field]: value };
setMaterials(next);
};
const removeMaterial = (index: number) => {
setMaterials(materials.filter((_, i) => i !== index));
};
return (
<div className="rounded-xl border border-slate-200 bg-slate-50/60 p-3 space-y-3">
<div className="flex items-center justify-between border-b border-slate-200 pb-2">
<span className="text-xs font-bold text-slate-700 flex items-center gap-1.5">
<Layers size={14} className="text-amber-600" />
<span>مصالح و تجهیزات وظیفه</span>
</span>
{materials.length > 0 && (
<span className="rounded-full bg-amber-100 text-amber-700 px-2 py-0.5 text-[10px] font-bold">
{materials.length} قلم
</span>
)}
</div>
{/* --- Materials & Equipment Content --- */}
<div className="space-y-2.5">
{materials.length === 0 ? (
<p className="py-2 text-center text-xs text-slate-400">
هیچ مصالح یا تجهیزاتی تعریف نشده است.
</p>
) : (
materials.map((m, i) => (
<div
key={i}
className="rounded-lg border border-slate-200 bg-white p-2.5 shadow-sm space-y-2"
>
<div className="flex items-start gap-2">
<div className="flex-1">
<label className="mb-0.5 block text-[10px] font-medium text-slate-500">نام مصالح، کالا یا تجهیزات</label>
<input
value={m.name}
onChange={(e) => updateMaterial(i, "name", e.target.value)}
placeholder="مثلاً: سیمان تیپ ۲، میلگرد ۱۶، پمپ بتن، تاور کرین"
className="h-8 w-full rounded-md border border-slate-300 px-2 text-xs text-slate-800 focus:border-amber-500 focus:outline-none"
/>
</div>
<div className="w-24">
<label className="mb-0.5 block text-[10px] font-medium text-slate-500">واحد سنجش</label>
<input
value={m.unit || ""}
onChange={(e) => updateMaterial(i, "unit", e.target.value)}
placeholder="تن، متر، دستگاه، شاخه"
className="h-8 w-full rounded-md border border-slate-300 px-2 text-xs text-slate-800 focus:border-amber-500 focus:outline-none"
/>
</div>
<div className="w-20">
<label className="mb-0.5 block text-[10px] font-medium text-slate-500">مقدار</label>
<input
type="number"
step="any"
min={0}
value={m.count}
onChange={(e) => updateMaterial(i, "count", Math.max(0, parseFloat(e.target.value) || 0))}
className="h-8 w-full rounded-md border border-slate-300 px-2 text-xs text-slate-800 focus:border-amber-500 focus:outline-none"
/>
</div>
<button
type="button"
onClick={() => removeMaterial(i)}
className="mt-3.5 flex h-8 w-8 items-center justify-center rounded-md text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-colors"
title="حذف این ردیف"
>
<Trash2 size={15} />
</button>
</div>
<div>
<label className="mb-0.5 block text-[10px] font-medium text-slate-500">مشخصه‌ها، استانداردها و مدل</label>
<TagInput
tags={m.specifications || []}
onChange={(tags) => updateMaterial(i, "specifications", tags)}
placeholder="مثال: استاندارد A3، عیار ۳۵۰، بوم ۳۶ متری (Enter بزنید)"
/>
</div>
</div>
))
)}
<button
type="button"
onClick={addMaterial}
className="inline-flex h-8 w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-amber-300 bg-amber-50/60 text-xs font-semibold text-amber-800 hover:bg-amber-100/70 transition-colors"
>
<Plus size={14} />
<span>افزودن مصالح و تجهیزات</span>
</button>
</div>
</div>
);
}