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:
2026-05-10 17:27:15 +03:00
parent af9e7ce1af
commit 3df6029f89
5 changed files with 314 additions and 8 deletions
+41 -8
View File
@@ -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 */}