diff --git a/src/admin/orders/OrderModal.jsx b/src/admin/orders/OrderModal.jsx
index a0b3f39..1ca3562 100644
--- a/src/admin/orders/OrderModal.jsx
+++ b/src/admin/orders/OrderModal.jsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
-import { X, Save, FileText, Camera, X as XIcon, Share2, MessageSquare } from 'lucide-react';
+import { X, Save, FileText, Camera, X as XIcon, Share2, MessageSquare, MessageCircle, Copy } from 'lucide-react';
import { useOrderPhotos } from './useOrderPhotos';
+import { useOrderComments } from './useOrderComments';
import { pdf } from '@react-pdf/renderer';
import { supabase } from '../../lib/supabase';
import { useAuth } from '../auth/useAuth';
@@ -90,12 +91,14 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
});
const [events, setEvents] = useState([]);
const { photos, uploading, uploadPhotos, deletePhoto } = useOrderPhotos(mode === 'edit' ? order?.id : null);
+ const { comments, loading: commentsLoading, addComment } = useOrderComments(mode === 'edit' ? order?.id : null);
const [saving, setSaving] = useState(false);
const [downloading, setDownloading] = useState(false);
const [error, setError] = useState('');
const [copied, setCopied] = useState(false);
const [templatesOpen, setTemplatesOpen] = useState(false);
const [copiedTpl, setCopiedTpl] = useState(null);
+ const [commentText, setCommentText] = useState('');
useEffect(() => {
if (order?.id) {
@@ -151,6 +154,29 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
});
};
+ const handleAddComment = async () => {
+ if (!commentText.trim()) return;
+ await addComment(session?.user?.email || 'Аноним', commentText.trim());
+ setCommentText('');
+ };
+
+ const handleDuplicate = async () => {
+ const { name, phone, object_type, area_sqm, film_thickness, final_cost, notes, assigned_to, address, telegram_username, max_username } = form;
+ const { error } = await supabase.from('orders').insert({
+ name, phone, object_type,
+ area_sqm: area_sqm === '' ? null : Number(area_sqm),
+ film_thickness,
+ final_cost: final_cost === '' ? null : Number(final_cost),
+ notes, assigned_to, address, telegram_username, max_username,
+ status: 'новая', source: 'manual',
+ });
+ if (!error) {
+ onSaved();
+ } else {
+ setError(error.message);
+ }
+ };
+
const handleSave = async () => {
if (!form.name.trim() || !form.phone.trim()) {
setError('Имя и телефон обязательны');
@@ -266,6 +292,23 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
{mode === 'create' ? 'Новая заявка' : order?.name}
+ {mode === 'edit' && (
+
+ )}
{mode === 'edit' && order?.public_token && (
)}
+ {mode === 'edit' && (
+
+
+
+ Комментарии {comments.length > 0 && {comments.length}}
+
+
+ {/* Comment list */}
+
+ {comments.length === 0 && (
+
Комментариев пока нет
+ )}
+ {comments.map((c) => (
+
+
+ {c.author}
+
+ {new Date(c.created_at).toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
+
+
+
{c.body}
+
+ ))}
+
+
+ {/* Add comment */}
+
+ setCommentText(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleAddComment()}
+ placeholder="Добавить комментарий... (Enter для отправки)"
+ style={{ ...inputStyle, flex: 1 }}
+ disabled={commentsLoading}
+ />
+
+
+
+ )}
+
{mode === 'edit' && (
@@ -523,16 +620,28 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
История изменений
-
- {events.map((e) => (
-
-
- {new Date(e.created_at).toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
-
-
{e.description}
- {e.created_by && (
-
{e.created_by}
- )}
+
+ {events.map((e, i) => (
+
+ {/* Timeline dot + line */}
+
+
+ {i < events.length - 1 && (
+
+ )}
+
+
+
{e.description}
+
+ {new Date(e.created_at).toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
+ {e.created_by && · {e.created_by}}
+
+
))}
diff --git a/src/admin/orders/useOrderComments.js b/src/admin/orders/useOrderComments.js
new file mode 100644
index 0000000..900a3c7
--- /dev/null
+++ b/src/admin/orders/useOrderComments.js
@@ -0,0 +1,30 @@
+import { useState, useEffect, useCallback } from 'react';
+import { supabase } from '../../lib/supabase';
+
+export function useOrderComments(orderId) {
+ const [comments, setComments] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ const fetch = useCallback(async () => {
+ if (!orderId) return;
+ const { data } = await supabase
+ .from('order_comments')
+ .select('*')
+ .eq('order_id', orderId)
+ .order('created_at', { ascending: true });
+ setComments(data ?? []);
+ }, [orderId]);
+
+ useEffect(() => { fetch(); }, [fetch]);
+
+ const addComment = async (author, body) => {
+ setLoading(true);
+ const { error } = await supabase.from('order_comments')
+ .insert({ order_id: orderId, author, body });
+ if (!error) await fetch();
+ setLoading(false);
+ return { error };
+ };
+
+ return { comments, loading, addComment };
+}
diff --git a/supabase/migrations/007_order_comments.sql b/supabase/migrations/007_order_comments.sql
new file mode 100644
index 0000000..955294d
--- /dev/null
+++ b/supabase/migrations/007_order_comments.sql
@@ -0,0 +1,10 @@
+CREATE TABLE IF NOT EXISTS order_comments (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
+ author TEXT NOT NULL,
+ body TEXT NOT NULL,
+ created_at TIMESTAMPTZ DEFAULT now()
+);
+
+ALTER TABLE order_comments ENABLE ROW LEVEL SECURITY;
+CREATE POLICY "auth_all" ON order_comments FOR ALL TO authenticated USING (true) WITH CHECK (true);