diff --git a/frontend/src/app/guests/page.tsx b/frontend/src/app/guests/page.tsx index a301d74e..084e2ab9 100644 --- a/frontend/src/app/guests/page.tsx +++ b/frontend/src/app/guests/page.tsx @@ -35,6 +35,7 @@ import { ThumbsDown, ThumbsUp, Trash2, + Upload, UserRoundPlus, XCircle, } from "lucide-react"; @@ -78,7 +79,6 @@ type GuestVisitFormState = { position: string; guest_comment: string; purpose: string; - visit_comment: string; all_day: boolean; unlimited: boolean; date_from: string; @@ -190,7 +190,6 @@ const emptyForm = (): GuestVisitFormState => ({ position: "", guest_comment: "", purpose: "", - visit_comment: "", all_day: true, unlimited: false, ...getDefaultAllDayPeriod(), @@ -673,7 +672,6 @@ function GuestsPageContent() { guestMode: "existing", guest_id: String(visit.guest.id), purpose: visit.purpose || "", - visit_comment: visit.visit_comment || "", all_day: visit.all_day, unlimited: visit.unlimited, date_from: formatLocalDateInput(visit.access_starts_at), @@ -736,7 +734,6 @@ function GuestsPageContent() { const buildVisitPayload = (): Record => { const payload: Record = { purpose: form.purpose.trim(), - visit_comment: form.visit_comment.trim(), all_day: form.all_day, unlimited: form.unlimited, document_ids: form.document_ids, @@ -801,16 +798,10 @@ function GuestsPageContent() { const getIncompleteFormWarning = (): string | null => { if (editingVisit || form.guestMode !== "new") return null; - const hasMissingGuestProfileField = [ - form.avatar, - form.birth_date, - form.phone.trim(), - form.email.trim(), - form.organization.trim(), - form.position.trim(), - ].some((value) => !value); + const hasGuestIdentityWarning = !form.last_name.trim() || !form.phone.trim() || !form.avatar; + const hasGuestDocumentWarning = form.document_ids.length === 0 && form.pending_documents.length === 0; - return hasMissingGuestProfileField + return hasGuestIdentityWarning || hasGuestDocumentWarning ? "В заявке указаны не все поля. Можно вернуться и заполнить данные или продолжить создание заявки." : null; }; @@ -2390,7 +2381,14 @@ function GuestAvatarPicker({ onSelectImage: (value: string) => void; value: string; }) { - const handleChange = async (event: React.ChangeEvent) => { + const fileInputRef = useRef(null); + const videoRef = useRef(null); + const [cameraOpen, setCameraOpen] = useState(false); + const [cameraStarting, setCameraStarting] = useState(false); + const [cameraStream, setCameraStream] = useState(null); + const [cameraError, setCameraError] = useState(""); + + const handleFileChange = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; try { @@ -2400,17 +2398,131 @@ function GuestAvatarPicker({ } }; + const stopCamera = useCallback(() => { + cameraStream?.getTracks().forEach((track) => track.stop()); + setCameraStream(null); + setCameraStarting(false); + setCameraOpen(false); + setCameraError(""); + }, [cameraStream]); + + const openCamera = async () => { + setCameraOpen(true); + setCameraError(""); + setCameraStarting(true); + + try { + if (!navigator.mediaDevices?.getUserMedia) { + throw new Error("Camera API is not available"); + } + + const stream = await navigator.mediaDevices.getUserMedia({ + audio: false, + video: { facingMode: "user" }, + }); + setCameraStream(stream); + } catch { + setCameraError("Не удалось открыть камеру. Проверьте разрешение браузера на доступ к камере."); + } finally { + setCameraStarting(false); + } + }; + + const captureCameraImage = () => { + const video = videoRef.current; + if (!video || !video.videoWidth || !video.videoHeight) { + setCameraError("Камера ещё не готова. Попробуйте ещё раз через секунду."); + return; + } + + const canvas = document.createElement("canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const context = canvas.getContext("2d"); + if (!context) return; + + context.drawImage(video, 0, 0, canvas.width, canvas.height); + const capturedImage = canvas.toDataURL("image/jpeg", 0.92); + stopCamera(); + onSelectImage(capturedImage); + }; + + useEffect(() => { + const video = videoRef.current; + if (!video || !cameraStream) return; + + video.srcObject = cameraStream; + void video.play().catch(() => { + setCameraError("Не удалось запустить превью камеры."); + }); + + return () => { + video.srcObject = null; + }; + }, [cameraStream]); + + useEffect(() => ( + () => { + cameraStream?.getTracks().forEach((track) => track.stop()); + } + ), [cameraStream]); + return ( -
+
-
+

Фото гостя

Используется в учетке и связанных сервисах.

- +
+ + + +
+ {cameraOpen ? ( +
+
+ {cameraStream ? ( +
+ {cameraError ? ( +

{cameraError}

+ ) : null} +
+ + +
+
+ ) : null}
); @@ -2496,7 +2608,6 @@ function GuestVisitFormModal({ const filledGuestExtraCount = [ form.patronymic, form.birth_date, - form.phone, form.email, form.organization, form.position, @@ -2627,6 +2738,7 @@ function GuestVisitFormModal({ setField("last_name", value)} /> setField("first_name", value)} required />
+ setField("phone", value)} />
)} {documentsSection} @@ -2650,7 +2762,6 @@ function GuestVisitFormModal({
setField("patronymic", value)} /> setField("birth_date", value)} type="date" /> - setField("phone", value)} /> setField("email", value)} type="email" /> setField("organization", value)} /> setField("position", value)} /> @@ -2710,10 +2821,6 @@ function GuestVisitFormModal({ Цель приглашения