import React, { useState, useEffect, createContext, useContext } from 'react';
import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth';
import { getFirestore, doc, getDoc, addDoc, setDoc, updateDoc, deleteDoc, onSnapshot, collection, query, where, getDocs } from 'firebase/firestore';
// Контекст для аутентификации и данных Firebase
const AuthContext = createContext(null);
// Компонент-провайдер аутентификации
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [db, setDb] = useState(null);
const [auth, setAuth] = useState(null);
const [userId, setUserId] = useState(null);
useEffect(() => {
// Инициализация Firebase
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {};
if (Object.keys(firebaseConfig).length > 0) {
const app = initializeApp(firebaseConfig);
const firestore = getFirestore(app);
const firebaseAuth = getAuth(app);
setDb(firestore);
setAuth(firebaseAuth);
// Слушатель изменений состояния аутентификации
const unsubscribe = onAuthStateChanged(firebaseAuth, async (currentUser) => {
if (currentUser) {
setUser(currentUser);
setUserId(currentUser.uid);
} else {
// Если пользователь не аутентифицирован, попробуйте войти анонимно
try {
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(firebaseAuth, __initial_auth_token);
} else {
await signInAnonymously(firebaseAuth);
}
} catch (error) {
console.error("Ошибка анонимной аутентификации:", error);
}
}
setLoading(false);
});
return () => unsubscribe(); // Отписка при размонтировании компонента
} else {
console.warn("Firebase config не предоставлен. Аутентификация и Firestore не будут работать.");
setLoading(false);
}
}, []);
if (loading) {
return (
);
}
return (
{children}
);
}
// Хук для использования контекста аутентификации
function useAuth() {
return useContext(AuthContext);
}
// Компонент для отображения сообщений (замена alert)
function MessageBox({ message, onClose }) {
if (!message) return null;
return (
);
}
// Модели данных (представляют структуру листов Google Таблиц)
// В реальном приложении эти данные будут загружаться из вашего бэкенда,
// который взаимодействует с Google Sheets API.
// Модель Проекта
class Project {
constructor({ id, name, manager, customer, startDate, endDate, status, budget, description, category, priority }) {
this.id = id || `proj-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Автогенерация ID
this.name = name;
this.manager = manager;
this.customer = customer;
this.startDate = startDate;
this.endDate = endDate;
this.status = status;
this.budget = budget;
this.description = description;
this.category = category;
this.priority = priority;
}
}
// Модель Задачи
class Task {
constructor({ id, projectId, parentTaskId, name, description, responsible, executors, plannedStartDate, plannedEndDate, actualStartDate, actualEndDate, status, priority, plannedEffort, actualEffort, progress }) {
this.id = id || `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Автогенерация ID
this.projectId = projectId;
this.parentTaskId = parentTaskId; // Для иерархии WBS
this.name = name;
this.description = description;
this.responsible = responsible;
this.executors = executors || []; // Массив исполнителей
this.plannedStartDate = plannedStartDate;
this.plannedEndDate = plannedEndDate;
this.actualStartDate = actualStartDate;
this.actualEndDate = actualEndDate;
this.status = status;
this.priority = priority;
this.plannedEffort = plannedEffort; // Плановые трудозатраты (человеко-часы)
this.actualEffort = actualEffort; // Фактические трудозатраты (человеко-часы)
this.progress = progress || 0; // Прогресс выполнения в %
}
}
// --- Имитация API для Google Таблиц ---
// В реальном приложении эти функции будут делать fetch-запросы к вашему бэкенду,
// который будет использовать Google Sheets API (например, через Google Service Account).
// Бэкенд будет обрабатывать аутентификацию, авторизацию и все операции с таблицами.
const MOCK_PROJECTS_DATA = [
new Project({
id: 'proj-1',
name: 'Разработка CRM системы',
manager: 'Иванов И.И.',
customer: 'ООО "Альфа"',
startDate: '2024-01-15',
endDate: '2024-06-30',
status: 'В работе',
budget: 1500000,
description: 'Разработка новой CRM системы для автоматизации продаж.',
category: 'ИТ',
priority: 'Высокий'
}),
new Project({
id: 'proj-2',
name: 'Внедрение ERP системы',
manager: 'Петров П.П.',
customer: 'ЗАО "Бета"',
startDate: '2024-03-01',
endDate: '2024-12-31',
status: 'Планируется',
budget: 2500000,
description: 'Внедрение комплексной ERP системы для управления ресурсами предприятия.',
category: 'Автоматизация',
priority: 'Высокий'
}),
];
const MOCK_TASKS_DATA = [
new Task({
id: 'task-1',
projectId: 'proj-1',
name: 'Сбор требований',
description: 'Сбор и анализ функциональных и нефункциональных требований.',
responsible: 'Сидорова А.В.',
executors: ['Сидорова А.В.', 'Козлов Д.С.'],
plannedStartDate: '2024-01-15',
plannedEndDate: '2024-01-31',
status: 'Завершено',
priority: 'Высокий',
plannedEffort: 80,
actualEffort: 75,
progress: 100
}),
new Task({
id: 'task-2',
projectId: 'proj-1',
parentTaskId: 'task-1', // Подзадача
name: 'Разработка ТЗ',
description: 'Разработка технического задания на основе собранных требований.',
responsible: 'Сидорова А.В.',
executors: ['Сидорова А.В.'],
plannedStartDate: '2024-02-01',
plannedEndDate: '2024-02-15',
status: 'В работе',
priority: 'Высокий',
plannedEffort: 60,
actualEffort: 30,
progress: 50
}),
new Task({
id: 'task-3',
projectId: 'proj-1',
name: 'Проектирование архитектуры',
description: 'Разработка архитектуры CRM системы.',
responsible: 'Козлов Д.С.',
executors: ['Козлов Д.С.'],
plannedStartDate: '2024-02-01',
plannedEndDate: '2024-02-29',
status: 'В работе',
priority: 'Высокий',
plannedEffort: 120,
actualEffort: 40,
progress: 30
}),
new Task({
id: 'task-4',
projectId: 'proj-2',
name: 'Анализ бизнес-процессов',
description: 'Анализ текущих бизнес-процессов для внедрения ERP.',
responsible: 'Новикова Е.М.',
executors: ['Новикова Е.М.', 'Федоров Г.В.'],
plannedStartDate: '2024-03-01',
plannedEndDate: '2024-03-31',
status: 'Планируется',
priority: 'Высокий',
plannedEffort: 100,
actualEffort: 0,
progress: 0
}),
];
const mockApi = {
// Имитация чтения проектов из Google Таблиц (лист "Projects")
getProjects: async () => {
return new Promise(resolve => setTimeout(() => resolve(MOCK_PROJECTS_DATA), 500));
},
// Имитация добавления проекта в Google Таблицы (лист "Projects")
addProject: async (projectData) => {
const newProject = new Project(projectData);
MOCK_PROJECTS_DATA.push(newProject);
return new Promise(resolve => setTimeout(() => resolve(newProject), 500));
},
// Имитация чтения задач из Google Таблиц (лист "Tasks") по ProjectID
getTasksByProjectId: async (projectId) => {
const projectTasks = MOCK_TASKS_DATA.filter(task => task.projectId === projectId);
return new Promise(resolve => setTimeout(() => resolve(projectTasks), 500));
},
// Имитация добавления задачи в Google Таблицы (лист "Tasks")
addTask: async (taskData) => {
const newTask = new Task(taskData);
MOCK_TASKS_DATA.push(newTask);
return new Promise(resolve => setTimeout(() => resolve(newTask), 500));
},
};
// --- Компоненты UI ---
// Компонент "Панель управления (Дашборд)"
function Dashboard() {
const { userId } = useAuth();
return (
Панель управления
Добро пожаловать в интегрированное приложение для проектного офиса!
Ваш ID пользователя: {userId || 'Неизвестен'}
Здесь будет отображаться агрегированная информация о проектах, задачах, ресурсах и KPI.
Данные будут автоматически обновляться из Google Таблиц.
);
}
// Компонент "Реестр проектов"
function ProjectRegister({ onSelectProject }) {
const [projects, setProjects] = useState([]);
const [showAddForm, setShowAddForm] = useState(false);
const [message, setMessage] = useState('');
useEffect(() => {
// Загрузка проектов при монтировании компонента
fetchProjects();
}, []);
const fetchProjects = async () => {
try {
const data = await mockApi.getProjects();
setProjects(data);
} catch (error) {
console.error("Ошибка при загрузке проектов:", error);
setMessage("Не удалось загрузить проекты.");
}
};
const handleAddProject = async (newProjectData) => {
try {
await mockApi.addProject(newProjectData);
setMessage('Проект успешно добавлен!');
setShowAddForm(false);
fetchProjects(); // Обновить список проектов
} catch (error) {
console.error("Ошибка при добавлении проекта:", error);
setMessage("Не удалось добавить проект.");
}
};
return (
Реестр проектов
setMessage('')} />
{showAddForm && (
setShowAddForm(false)} />
)}
{projects.length === 0 ? (
Проекты не найдены.
) : (
Название проекта | Руководитель | Статус | Бюджет | Действия |
{projects.map((project) => (
{project.name} | {project.manager} |
{project.status}
| {project.budget.toLocaleString()} | |
))}
)}
);
}
// Форма для добавления/редактирования проекта
function ProjectForm({ onSubmit, onCancel, initialData = {} }) {
const [formData, setFormData] = useState({
name: initialData.name || '',
manager: initialData.manager || '',
customer: initialData.customer || '',
startDate: initialData.startDate || '',
endDate: initialData.endDate || '',
status: initialData.status || 'Планируется',
budget: initialData.budget || '',
description: initialData.description || '',
category: initialData.category || '',
priority: initialData.priority || 'Средний',
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(formData);
};
return (
{initialData.id ? 'Редактировать проект' : 'Новый проект'}
);
}
// Компонент "Планы работ проектов (WBS)"
function ProjectWorkPlans({ project }) {
const [tasks, setTasks] = useState([]);
const [showAddTaskForm, setShowAddTaskForm] = useState(false);
const [message, setMessage] = useState('');
useEffect(() => {
if (project) {
fetchTasks(project.id);
} else {
setTasks([]);
}
}, [project]);
const fetchTasks = async (projectId) => {
try {
const data = await mockApi.getTasksByProjectId(projectId);
setTasks(data);
} catch (error) {
console.error("Ошибка при загрузке задач:", error);
setMessage("Не удалось загрузить задачи.");
}
};
const handleAddTask = async (newTaskData) => {
try {
await mockApi.addTask({ ...newTaskData, projectId: project.id });
setMessage('Задача успешно добавлена!');
setShowAddTaskForm(false);
fetchTasks(project.id); // Обновить список задач
} catch (error) {
console.error("Ошибка при добавлении задачи:", error);
setMessage("Не удалось добавить задачу.");
}
};
if (!project) {
return (
Выберите проект из "Реестра проектов", чтобы просмотреть его задачи.
);
}
return (
Планы работ для проекта: "{project.name}"
setMessage('')} />
{showAddTaskForm && (
setShowAddTaskForm(false)} />
)}
{tasks.length === 0 ? (
Задачи для этого проекта не найдены.
) : (
Название задачи | Ответственный | Статус | Прогресс | Плановые даты |
{tasks.map((task) => (
{task.name} | {task.responsible} |
{task.status}
| {task.progress}% | {task.plannedStartDate} - {task.plannedEndDate} |
))}
)}
);
}
// Форма для добавления/редактирования задачи
function TaskForm({ onSubmit, onCancel, initialData = {} }) {
const [formData, setFormData] = useState({
name: initialData.name || '',
description: initialData.description || '',
responsible: initialData.responsible || '',
executors: initialData.executors ? initialData.executors.join(', ') : '',
plannedStartDate: initialData.plannedStartDate || '',
plannedEndDate: initialData.plannedEndDate || '',
status: initialData.status || 'Планируется',
priority: initialData.priority || 'Средний',
plannedEffort: initialData.plannedEffort || '',
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
const dataToSubmit = {
...formData,
executors: formData.executors.split(',').map(s => s.trim()).filter(s => s), // Преобразовать строку в массив
plannedEffort: parseFloat(formData.plannedEffort) || 0,
};
onSubmit(dataToSubmit);
};
return (
);
}
// Главный компонент приложения
function App() {
const [activeTab, setActiveTab] = useState('dashboard'); // 'dashboard', 'projects', 'tasks'
const [selectedProject, setSelectedProject] = useState(null);
const handleSelectProject = (project) => {
setSelectedProject(project);
setActiveTab('tasks');
};
return (
Project Office App
{activeTab === 'dashboard' && }
{activeTab === 'projects' && }
{activeTab === 'tasks' && }
);
}
export default App;