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);