File size: 9,846 Bytes
33d9e63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/**
 * @license
 * SPDX-License-Identifier: Apache-2.0
 */

import React from 'react';
import { Project } from '../../types';
import { projectService } from '../../services/projectService';
import { Search, Plus, Calendar, Users, Filter, BarChart2 } from 'lucide-react';

interface OpsProjectsListProps {
  onSelectProject: (projectId: string, tab: 'dashboard' | 'survey' | 'distribution') => void;
  onNavigateToCreateProject: () => void;
}

export const OpsProjectsList: React.FC<OpsProjectsListProps> = ({
  onSelectProject,
  onNavigateToCreateProject,
}) => {
  const [projects, setProjects] = React.useState<Project[]>([]);
  const [searchQuery, setSearchQuery] = React.useState('');
  const [statusFilter, setStatusFilter] = React.useState<string>('all');

  React.useEffect(() => {
    const loadAndSync = async () => {
      setProjects(projectService.getProjects());
      try {
        await projectService.syncFromBackend();
        setProjects(projectService.getProjects());
      } catch (err) {
        console.error('Failed to sync projects on list load', err);
      }
    };
    loadAndSync();
  }, []);

  const handleDeleteProject = async (id: string, e: React.MouseEvent) => {
    e.stopPropagation();
    if (window.confirm('정말로 이 프로젝트를 삭제하시겠습니까? 관련 모든 조직도 및 응답 정보가 지워집니다.')) {
      try {
        await projectService.deleteProject(id);
        const remaining = projects.filter((p) => p.id !== id);
        setProjects(remaining);
      } catch (err) {
        alert('프로젝트 삭제에 실패했습니다.');
      }
    }
  };

  // Filter projects
  const filteredProjects = projects.filter((proj) => {
    const matchesSearch =
      proj.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
      proj.company_info.name.toLowerCase().includes(searchQuery.toLowerCase());

    const matchesStatus = statusFilter === 'all' || proj.status === statusFilter;

    return matchesSearch && matchesStatus;
  });

  return (
    <div className="space-y-6 font-sans antialiased" id="ops-projects-list-view">
      {/* Top Header Controls */}
      <div className="flex flex-col md:flex-row md:items-center justify-between gap-4" id="projects-list-header">
        <div>
          <h2 className="text-lg font-bold text-slate-900 tracking-tight">전체 프로젝트 목록</h2>
          <p className="text-sm text-slate-400 mt-1">
            등록된 모든 조직 네트워크 분석 진단 목록입니다. 검색 및 상태 필터를 지원합니다.
          </p>
        </div>
        <button
          onClick={onNavigateToCreateProject}
          className="inline-flex items-center gap-1.5 px-4 py-2 bg-orange-500 hover:bg-orange-600 active:bg-orange-700 text-white text-sm font-bold rounded-lg transition-colors cursor-pointer"
          id="create-project-btn"
        >
          <Plus className="w-4 h-4" />
          새 프로젝트 생성
        </button>
      </div>

      {/* Filter and Search Bar Row */}
      <div
        className="bg-white p-4 rounded-xl border border-slate-100 flex flex-col md:flex-row gap-4"
        id="projects-filter-bar"
      >
        {/* Search */}
        <div className="relative flex-1">
          <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
          <input
            type="text"
            placeholder="프로젝트명 또는 고객사명 검색..."
            value={searchQuery}
            onChange={(e) => setSearchQuery(e.target.value)}
            className="w-full pl-9 pr-4 py-2 text-sm border border-slate-200 focus:bg-white focus:ring-2 focus:ring-orange-500/10 focus:border-orange-500 rounded-lg transition-all outline-none"
            id="projects-search-input"
          />
        </div>

        {/* Filter */}
        <div className="flex items-center gap-2" id="status-filter-wrapper">
          <Filter className="w-4 h-4 text-slate-400 shrink-0" />
          <select
            value={statusFilter}
            onChange={(e) => setStatusFilter(e.target.value)}
            className="px-3 py-2 text-sm border border-slate-200 bg-white rounded-lg outline-none focus:ring-2 focus:ring-orange-500/10 focus:border-orange-500 font-medium"
            id="projects-status-select"
          >
            <option value="all">모든 상태</option>
            <option value="draft">초안 (Draft)</option>
            <option value="collecting">수집 중 (Collecting)</option>
            <option value="closed">마감됨 (Closed)</option>
          </select>
        </div>
      </div>

      {/* Projects Grid List */}
      {filteredProjects.length === 0 ? (
        <div
          className="bg-white p-16 text-center border border-dashed border-slate-200 rounded-xl"
          id="no-projects-view"
        >
          <p className="text-sm text-slate-400">검색 조건에 맞는 진단 프로젝트가 없습니다.</p>
        </div>
      ) : (
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4" id="projects-grid">
          {filteredProjects.map((proj) => {
            const totalEmps = proj.employees.length;
            const submitted = proj.employees.filter((e) => e.response_status === 'submitted').length;
            const progressPct = totalEmps > 0 ? Math.round((submitted / totalEmps) * 100) : 0;

            return (
              <div
                key={proj.id}
                onClick={() => onSelectProject(proj.id, 'dashboard')}
                className="bg-white border border-slate-100 hover:border-orange-200 rounded-xl p-5 transition-all cursor-pointer flex flex-col justify-between gap-4 group"
                id={`project-item-${proj.id}`}
              >
                <div className="space-y-2">
                  <div className="flex justify-between items-start gap-4">
                    <div className="space-y-0.5">
                      <span className="text-sm font-bold text-slate-400 block tracking-wide">
                        {proj.company_info.name}
                      </span>
                      <h3 className="text-base font-bold text-slate-900 line-clamp-1 group-hover:text-orange-600 transition-colors">
                        {proj.title}
                      </h3>
                    </div>
                    <span
                      className={`px-1.5 py-0.5 text-sm font-bold rounded-sm uppercase shrink-0 border ${proj.status === 'collecting'
                        ? 'bg-orange-50 text-orange-600 border-orange-100'
                        : proj.status === 'draft'
                          ? 'bg-gray-50 text-gray-500 border-gray-100'
                          : 'bg-slate-50 text-slate-700 border-slate-150'
                        }`}
                    >
                      {proj.status === 'collecting'
                        ? '수집 중'
                        : proj.status === 'draft'
                          ? '초안'
                          : '마감됨'}
                    </span>
                  </div>

                  <div className="flex items-center gap-3 text-sm text-slate-400 pt-1">
                    <span className="flex items-center gap-1">
                      <Calendar className="w-3.5 h-3.5 text-slate-300" />
                      {proj.start_date} ~ {proj.end_date}
                    </span>
                    <span className="flex items-center gap-1">
                      <Users className="w-3.5 h-3.5 text-slate-300" />
                      {totalEmps}명 대상
                    </span>
                  </div>
                </div>

                {/* Progress bar info */}
                <div className="space-y-1.5 border-t border-slate-100 pt-3">
                  <div className="flex justify-between text-sm font-semibold text-slate-500">
                    <span>진행률</span>
                    <span>
                      {submitted}/{totalEmps}명 ({progressPct}%)
                    </span>
                  </div>
                  <div className="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden">
                    <div
                      className="bg-orange-500 h-1.5 rounded-full transition-all duration-500"
                      style={{ width: `${progressPct}%` }}
                    />
                  </div>
                </div>

                {/* Foot card actions */}
                <div className="flex justify-between items-center pt-1">
                  <span className="text-sm text-slate-400">
                    ID: <code className="font-mono text-sm text-slate-600">{proj.survey_login_id}</code>
                  </span>
                  <div className="flex gap-2">
                    <button
                      onClick={(e) => handleDeleteProject(proj.id, e)}
                      className="px-2 py-1 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-md text-sm font-semibold cursor-pointer"
                      id={`project-delete-${proj.id}`}
                    >
                      삭제
                    </button>
                    <button
                      onClick={(e) => {
                        e.stopPropagation();
                        onSelectProject(proj.id, 'dashboard');
                      }}
                      className="px-2.5 py-1 bg-orange-50 text-orange-600 group-hover:bg-orange-500 group-hover:text-white rounded-md text-sm font-bold transition-all inline-flex items-center gap-1 cursor-pointer"
                      id={`project-open-${proj.id}`}
                    >
                      <BarChart2 className="w-3.5 h-3.5" />
                      상세 보기
                    </button>
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
};