ColumnConfigModal.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import { useState, useEffect, useRef } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { GripVertical, Eye, EyeOff, ChevronUp, ChevronDown, RotateCcw } from 'lucide-react';
  4. import { Card, CardContent } from './Card';
  5. import { Button } from './Button';
  6. export interface ColumnConfig {
  7. id: string;
  8. label: string;
  9. visible: boolean;
  10. }
  11. interface ColumnConfigModalProps {
  12. isOpen: boolean;
  13. onClose: () => void;
  14. columns: ColumnConfig[];
  15. defaultColumns: ColumnConfig[];
  16. onSave: (columns: ColumnConfig[]) => void;
  17. }
  18. export function ColumnConfigModal({ isOpen, onClose, columns, defaultColumns, onSave }: ColumnConfigModalProps) {
  19. const { t } = useTranslation();
  20. const [localColumns, setLocalColumns] = useState<ColumnConfig[]>(columns);
  21. const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
  22. const draggedIndexRef = useRef<number | null>(null);
  23. useEffect(() => {
  24. if (isOpen) {
  25. setLocalColumns(columns.map((c) => ({ ...c })));
  26. }
  27. }, [isOpen, columns]);
  28. useEffect(() => {
  29. if (!isOpen) return;
  30. const handleKeyDown = (e: KeyboardEvent) => {
  31. if (e.key === 'Escape') onClose();
  32. };
  33. window.addEventListener('keydown', handleKeyDown);
  34. return () => window.removeEventListener('keydown', handleKeyDown);
  35. }, [isOpen, onClose]);
  36. if (!isOpen) return null;
  37. const toggleVisibility = (index: number) => {
  38. setLocalColumns((prev) =>
  39. prev.map((col, i) => (i === index ? { ...col, visible: !col.visible } : col))
  40. );
  41. };
  42. const moveColumn = (fromIndex: number, toIndex: number) => {
  43. if (toIndex < 0 || toIndex >= localColumns.length) return;
  44. setLocalColumns((prev) => {
  45. const newColumns = [...prev];
  46. const [moved] = newColumns.splice(fromIndex, 1);
  47. newColumns.splice(toIndex, 0, moved);
  48. return newColumns;
  49. });
  50. };
  51. const handleDragStart = (e: React.DragEvent, index: number) => {
  52. draggedIndexRef.current = index;
  53. setDraggedIndex(index);
  54. e.dataTransfer.effectAllowed = 'move';
  55. };
  56. const handleDragOver = (e: React.DragEvent, index: number) => {
  57. e.preventDefault();
  58. e.dataTransfer.dropEffect = 'move';
  59. const from = draggedIndexRef.current;
  60. if (from !== null && from !== index) {
  61. moveColumn(from, index);
  62. draggedIndexRef.current = index;
  63. setDraggedIndex(index);
  64. }
  65. };
  66. const handleDrop = (e: React.DragEvent) => {
  67. e.preventDefault();
  68. };
  69. const handleDragEnd = () => {
  70. draggedIndexRef.current = null;
  71. setDraggedIndex(null);
  72. };
  73. const resetToDefaults = () => {
  74. setLocalColumns(defaultColumns.map((c) => ({ ...c })));
  75. };
  76. const handleSave = () => {
  77. onSave(localColumns);
  78. onClose();
  79. };
  80. const visibleCount = localColumns.filter((c) => c.visible).length;
  81. return (
  82. <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={onClose}>
  83. <Card className="w-full max-w-md max-h-[80vh] flex flex-col" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
  84. <CardContent className="p-6 flex flex-col min-h-0">
  85. {/* Header */}
  86. <h3 className="text-lg font-semibold text-white mb-2">{t('inventory.configureColumns')}</h3>
  87. <p className="text-sm text-bambu-gray mb-4">
  88. {t('inventory.configureColumnsDesc')}
  89. <span className="ml-2 text-bambu-gray/60">
  90. ({visibleCount} {t('inventory.of')} {localColumns.length} {t('inventory.visible')})
  91. </span>
  92. </p>
  93. {/* Column list */}
  94. <div className="space-y-1 overflow-y-auto flex-1 min-h-0 pr-1">
  95. {localColumns.map((column, index) => (
  96. <div
  97. key={column.id}
  98. className={`flex items-center gap-2 p-2 rounded-lg border transition-colors ${
  99. draggedIndex === index
  100. ? 'border-bambu-green bg-bambu-green/10'
  101. : 'border-bambu-dark-tertiary bg-bambu-dark-tertiary/50'
  102. } ${!column.visible ? 'opacity-50' : ''}`}
  103. draggable
  104. onDragStart={(e) => handleDragStart(e, index)}
  105. onDragOver={(e) => handleDragOver(e, index)}
  106. onDrop={handleDrop}
  107. onDragEnd={handleDragEnd}
  108. >
  109. {/* Drag Handle */}
  110. <div className="cursor-grab text-bambu-gray/50 hover:text-bambu-gray">
  111. <GripVertical className="w-4 h-4" />
  112. </div>
  113. {/* Column Name */}
  114. <span className="flex-1 font-medium text-sm text-white">{column.label}</span>
  115. {/* Move Buttons */}
  116. <div className="flex items-center gap-0.5">
  117. <button
  118. onClick={() => moveColumn(index, index - 1)}
  119. disabled={index === 0}
  120. className="p-1 rounded text-bambu-gray hover:bg-bambu-dark-secondary disabled:opacity-30 disabled:cursor-not-allowed"
  121. title={t('inventory.moveUp')}
  122. >
  123. <ChevronUp className="w-4 h-4" />
  124. </button>
  125. <button
  126. onClick={() => moveColumn(index, index + 1)}
  127. disabled={index === localColumns.length - 1}
  128. className="p-1 rounded text-bambu-gray hover:bg-bambu-dark-secondary disabled:opacity-30 disabled:cursor-not-allowed"
  129. title={t('inventory.moveDown')}
  130. >
  131. <ChevronDown className="w-4 h-4" />
  132. </button>
  133. </div>
  134. {/* Visibility Toggle */}
  135. <button
  136. onClick={() => toggleVisibility(index)}
  137. className={`p-1.5 rounded transition-colors ${
  138. column.visible
  139. ? 'text-bambu-green hover:bg-bambu-green/10'
  140. : 'text-bambu-gray/50 hover:bg-bambu-dark-secondary'
  141. }`}
  142. title={column.visible ? t('inventory.hideColumn') : t('inventory.showColumn')}
  143. >
  144. {column.visible ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
  145. </button>
  146. </div>
  147. ))}
  148. </div>
  149. {/* Footer */}
  150. <div className="flex items-center gap-3 mt-4 pt-4 border-t border-bambu-dark-tertiary">
  151. <Button variant="secondary" onClick={resetToDefaults} className="mr-auto">
  152. <RotateCcw className="w-4 h-4" />
  153. {t('inventory.reset')}
  154. </Button>
  155. <Button variant="secondary" onClick={onClose}>
  156. {t('inventory.cancel')}
  157. </Button>
  158. <Button onClick={handleSave}>
  159. {t('inventory.applyChanges')}
  160. </Button>
  161. </div>
  162. </CardContent>
  163. </Card>
  164. </div>
  165. );
  166. }