feat: add optional inventory package association to service items and implement audit logging
- Added `inventory_package_uuid` and `inventory_package_title` fields to the `ServiceItem` interface. - Updated API documentation to reflect new fields in service item responses. - Implemented methods in `ClinicServiceController` to handle inventory package associations. - Created `ServiceItemAuditLog` entity and repository for tracking changes to service items. - Added functionality to log changes to service items, including inventory package associations. - Implemented tests for attaching/detaching inventory packages and auditing changes. - Created database migrations for new fields and audit log table.
This commit is contained in:
@@ -8,6 +8,7 @@ import { toast } from 'sonner';
|
||||
import { api } from '../lib/api';
|
||||
import type { ApiResponse } from '../lib/api';
|
||||
import type { ServiceItem, ClinicStaff } from '../types';
|
||||
import type { InventoryPackage } from '../hooks/useInventory';
|
||||
import { rialToToman, tomanToRial } from '../lib/utils';
|
||||
import { numericField } from '../lib/forms';
|
||||
import Modal from './ui/Modal';
|
||||
@@ -20,10 +21,15 @@ const itemSchema = z.object({
|
||||
staff_uuids: z.array(z.string()).optional(),
|
||||
duration_minutes: z.coerce.number().min(0).optional(),
|
||||
bookable: z.boolean().optional(),
|
||||
/** پکیج کالای مصرفی؛ رشتهی خالی یعنی بدون پکیج. */
|
||||
inventory_package_uuid: z.string().optional(),
|
||||
});
|
||||
type ItemForm = z.infer<typeof itemSchema>;
|
||||
|
||||
const EMPTY_FORM: ItemForm = { name: '', price_rials: 0, staff_uuids: [], duration_minutes: undefined, bookable: false };
|
||||
const EMPTY_FORM: ItemForm = {
|
||||
name: '', price_rials: 0, staff_uuids: [], duration_minutes: undefined,
|
||||
bookable: false, inventory_package_uuid: '',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
/** `'create'` برای سرویس جدید، شیء سرویس برای ویرایش، `null` یعنی بسته. */
|
||||
@@ -52,6 +58,13 @@ export default function ServiceItemFormModal({ item, sectionUuid, onClose, onMan
|
||||
});
|
||||
const allStaff = staffData?.data ?? [];
|
||||
|
||||
const { data: packagesData } = useQuery<ApiResponse<InventoryPackage[]>>({
|
||||
queryKey: ['inventory-packages'],
|
||||
queryFn: () => api.get('/api/v1/inventory-packages'),
|
||||
enabled: item !== null,
|
||||
});
|
||||
const packages = packagesData?.data ?? [];
|
||||
|
||||
const form = useForm<ItemForm>({ resolver: zodResolver(itemSchema), defaultValues: EMPTY_FORM });
|
||||
|
||||
useEffect(() => {
|
||||
@@ -63,6 +76,7 @@ export default function ServiceItemFormModal({ item, sectionUuid, onClose, onMan
|
||||
staff_uuids: (editing.staff_members ?? (editing.staff ? [editing.staff] : [])).map((s) => s.uuid),
|
||||
duration_minutes: editing.duration_minutes ?? undefined,
|
||||
bookable: editing.bookable ?? false,
|
||||
inventory_package_uuid: editing.inventory_package_uuid ?? '',
|
||||
}
|
||||
: EMPTY_FORM);
|
||||
}, [item]);
|
||||
@@ -73,21 +87,24 @@ export default function ServiceItemFormModal({ item, sectionUuid, onClose, onMan
|
||||
if (editing) qc.invalidateQueries({ queryKey: ['service-item', editing.uuid] });
|
||||
};
|
||||
|
||||
// رشتهی خالی در payload یعنی «بدون پکیج»؛ بکاند null میخواهد.
|
||||
const toPayload = (body: ItemForm) => ({
|
||||
...body,
|
||||
price_rials: tomanToRial(body.price_rials),
|
||||
inventory_package_uuid: body.inventory_package_uuid || null,
|
||||
});
|
||||
|
||||
const createItem = useMutation({
|
||||
mutationFn: (body: ItemForm) => api.post('/api/v1/service-item', {
|
||||
...body,
|
||||
...toPayload(body),
|
||||
section_uuid: sectionUuid,
|
||||
price_rials: tomanToRial(body.price_rials),
|
||||
}),
|
||||
onSuccess: () => { invalidate(); onClose(); toast.success('سرویس ایجاد شد'); },
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
const editItem = useMutation({
|
||||
mutationFn: (body: ItemForm) => api.patch(`/api/v1/service-item/${editing!.uuid}`, {
|
||||
...body,
|
||||
price_rials: tomanToRial(body.price_rials),
|
||||
}),
|
||||
mutationFn: (body: ItemForm) => api.patch(`/api/v1/service-item/${editing!.uuid}`, toPayload(body)),
|
||||
onSuccess: () => { invalidate(); onClose(); toast.success('سرویس ویرایش شد'); },
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
@@ -204,6 +221,19 @@ export default function ServiceItemFormModal({ item, sectionUuid, onClose, onMan
|
||||
<span style={{ fontSize: 13 }}>نمایش در نوبتدهی</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="field-label">پکیج کالای مصرفی</label>
|
||||
<SearchableSelect
|
||||
options={packages.map((p) => ({ value: p.uuid, label: p.title }))}
|
||||
value={form.watch('inventory_package_uuid') || null}
|
||||
onChange={(v) => form.setValue('inventory_package_uuid', v == null ? '' : String(v))}
|
||||
placeholder="بدون پکیج (اختیاری)"
|
||||
noOptionsMessage="پکیجی تعریف نشده است"
|
||||
isClearable
|
||||
height={42}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* بیمه — تنظیمات فقط در «پوشش بیمه» مدیریت میشود تا دادهی تکراری ساخته نشود. */}
|
||||
|
||||
Reference in New Issue
Block a user