Starting a session created area records with no device, and the panel only ever read the device it never set — so every "اتمام این ناحیه" came back 422 with "دستگاه این ناحیه مشخص نیست". The backend tests passed because they sent resource_uuid explicitly; from the UI the flow was unusable end to end. The device now inherits from the appointment's resource, which the secretary already chose at booking; asking the operator again is taking one decision twice. The session screen offers a picker per area on top of that, because one session really does run bikini on an alexandrite and underarms on a diode. Treating without a device is allowed: botox is an injection, and requiring a device would make clinics invent a fake resource per injection. Sending readings with no device is still rejected — there would be no schema to validate against. A protocol whose service has no ResourceServiceOffering rows now says so in the tab where the manager is standing. It does not block booking: "no offering means any resource" is a deliberate, tested rule. But silence meant the gap surfaced only when the operator was already in front of a patient. Also adds the live timer the spec asked for, and wires slot-suggestions into the unbooked queue — the endpoint existed and tested green but no screen called it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
41 lines
1.6 KiB
TypeScript
41 lines
1.6 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
|
|
/**
|
|
* مدت سپریشده از یک لحظه، بهصورت زنده.
|
|
*
|
|
* `startedAt` تایماستمپ **سرور** است و مبنا همان میماند؛ اینجا فقط هر ثانیه دوباره
|
|
* رندر میشود. ساعت مرورگر ممکن است چند ثانیه جلو یا عقب باشد، ولی زمانِ ثبتشده
|
|
* همان است که سرور نوشته — این عدد فقط برای دیدن است، نه برای ذخیره.
|
|
*
|
|
* `null` یعنی هنوز شروع نشده؛ `finishedAt` که بیاید تایمر میایستد.
|
|
*/
|
|
export function useElapsed(startedAt: number | null, finishedAt: number | null = null): string | null {
|
|
const [now, setNow] = useState(() => Math.floor(Date.now() / 1000));
|
|
|
|
const running = startedAt !== null && finishedAt === null;
|
|
|
|
useEffect(() => {
|
|
if (!running) return;
|
|
|
|
const id = setInterval(() => setNow(Math.floor(Date.now() / 1000)), 1000);
|
|
|
|
return () => clearInterval(id);
|
|
}, [running]);
|
|
|
|
if (startedAt === null) return null;
|
|
|
|
const seconds = Math.max(0, (finishedAt ?? now) - startedAt);
|
|
|
|
return formatDuration(seconds);
|
|
}
|
|
|
|
/** «۰۵:۳۲» یا «۱:۱۲:۰۴» — ساعت فقط وقتی واقعاً از یک ساعت گذشته باشد. */
|
|
export function formatDuration(seconds: number): string {
|
|
const h = Math.floor(seconds / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
const s = seconds % 60;
|
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
|
|
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
}
|