feat: checklists, minibar and deposit modules

- DB migrations 057-059: checklist_templates/items/completions, minibar_items/consumptions, hotel_deposit_settings, booking_deposits
- Backend routes: checklists (templates CRUD + task completions), minibar (items + consumptions), deposit (settings, cash/yookassa hold, release/capture)
- YooKassa service for hold/capture/cancel payments
- Frontend: ChecklistSettingsPage, MinibarSettingsPage, DepositSettingsPage
- HousekeepingPage: task cards now show checklist + minibar panel with checkboxes and quantity buttons
- BookingModal: minibar charges summary + deposit management (cash/YooKassa/release) for existing bookings
- Sidebar + App.tsx: new routes /settings/checklists, /settings/minibar, /settings/deposit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 19:24:53 +03:00
parent ed2dfd4c7f
commit 3c1a6ccebf
17 changed files with 2569 additions and 9 deletions

View File

@@ -640,6 +640,118 @@ export const api = {
getCards: (slug: string, bookingId: string) =>
req<CardIssuance[]>('GET', `/api/hotels/${slug}/bookings/${bookingId}/cards`),
},
// ── Checklists ────────────────────────────────────────────────────────────
checklists: {
listTemplates: (slug: string) =>
req<ChecklistTemplate[]>('GET', `/api/hotels/${slug}/checklist-templates`),
createTemplate: (slug: string, data: { name: string; taskType?: string }) =>
req<ChecklistTemplate>('POST', `/api/hotels/${slug}/checklist-templates`, {
name: data.name,
task_type: data.taskType ?? null,
}),
updateTemplate: (slug: string, templateId: string, data: { name?: string; taskType?: string | null; isActive?: boolean; sortOrder?: number }) =>
req<ChecklistTemplate>('PATCH', `/api/hotels/${slug}/checklist-templates/${templateId}`, {
name: data.name,
task_type: data.taskType,
is_active: data.isActive,
sort_order: data.sortOrder,
}),
deleteTemplate: (slug: string, templateId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/checklist-templates/${templateId}`),
addItem: (slug: string, templateId: string, data: { text: string; sortOrder?: number }) =>
req<ChecklistItem>('POST', `/api/hotels/${slug}/checklist-templates/${templateId}/items`, {
text: data.text,
sort_order: data.sortOrder ?? 0,
}),
updateItem: (slug: string, templateId: string, itemId: string, data: { text?: string; sortOrder?: number }) =>
req<ChecklistItem>('PATCH', `/api/hotels/${slug}/checklist-templates/${templateId}/items/${itemId}`, {
text: data.text,
sort_order: data.sortOrder,
}),
deleteItem: (slug: string, templateId: string, itemId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/checklist-templates/${templateId}/items/${itemId}`),
getTaskChecklist: (slug: string, taskId: string) =>
req<TaskChecklist>('GET', `/api/hotels/${slug}/housekeeping/${taskId}/checklist`),
completeItem: (slug: string, taskId: string, itemId: string) =>
req<{ id: string }>('POST', `/api/hotels/${slug}/housekeeping/${taskId}/checklist/${itemId}/complete`),
uncompleteItem: (slug: string, taskId: string, itemId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/housekeeping/${taskId}/checklist/${itemId}/complete`),
},
// ── Minibar ───────────────────────────────────────────────────────────────
minibar: {
listItems: (slug: string) =>
req<MinibarItem[]>('GET', `/api/hotels/${slug}/minibar-items`),
createItem: (slug: string, data: { name: string; price: number; category?: string; sortOrder?: number }) =>
req<MinibarItem>('POST', `/api/hotels/${slug}/minibar-items`, {
name: data.name,
price: data.price,
category: data.category,
sort_order: data.sortOrder ?? 0,
}),
updateItem: (slug: string, itemId: string, data: { name?: string; price?: number; category?: string; sortOrder?: number; isActive?: boolean }) =>
req<MinibarItem>('PATCH', `/api/hotels/${slug}/minibar-items/${itemId}`, {
name: data.name,
price: data.price,
category: data.category,
sort_order: data.sortOrder,
is_active: data.isActive,
}),
deleteItem: (slug: string, itemId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/minibar-items/${itemId}`),
getTaskConsumptions: (slug: string, taskId: string) =>
req<MinibarConsumption[]>('GET', `/api/hotels/${slug}/housekeeping/${taskId}/minibar`),
addConsumption: (slug: string, taskId: string, data: { itemId: string; quantity: number }) =>
req<MinibarConsumption>('POST', `/api/hotels/${slug}/housekeeping/${taskId}/minibar`, {
item_id: data.itemId,
quantity: data.quantity,
}),
deleteConsumption: (slug: string, consumptionId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/minibar-consumptions/${consumptionId}`),
getBookingMinibar: (slug: string, bookingId: string) =>
req<MinibarBookingCharge[]>('GET', `/api/hotels/${slug}/bookings/${bookingId}/minibar`),
},
// ── Deposits ──────────────────────────────────────────────────────────────
deposits: {
getSettings: (slug: string) =>
req<DepositSettings>('GET', `/api/hotels/${slug}/deposit/settings`),
updateSettings: (slug: string, data: Partial<DepositSettingsPayload>) =>
req<DepositSettings>('PATCH', `/api/hotels/${slug}/deposit/settings`, data),
getBookingDeposit: (slug: string, bookingId: string) =>
req<BookingDeposit>('GET', `/api/hotels/${slug}/bookings/${bookingId}/deposit`),
payByCash: (slug: string, bookingId: string) =>
req<BookingDeposit>('POST', `/api/hotels/${slug}/bookings/${bookingId}/deposit/cash`),
createYookassaHold: (slug: string, bookingId: string) =>
req<BookingDeposit & { confirmationUrl: string | null }>('POST', `/api/hotels/${slug}/bookings/${bookingId}/deposit/yookassa`),
release: (slug: string, bookingId: string, capturedAmount: number, reason?: string) =>
req<BookingDeposit>('POST', `/api/hotels/${slug}/bookings/${bookingId}/deposit/release`, {
captured_amount: capturedAmount,
reason,
}),
},
}
// ── Schedule ─────────────────────────────────────────────────────────────────
@@ -1153,6 +1265,111 @@ export interface WifiSettings {
sessionHours: number
}
// ── Checklist types ──────────────────────────────────────────────────────────
export interface ChecklistItem {
id: string
templateId: string
text: string
sortOrder: number
}
export interface ChecklistTemplate {
id: string
hotelId: string
name: string
taskType: string | null
isActive: boolean
sortOrder: number
createdAt: string
items: ChecklistItem[]
}
export interface TaskChecklistItem {
id: string
text: string
sortOrder: number
completedAt: string | null
completedBy: string | null
}
export interface TaskChecklist {
templateId: string | null
templateName: string | null
items: TaskChecklistItem[]
}
// ── Minibar types ─────────────────────────────────────────────────────────────
export interface MinibarItem {
id: string
hotelId: string
name: string
price: number
category: string | null
isActive: boolean
sortOrder: number
}
export interface MinibarConsumption {
id: string
hotelId: string
roomId: string
bookingId: string | null
taskId: string | null
itemId: string
itemName: string
quantity: number
pricePerUnit: number
recordedByName: string | null
recordedAt: string
}
export interface MinibarBookingCharge {
id: string
itemName: string
quantity: number
pricePerUnit: number
total: number
recordedAt: string
recordedByName: string | null
}
// ── Deposit types ─────────────────────────────────────────────────────────────
export interface DepositSettings {
hotelId: string
isEnabled: boolean
amount: number
yookassaShopId: string | null
yookassaSecretKey: string | null
updatedAt?: string
}
export interface DepositSettingsPayload {
is_enabled?: boolean
amount?: number
yookassa_shop_id?: string
yookassa_secret_key?: string
}
export interface BookingDeposit {
id: string
hotelId: string
bookingId: string
amount: number
status: string
paymentMethod: string | null
yookassaPaymentId: string | null
yookassaConfirmationUrl: string | null
capturedAmount: number | null
retentionReason: string | null
guestEmailSent: boolean
createdAt: string
paidAt: string | null
releasedAt: string | null
}
function toHotelPayload(h: HotelPayload): Record<string, unknown> {
const out: Record<string, unknown> = {}
if (h.name !== undefined) out.name = h.name