import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, signInWithCustomToken } from 'firebase/auth';
import { getFirestore, collection, query, onSnapshot, doc, setDoc, deleteDoc, Timestamp } from 'firebase/firestore';
import { Settings, Plus, X, Trash2, Save, DollarSign, Clock, Package, Zap, Percent, Anchor, Link2, Aperture, Gem, Scissors, Ruler, Search } from 'lucide-react';
// --- КОНСТАНТЫ КАМНЕЙ (РАСШИРЕННЫЙ СПИСОК: 50 ПОРОД) ---
const STONE_OPTIONS = [
'Авантюрин', 'Агат (полосатый)', 'Аквамарин', 'Амазонит', 'Аметист',
'Аметрин', 'Апатит', 'Бирюза', 'Бычий глаз', 'Варисцит',
'Гематит', 'Гелиотроп (Кровавик)', 'Горный хрусталь', 'Гранат', 'Говлит',
'Дендритовый агат', 'Диопсид', 'Дымчатый кварц (Раухтопаз)', 'Жадеит', 'Жемчуг (Речной)',
'Змеевик', 'Кахолонг', 'Кианит', 'Коралл', 'Кошачий глаз',
'Кварц (Розовый)', 'Кварц (Волосатик)', 'Лабрадорит', 'Лазурит', 'Ларимар',
'Лунный камень (Белый)', 'Магнезит', 'Малахит', 'Моховый агат', 'Нефрит',
'Обсидиан (Снежный)', 'Оникс', 'Опал (Общий)', 'Перидот (Хризолит)', 'Пирит',
'Пренит', 'Родонит', 'Родохрозит', 'Сардоникс', 'Сердолик',
'Солнечный камень', 'Соколиный глаз', 'Содалит', 'Тигровый глаз', 'Турмалин',
'Унакит', 'Флюорит', 'Халцедон', 'Хризоколла', 'Хризопраз',
'Цитрин', 'Чароит', 'Шунгит', 'Янтарь', 'Яшма (красная)'
]; // Всего 50 самых популярных пород
// --- НАСТРОЙКА FIREBASE (ОБЯЗАТЕЛЬНАЯ ЧАСТЬ) ---
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {};
const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
// Инициализация Firebase и Firestore
const app = Object.keys(firebaseConfig).length ? initializeApp(firebaseConfig) : null;
const db = app ? getFirestore(app) : null;
const auth = app ? getAuth(app) : null;
// --- КОНСТАНТЫ И ВЫЧИСЛЕНИЯ ---
const JEWELRY_TYPES = [
{ value: 'bracelet', label: 'Браслет' },
{ value: 'earrings', label: 'Серьги' },
{ value: 'rosary', label: 'Четки' },
{ value: 'keychain', label: 'Подвеска/Брелок' },
{ value: 'other', label: 'Другое/Комплект' },
];
const TAX_OPTIONS = [
{ value: 'none', label: 'Без налогов (0%)', percent: 0 },
{ value: 'self_employed_4', label: 'Самозанятый (4% физ. лица)', percent: 4 },
{ value: 'self_employed_6', label: 'Самозанятый/ИП УСН (6%)', percent: 6 },
];
const getTaxPercentage = (taxesType) => {
const selectedTax = TAX_OPTIONS.find(opt => opt.value === taxesType);
return selectedTax ? selectedTax.percent : 0;
};
const DEFAULT_FINDINGS = {
standard: {
cordWire: { name: 'Леска/Шнур (за метр/шт)', price: 50, quantity: 1 },
clasp: { name: 'Замок/Застежка', price: 100, quantity: 1 },
earringHooks: { name: 'Швензы/Основа серег', price: 80, quantity: 0 },
pendant: { name: 'Подвеска/Брелок', price: 150, quantity: 1 },
},
custom: [
{ name: 'Разделительные колечки (комплект)', price: 20, quantity: 1 },
],
};
const DEFAULT_PROJECT = {
id: '',
name: 'Новый Проект Мастера',
type: 'bracelet',
date: new Date().toISOString().split('T')[0],
stones: [],
findings: DEFAULT_FINDINGS,
otherCosts: {
workHours: 1,
ratePerHour: 500,
markupPercent: 150,
marketing: 100,
packaging: 150,
toolAmortization: 50,
taxesType: 'self_employed_6',
commissionPercent: 15,
},
};
// Функция для форматирования цены
const formatPrice = (price) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(price);
};
// --- ОСНОВНЫЕ КОМПОНЕНТЫ UI ---
const IconButton = ({ children, onClick, className = '', disabled = false }) => (
);
const InputField = ({ label, type = 'text', value, onChange, name, min = 0, step = 1, placeholder = '' }) => (
);
const SelectField = ({ label, value, onChange, options, name }) => (
);
const ResultCard = ({ title, value, icon: Icon, color = 'bg-blue-500' }) => (
);
// --- КОМПОНЕНТ: ПОИСКОВЫЙ ВЫПАДАЮЩИЙ СПИСОК ---
const SearchableSelectInput = ({ label, value, onChange, options, placeholder }) => {
const [searchTerm, setSearchTerm] = useState(value);
const [isFocused, setIsFocused] = useState(false);
const [filteredOptions, setFilteredOptions] = useState(options);
useEffect(() => {
setSearchTerm(value);
}, [value]);
useEffect(() => {
if (!searchTerm || searchTerm.trim() === '') {
setFilteredOptions(options);
return;
}
const lowerCaseSearch = searchTerm.toLowerCase();
const filtered = options.filter(option =>
option.toLowerCase().includes(lowerCaseSearch)
);
setFilteredOptions(filtered);
}, [searchTerm, options]);
const handleSelect = (selection) => {
setSearchTerm(selection);
onChange(selection);
setIsFocused(false);
};
const handleChange = (e) => {
const newValue = e.target.value;
setSearchTerm(newValue);
onChange(newValue); // Обновляем основное состояние, чтобы можно было вводить и пользовательские значения
};
return (
setIsFocused(true)}
onBlur={() => setTimeout(() => setIsFocused(false), 200)} // Задержка для клика
placeholder={placeholder}
className="p-3 pr-10 border border-gray-300 rounded-xl shadow-inner w-full focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition duration-150 ease-in-out bg-white text-gray-800"
/>
{isFocused && filteredOptions.length > 0 && (
{filteredOptions.map((option, index) => (
- handleSelect(option)} // onMouseDown, чтобы сработал до onBlur
className="p-3 cursor-pointer hover:bg-indigo-50 text-gray-800 transition duration-150 ease-in-out"
>
{option}
))}
{filteredOptions.length > 10 && (
-
Показаны первые {filteredOptions.length} совпадений...
)}
)}
);
};
// --- КОМПОНЕНТЫ СПИСКОВ ---
const StoneList = ({ items, onAddItem, onRemoveItem, onChangeItem }) => (
<>
2. Камни (Бусины)
{items.map((item, index) => (
onChangeItem('stones', index, 'name', value)}
options={STONE_OPTIONS}
placeholder="Введите или выберите камень..."
/>
onChangeItem('stones', index, 'price', e.target.value)}
min={0}
step={1}
/>
onChangeItem('stones', index, 'quantity', e.target.value)}
min={0}
step={0.1}
/>
onRemoveItem('stones', index)} className="p-1 bg-red-100 text-red-600 hover:bg-red-200">
))}
>
);
const CustomFindingList = ({ title, items, onAddItem, onRemoveItem, onChangeItem, arrayName, placeholder, icon: Icon }) => (
<>
{title}
{items.map((item, index) => (
onChangeItem(arrayName, index, 'name', e.target.value)}
placeholder={placeholder}
/>
onChangeItem(arrayName, index, 'price', e.target.value)}
min={0}
step={1}
/>
onChangeItem(arrayName, index, 'quantity', e.target.value)}
min={0}
step={0.1}
/>
onRemoveItem(arrayName, index)} className="p-1 bg-red-100 text-red-600 hover:bg-red-200">
))}
>
);
const StandardFindings = ({ standardFindings, onChangeStandardFinding }) => {
const FINDING_FIELDS = [
{ key: 'cordWire', label: 'Леска/Шнур (за м/шт)', icon: Scissors, color: 'text-blue-500' },
{ key: 'clasp', label: 'Замок/Застежка (шт)', icon: Anchor, color: 'text-green-500' },
{ key: 'earringHooks', label: 'Швензы/Основа серег (пара)', icon: Link2, color: 'text-yellow-600' },
{ key: 'pendant', label: 'Подвеска/Брелок (шт)', icon: Aperture, color: 'text-purple-500' },
];
return (
{FINDING_FIELDS.map(({ key, label, icon: Icon, color }) => (
{label}
onChangeStandardFinding(key, 'price', e.target.value)}
min={0}
step={1}
/>
onChangeStandardFinding(key, 'quantity', e.target.value)}
min={0}
step={1}
/>
))}
);
};
// --- ОСНОВНАЯ ЛОГИКА КАЛЬКУЛЯТОРА ---
const App = () => {
const [currentProject, setCurrentProject] = useState(DEFAULT_PROJECT);
const [projects, setProjects] = useState([]);
const [userId, setUserId] = useState(null);
const [isAuthReady, setIsAuthReady] = useState(false);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
// --- 1. АВТОРИЗАЦИЯ FIREBASE ---
useEffect(() => {
if (!auth) return;
const authenticate = async () => {
try {
if (initialAuthToken) {
await signInWithCustomToken(auth, initialAuthToken);
} else {
await signInAnonymously(auth);
}
setUserId(auth.currentUser?.uid || crypto.randomUUID());
setIsAuthReady(true);
} catch (error) {
console.error('Ошибка аутентификации Firebase:', error);
setUserId(crypto.randomUUID());
setIsAuthReady(true);
}
};
authenticate();
}, [auth]);
// --- 2. ПОДПИСКА НА ПРОЕКТЫ (onSnapshot) ---
useEffect(() => {
if (!db || !isAuthReady || !userId) return;
const projectsColRef = collection(db, `artifacts/${appId}/users/${userId}/jewelry_projects`);
const q = query(projectsColRef);
const unsubscribe = onSnapshot(q, (snapshot) => {
const fetchedProjects = snapshot.docs.map(doc => {
const data = doc.data();
// ** Логика миграции данных **
if (!data.findings || !data.findings.standard || !data.findings.custom) {
const oldFindings = data.findings || [];
data.findings = {
standard: DEFAULT_FINDINGS.standard,
custom: oldFindings,
};
}
if (!data.stones) data.stones = [];
if (!data.otherCosts.taxesType) data.otherCosts.taxesType = DEFAULT_PROJECT.otherCosts.taxesType;
// Конец логики миграции
return {
id: doc.id,
...data,
date: data.date instanceof Timestamp ? data.date.toDate().toISOString().split('T')[0] : data.date,
};
});
fetchedProjects.sort((a, b) => new Date(b.date) - new Date(a.date));
setProjects(fetchedProjects);
if (!currentProject.id && fetchedProjects.length > 0 && currentProject.name === DEFAULT_PROJECT.name) {
loadProject(fetchedProjects[0]);
} else if (currentProject.id) {
const updatedCurrent = fetchedProjects.find(p => p.id === currentProject.id);
if (updatedCurrent) {
setCurrentProject(updatedCurrent);
}
}
setMessage('Данные проектов обновлены.');
}, (error) => {
console.error('Ошибка подписки на Firestore:', error);
setMessage('Ошибка загрузки данных.');
});
return () => unsubscribe();
}, [db, isAuthReady, userId]);
// --- 3. ОБРАБОТЧИКИ СОСТОЯНИЯ ---
// Обработчик для камней и кастомной фурнитуры (динамические списки)
const handleItemChange = (arrayName, index, field, value) => {
let newItems;
if (arrayName === 'stones') {
newItems = [...currentProject.stones];
} else { // 'custom'
newItems = [...currentProject.findings.custom];
}
newItems[index] = {
...newItems[index],
[field]: field === 'name' ? value : parseFloat(value) || 0,
};
if (arrayName === 'stones') {
setCurrentProject({ ...currentProject, stones: newItems });
} else {
setCurrentProject({
...currentProject.findings,
findings: {
...currentProject.findings,
custom: newItems,
}
});
}
};
// Обработчик для СТАНДАРТНОЙ фурнитуры (объект)
const handleStandardFindingChange = (key, field, value) => {
setCurrentProject({
...currentProject,
findings: {
...currentProject.findings,
standard: {
...currentProject.findings.standard,
[key]: {
...currentProject.findings.standard[key],
[field]: field === 'name' ? value : parseFloat(value) || 0,
},
},
},
});
};
const handleOtherCostChange = (name, value) => {
setCurrentProject({
...currentProject,
otherCosts: {
...currentProject.otherCosts,
[name]: name === 'taxesType' ? value : parseFloat(value) || 0,
},
});
};
const handleTypeChange = (e) => {
setCurrentProject({
...currentProject,
type: e.target.value,
});
};
const addItem = (arrayName) => {
let newItem;
if (arrayName === 'stones') {
newItem = { name: `Новый Камень ${currentProject.stones.length + 1}`, price: 0, quantity: 1 };
setCurrentProject({ ...currentProject, stones: [...currentProject.stones, newItem] });
} else if (arrayName === 'custom') {
newItem = { name: `Доп. Фурнитура ${currentProject.findings.custom.length + 1}`, price: 0, quantity: 1 };
setCurrentProject({
...currentProject,
findings: {
...currentProject.findings,
custom: [...currentProject.findings.custom, newItem],
}
});
}
};
const removeItem = (arrayName, index) => {
if (arrayName === 'stones') {
const newItems = currentProject.stones.filter((_, i) => i !== index);
setCurrentProject({ ...currentProject, stones: newItems });
} else if (arrayName === 'custom') {
const newItems = currentProject.findings.custom.filter((_, i) => i !== index);
setCurrentProject({
...currentProject,
findings: {
...currentProject.findings,
custom: newItems,
}
});
}
};
const newProject = () => {
setCurrentProject({
...DEFAULT_PROJECT,
id: '',
name: `Новый Проект ${new Date().toLocaleTimeString('ru-RU')}`,
findings: JSON.parse(JSON.stringify(DEFAULT_FINDINGS)), // Глубокое копирование
});
setMessage('Создан новый проект.');
};
// --- 4. ВЫЧИСЛЕНИЕ РЕЗУЛЬТАТОВ (useMemo) ---
const { netCost, targetRetailPrice, taxAmount, commissionAmount, netProfit, currentTaxPercent } = useMemo(() => {
const { stones, findings, otherCosts } = currentProject;
const {
workHours,
ratePerHour,
marketing,
packaging,
markupPercent,
toolAmortization,
taxesType,
commissionPercent,
} = otherCosts;
// 1. Общая стоимость камней
const totalStoneCost = stones.reduce((sum, item) => sum + item.price * item.quantity, 0);
// 2. Общая стоимость фурнитуры (Стандартная + Дополнительная)
const standardFindingsCost = Object.values(findings.standard).reduce((sum, item) => sum + item.price * item.quantity, 0);
const customFindingsCost = findings.custom.reduce((sum, item) => sum + item.price * item.quantity, 0);
const totalMaterialCost = totalStoneCost + standardFindingsCost + customFindingsCost;
// 3. Стоимость работы и доп. расходы
const workCost = workHours * ratePerHour;
const otherFixedCosts = marketing + packaging + toolAmortization;
// 4. Расширенная Себестоимость (Net Cost) - все расходы до наценки
const netCost = totalMaterialCost + workCost + otherFixedCosts;
// 5. Целевая Розничная Цена (Target Retail Price) - Себестоимость + Наценка
const targetRetailPrice = netCost * (1 + markupPercent / 100);
// 6. Определение актуального процента налога
const currentTaxPercent = getTaxPercentage(taxesType);
// 7. Расчет вычетов (Налоги и Комиссия) от Target Retail Price
const taxAmount = targetRetailPrice * (currentTaxPercent / 100);
const commissionAmount = targetRetailPrice * (commissionPercent / 100);
// 8. Чистая Прибыль (Net Profit)
const netProfit = targetRetailPrice - netCost - taxAmount - commissionAmount;
return {
netCost,
targetRetailPrice,
taxAmount,
commissionAmount,
netProfit,
currentTaxPercent,
};
}, [currentProject]);
// --- 5. CRUD ОПЕРАЦИИ FIREBASE ---
const saveProject = useCallback(async () => {
if (!db || !userId) {
setMessage('Ошибка: База данных недоступна или пользователь не авторизован.');
return;
}
setLoading(true);
try {
let docRef;
const { id, ...projectData } = currentProject;
const projectToSave = {
...projectData,
date: currentProject.date ? Timestamp.fromDate(new Date(currentProject.date)) : Timestamp.now(),
otherCosts: {
...projectData.otherCosts,
taxesPercent: undefined,
}
};
if (id) {
docRef = doc(db, `artifacts/${appId}/users/${userId}/jewelry_projects`, id);
await setDoc(docRef, projectToSave, { merge: true });
setMessage(`Проект "${currentProject.name}" обновлен.`);
} else {
docRef = doc(collection(db, `artifacts/${appId}/users/${userId}/jewelry_projects`));
await setDoc(docRef, { ...projectToSave, id: docRef.id });
setCurrentProject({ ...currentProject, id: docRef.id });
setMessage(`Проект "${currentProject.name}" сохранен.`);
}
} catch (e) {
console.error('Ошибка сохранения документа:', e);
setMessage('Ошибка сохранения проекта. Проверьте консоль.');
} finally {
setLoading(false);
}
}, [db, userId, currentProject]);
const loadProject = useCallback((project) => {
setCurrentProject(project);
setIsSidebarOpen(false);
setMessage(`Проект "${project.name}" загружен.`);
}, []);
const deleteProject = useCallback(async (id, name) => {
if (!db || !userId) return;
setLoading(true);
try {
await deleteDoc(doc(db, `artifacts/${appId}/users/${userId}/jewelry_projects`, id));
setMessage(`Проект "${name}" удален.`);
if (currentProject.id === id) {
newProject();
}
} catch (e) {
console.error('Ошибка удаления документа:', e);
setMessage('Ошибка удаления проекта. Проверьте консоль.');
} finally {
setLoading(false);
}
}, [db, userId, currentProject]);
// --- UI-РЕНДЕРИНГ ---
if (!isAuthReady) {
return (
);
}
return (
{/* Боковое меню (Sidebar) */}
Проекты Мастера
setIsSidebarOpen(false)} className="md:hidden"> Ваши сохраненные проекты ({projects.length}):
{projects.map((project) => (
loadProject(project)}
>
{project.name}
{project.date}
{
e.stopPropagation();
deleteProject(project.id, project.name);
}}
className="p-1 ml-2 bg-red-100 text-red-600 hover:bg-red-200"
>
))}
Ваш ID: {userId || 'N/A'}
{/* Основной контент */}
{/* Мобильное меню */}
setIsSidebarOpen(true)} className="p-3">
Stone Alchemist
{/* Заголовок проекта */}
setCurrentProject({ ...currentProject, name: e.target.value })}
placeholder="Например, Браслет 'Лавандовая Дымка'"
/> setCurrentProject({ ...currentProject, date: e.target.value })}
/>
{/* Блок 1: Расчет Результатов */}
1. Сводка Расчетов
0 ? 'bg-green-500' : 'bg-gray-500'}
/>
{/* Блок 2: Камни (С поиском) */}
{/* Блок 3: Фурнитура (Интерактивный) */}
3. Фурнитура (Ключевые Элементы и Прочее)
3.1 Стандартные Элементы
{/* Блок 4: Дополнительные Расходы, Налоги и Комиссии */}
4. Работа и Неочевидные Расходы
{/* Расходы на Работу/Логистику */}
handleOtherCostChange('workHours', e.target.value)}
min={0}
step={0.5}
/> handleOtherCostChange('ratePerHour', e.target.value)}
min={1}
step={50}
/> handleOtherCostChange('marketing', e.target.value)}
min={0}
step={10}
/> handleOtherCostChange('packaging', e.target.value)}
min={0}
step={10}
/>
{/* Неочевидные Расходы */}
handleOtherCostChange('toolAmortization', e.target.value)}
min={0}
step={5}
/> handleOtherCostChange('markupPercent', e.target.value)}
min={1}
step={10}
/>
{/* Налоги и Комиссии (Обновлено) */}
handleOtherCostChange('taxesType', e.target.value)}
options={TAX_OPTIONS}
/> handleOtherCostChange('commissionPercent', e.target.value)}
min={0}
step={1}
/>
{/* Панель управления и сообщения */}
{loading ? 'Сохранение...' : message || 'Готов к сохранению.'}
);
};