feat: client portal — public order tracking page at /track/:token
Adds anonymous order tracking via a unique public_token UUID on each order, a standalone React page at /track/:token, and a share button in OrderModal.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Save, FileText, Camera, X as XIcon } from 'lucide-react';
|
||||
import { X, Save, FileText, Camera, X as XIcon, Share2 } from 'lucide-react';
|
||||
import { useOrderPhotos } from './useOrderPhotos';
|
||||
import { pdf } from '@react-pdf/renderer';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
@@ -57,6 +57,7 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (order?.id) {
|
||||
@@ -96,6 +97,14 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
|
||||
scheduled_at: f.scheduled_at === '' ? null : f.scheduled_at,
|
||||
});
|
||||
|
||||
const handleShare = () => {
|
||||
const url = `${window.location.origin}/track/${order.public_token}`;
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.name.trim() || !form.phone.trim()) {
|
||||
setError('Имя и телефон обязательны');
|
||||
@@ -210,13 +219,37 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
|
||||
<h2 style={{ fontFamily: 'var(--font-heading)', fontSize: '1.2rem', fontWeight: 800 }}>
|
||||
{mode === 'create' ? 'Новая заявка' : order?.name}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Закрыть"
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)' }}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{mode === 'edit' && order?.public_token && (
|
||||
<button
|
||||
onClick={handleShare}
|
||||
aria-label="Поделиться ссылкой на заявку"
|
||||
title="Скопировать ссылку для клиента"
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: '5px',
|
||||
background: copied ? '#10b981' : 'var(--bg-lighter)',
|
||||
border: '1.5px solid var(--border-light)',
|
||||
borderRadius: '7px',
|
||||
padding: '5px 10px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 600,
|
||||
color: copied ? '#fff' : 'var(--text-muted)',
|
||||
transition: 'background 0.2s, color 0.2s',
|
||||
}}
|
||||
>
|
||||
<Share2 size={13} />
|
||||
{copied ? 'Скопировано!' : 'Поделиться'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Закрыть"
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)' }}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fields */}
|
||||
|
||||
Reference in New Issue
Block a user