Files
aeroresolve_frontend/src/app/configuration/actionBuilder/components/ConfigurationFieldsColumn.tsx
T

277 lines
11 KiB
TypeScript

import { useState } from 'react';
import {
PlusIcon,
MagnifyingGlassIcon,
PencilSimpleLineIcon,
TrashIcon,
DotsSixVerticalIcon,
} from '@phosphor-icons/react';
import { CustomInput, CustomButton, Skeleton } from '../../../../components/custom';
import type { FieldDefinition } from '../ActionBuilderTypes';
interface ConfigurationFieldsColumnProps {
fields: FieldDefinition[];
selectedActionTypeId: string;
loading: boolean;
onOpenAdd: () => void;
onOpenEdit: (field: FieldDefinition) => void;
onDelete: (field: FieldDefinition) => void;
onReorderFields?: (fromIndex: number, toIndex: number) => void;
}
export function ConfigurationFieldsColumn({
fields,
selectedActionTypeId,
loading,
onOpenAdd,
onOpenEdit,
onDelete,
onReorderFields,
}: ConfigurationFieldsColumnProps) {
const [search, setSearch] = useState('');
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
const filteredFields = fields.filter(
(f) =>
f.fieldName.toLowerCase().includes(search.toLowerCase()) ||
f.fieldCode.toLowerCase().includes(search.toLowerCase()) ||
(f.section || '').toLowerCase().includes(search.toLowerCase())
);
const fieldNameByCode = new Map(fields.map((f) => [f.fieldCode, f.fieldName]));
return (
<div className="bg-[#FFFFFF] p-4 rounded-[20px] border border-gray-200/70 shadow-sm space-y-4 flex flex-col min-h-[600px]">
{/* Column Header */}
<div className="flex items-center justify-between">
<h3 className="text-[16px] font-bold text-[#0F172B]">Configuration Field</h3>
<CustomButton
variant="outlined"
size="sm"
onClick={onOpenAdd}
disabled={!selectedActionTypeId}
leftIcon={<PlusIcon size={15} weight="bold" />}
>
Add
</CustomButton>
</div>
{/* Search Input */}
<div className="relative">
<CustomInput
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search by name or code..."
leftIcon={<MagnifyingGlassIcon size={16} className="text-slate-400" />}
className="!bg-[#F3F6F5] !rounded-[12px] !h-[40px] !border-none !text-[13px]"
containerClassName="!gap-0"
/>
</div>
{/* Field Cards List */}
<div className="space-y-3 flex-1 overflow-y-auto max-h-[580px] pr-0.5">
{loading ? (
<div className="flex flex-col gap-3">
<Skeleton width="100%" height={100} className="rounded-[16px]" />
<Skeleton width="100%" height={100} className="rounded-[16px]" />
<Skeleton width="100%" height={100} className="rounded-[16px]" />
</div>
) : !selectedActionTypeId ? (
<div className="py-12 text-center text-slate-400 text-[13px]">
Select an Action Type to view configuration fields.
</div>
) : filteredFields.length === 0 ? (
<div className="py-12 text-center text-slate-400 text-[13px]">
No fields configured for this action type yet. Click "+ Add" above to create one.
</div>
) : (
filteredFields.map((f, idx) => {
const hasVisibility = f.visibilityConditionJson?.field && f.visibilityConditionJson?.value;
const depFieldName = hasVisibility
? fieldNameByCode.get(f.visibilityConditionJson!.field) || f.visibilityConditionJson!.field
: 'Field Name';
const minVal = f.validationJson?.min;
const maxVal = f.validationJson?.max;
const hasMinMax = minVal !== undefined || maxVal !== undefined;
const isDragging = draggedIndex === idx;
const isDragOver = dragOverIndex === idx;
return (
<div
key={f.id}
draggable
onDragStart={(e) => {
setDraggedIndex(idx);
e.dataTransfer.setData('text/plain', String(idx));
e.dataTransfer.effectAllowed = 'move';
}}
onDragOver={(e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (dragOverIndex !== idx) {
setDragOverIndex(idx);
}
}}
onDragLeave={() => {
if (dragOverIndex === idx) {
setDragOverIndex(null);
}
}}
onDrop={(e) => {
e.preventDefault();
if (draggedIndex !== null && draggedIndex !== idx) {
const fromOriginalIndex = fields.findIndex((item) => item.id === filteredFields[draggedIndex].id);
const toOriginalIndex = fields.findIndex((item) => item.id === f.id);
if (fromOriginalIndex !== -1 && toOriginalIndex !== -1) {
onReorderFields?.(fromOriginalIndex, toOriginalIndex);
}
}
setDraggedIndex(null);
setDragOverIndex(null);
}}
onDragEnd={() => {
setDraggedIndex(null);
setDragOverIndex(null);
}}
className={`bg-[#EFF3F7] rounded-[16px] border border-[#E2E8F0] overflow-hidden flex items-stretch transition-all duration-150 ${isDragOver ? 'ring-2 ring-[#1E7D5C] scale-[1.01]' : 'hover:border-slate-300'
} ${isDragging ? 'opacity-40' : ''}`}
>
{/* Left Drag Handle Bar */}
<div
className="w-8 flex items-center justify-center text-slate-400 hover:text-slate-700 cursor-grab active:cursor-grabbing flex-shrink-0 select-none"
title="Drag to reorder"
>
<DotsSixVerticalIcon size={18} weight="bold" />
</div>
{/* Inner White Card Content */}
<div className="bg-white flex-1 p-4 rounded-r-[15px] border-l border-[#E2E8F0] space-y-3 min-w-0">
{/* Top Header Row */}
<div className="flex items-start justify-between gap-2">
<div>
<div className="font-bold text-[15px] text-[#0F172B] leading-tight">
{f.fieldName}
</div>
<div className="font-mono text-[12px] text-[#8C9AA8] uppercase tracking-wider mt-0.5">
{f.fieldCode}
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{/* Required Badge */}
{f.isRequired && (
<span className="bg-[#FEECEB] text-[#D9383A] text-[12px] font-medium px-3 py-1 rounded-full">
Required
</span>
)}
{/* Active Badge */}
<span
className={`text-[12px] font-medium px-3 py-1 rounded-full ${f.isActive !== false
? 'bg-[#E6F7ED] text-[#1E7D5C]'
: 'bg-slate-100 text-slate-500'
}`}
>
{f.isActive !== false ? 'Active' : 'Inactive'}
</span>
{/* Divider */}
<span className="text-slate-300 font-light mx-0.5">|</span>
{/* Action Buttons */}
<button
type="button"
onClick={() => onOpenEdit(f)}
title="Edit Field"
className="p-1 text-slate-800 hover:text-[#1E7D5C] transition-colors cursor-pointer"
>
<PencilSimpleLineIcon size={18} weight="bold" />
</button>
<button
type="button"
onClick={() => onDelete(f)}
title="Delete Field"
className="p-1 text-[#D9383A] hover:text-red-700 transition-colors cursor-pointer"
>
<TrashIcon size={18} weight="bold" />
</button>
</div>
</div>
<div className="border-b border-slate-100 my-2" />
{/* Details Grid (2 columns x 2 rows) */}
<div className="grid grid-cols-2 gap-y-3 gap-x-4">
<div>
<div className="text-[11px] font-bold text-[#8C9AA8] uppercase tracking-wider">
CONTROL TYPE
</div>
<div className="text-[13px] font-bold text-[#0F172B] mt-0.5">
{f.fieldType}
</div>
</div>
<div>
<div className="text-[11px] font-bold text-[#8C9AA8] uppercase tracking-wider">
WIDTH
</div>
<div className="text-[13px] font-bold text-[#0F172B] mt-0.5">
{f.width ? `${f.width} width` : 'full width'}
</div>
</div>
<div>
<div className="text-[11px] font-bold text-[#8C9AA8] uppercase tracking-wider">
MASTER LOOKUP
</div>
<div className="text-[13px] font-bold text-[#0F172B] mt-0.5">
{f.lookupSource || '--'}
</div>
</div>
<div>
<div className="text-[11px] font-bold text-[#8C9AA8] uppercase tracking-wider">
SECTION
</div>
<div className="text-[13px] font-medium text-[#5A6E85] mt-0.5">
{f.section || 'General Information'}
</div>
</div>
</div>
{/* Visibility Condition Banner & Min/Max Footer */}
<div className="pt-2 flex items-end justify-between gap-3">
{hasVisibility ? (
<div className="bg-[#FFF8E6] border border-[#FDE6B8] text-[#B45309] rounded-[10px] px-3.5 py-2 text-[12px] space-y-0.5 max-w-[70%]">
<div className="font-medium text-[#B45309]">Visible if {depFieldName} =</div>
<div className="font-bold text-[#B45309] uppercase break-all">
{String(f.visibilityConditionJson?.value)}
</div>
</div>
) : (
<div />
)}
{hasMinMax && (
<div className="text-right text-[12px] font-medium text-[#5A6E85] ml-auto">
Min: {minVal ?? '—'} | Max: {maxVal ?? '—'}
</div>
)}
</div>
</div>
</div>
);
})
)}
</div>
</div>
);
}