Files
Crussell/frontend/src/lib/components/admin/EditBookingModal.svelte
T
2026-05-29 16:08:05 +01:00

841 lines
26 KiB
Svelte

<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { SvelteDate } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
import { formatDuration } from '$lib/utils/format';
import * as Modal from '$lib/components/ui/dialog';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Textarea } from '$lib/components/ui/textarea';
import type { Booking, BookingService, Service } from '$lib/types/booking';
interface Props {
open: boolean;
bookingId: string;
nextAppointmentStart?: string | null;
onSaved?: () => void;
}
let { open = $bindable(), bookingId, nextAppointmentStart = null, onSaved }: Props = $props();
let booking = $state<Booking | null>(null);
let services = $state<BookingService[]>([]);
let loading = $state(false);
let saving = $state(false);
let notes = $state('');
let showRemoveConfirm = $state(false);
let serviceToRemoveIndex = $state(-1);
let serviceToRemoveName = $state('');
let showOverrideModal = $state(false);
let overrideServiceIndex = $state(-1);
let overridePrice = $state('');
let overrideDuration = $state('');
let overrideOriginalPrice = $state(0);
let overrideOriginalDuration = $state(0);
let showAddServiceModal = $state(false);
let availableServices = $state<Service[]>([]);
let loadingServices = $state(false);
// Refund dialog state
let showRefundModal = $state(false);
let refundPaymentId = $state('');
let refundAmount = $state('');
let refundReason = $state('');
let refundLoading = $state(false);
$effect(() => {
if (open && bookingId) {
fetchBooking();
}
});
async function fetchBooking() {
loading = true;
try {
const response = await fetch(`/api/admin/bookings/${bookingId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
});
if (response.ok) {
const data = await response.json();
booking = {
id: data.id,
start_time: data.start_time,
status: data.status,
notes: data.notes,
created_at: data.created_at,
updated_at: data.updated_at,
created_by: data.created_by,
deposit_required: data.deposit_required ?? false,
deposit_amount: data.deposit_amount,
deposit_paid: data.deposit_paid ?? false,
deposit_deadline: data.deposit_deadline,
user: data.user
? {
id: data.user.id,
first_name: data.user.first_name,
last_name: data.user.last_name,
full_name: data.user.full_name,
email: data.user.email,
phone: data.user.phone,
profile_pic_url: data.user.profile_pic_url,
date_of_birth: data.user.date_of_birth,
account_role: data.user.account_role,
loyalty_stamps: data.user.loyalty_stamps,
referral_code: data.user.referral_code,
referral_code_uses: data.user.referral_code_uses,
created_at: data.user.created_at,
notes: data.user.notes
}
: undefined,
services: (data.services || []).map((s: BookingService) => ({
booking_id: s.booking_id,
service_id: s.service_id,
service_name: s.service_name,
service_description: s.service_description,
price: s.price,
duration_minutes: s.duration_minutes,
override_price: s.override_price,
override_duration_minutes: s.override_duration_minutes
})),
payments: (data.payments || []).map((p) => ({
id: p.id,
booking_id: p.booking_id,
payment_type: p.payment_type,
payment_method: p.payment_method,
vendor_code: p.vendor_code,
invoice_number: p.invoice_number,
status: p.status,
amount: p.amount,
is_vat_applicable: p.is_vat_applicable,
vat_rate: p.vat_rate,
vat_amount: p.vat_amount,
net_amount: p.net_amount,
created_at: p.created_at,
updated_at: p.updated_at,
created_by: p.created_by
})),
total_amount: data.total_amount || 0,
amount_paid: data.amount_paid || 0,
amount_due: data.amount_due || 0,
duration_minutes: data.duration_minutes || 0
};
services = booking.services || [];
notes = booking.notes || '';
} else {
const text = await response.text();
toast.error('Failed to load booking: ' + text);
}
} catch (err) {
console.error('Error fetching booking:', err);
toast.error('Network error loading booking');
} finally {
loading = false;
}
}
let totalDuration = $derived(
services.reduce((sum, s) => sum + (s.override_duration_minutes ?? s.duration_minutes ?? 0), 0)
);
let totalPrice = $derived(
services.reduce((sum, s) => sum + (s.override_price ?? s.price ?? 0), 0)
);
function confirmRemove(index: number) {
serviceToRemoveIndex = index;
serviceToRemoveName = services[index]?.service_name || 'this service';
showRemoveConfirm = true;
}
function removeService() {
if (serviceToRemoveIndex >= 0 && serviceToRemoveIndex < services.length) {
services = services.filter((_, i) => i !== serviceToRemoveIndex);
}
showRemoveConfirm = false;
serviceToRemoveIndex = -1;
}
function openOverrideModal(index: number) {
overrideServiceIndex = index;
const service = services[index];
overrideOriginalPrice = service.price ?? 0;
overrideOriginalDuration = service.duration_minutes ?? 0;
overridePrice = service.override_price !== undefined ? service.override_price.toFixed(2) : '';
overrideDuration = service.override_duration_minutes !== undefined ? service.override_duration_minutes.toString() : '';
showOverrideModal = true;
}
function handlePriceInput(value: string) {
let cleaned = value.replace(/[^\d.]/g, '');
const parts = cleaned.split('.');
if (parts.length > 2) {
cleaned = parts[0] + '.' + parts.slice(1).join('');
}
if (cleaned.includes('.')) {
const [integer, decimal] = cleaned.split('.');
if (decimal.length > 2) {
cleaned = integer + '.' + decimal.substring(0, 2);
}
}
overridePrice = cleaned;
}
function handleDurationInput(value: string) {
overrideDuration = value.replace(/\D/g, '');
}
function saveOverride() {
if (overrideServiceIndex < 0 || overrideServiceIndex >= services.length) return;
const service = services[overrideServiceIndex];
const newServices = [...services];
const updated = { ...service };
if (overridePrice !== '') {
const price = parseFloat(overridePrice);
if (!isNaN(price)) {
updated.override_price = price;
} else {
delete updated.override_price;
}
} else {
delete updated.override_price;
}
if (overrideDuration !== '') {
const duration = parseInt(overrideDuration);
if (!isNaN(duration) && duration > 0) {
updated.override_duration_minutes = duration;
} else {
delete updated.override_duration_minutes;
}
} else {
delete updated.override_duration_minutes;
}
newServices[overrideServiceIndex] = updated;
services = newServices;
showOverrideModal = false;
overrideServiceIndex = -1;
}
async function fetchAvailableServices() {
loadingServices = true;
try {
const response = await fetch('/api/services', {
headers: {
Authorization: `Bearer ${authStore.currentToken}`
}
});
if (response.ok) {
const data = await response.json();
availableServices = data.services || data;
}
} catch (err) {
console.error('Error fetching services:', err);
toast.error('Failed to load services');
} finally {
loadingServices = false;
}
}
function openAddServiceModal() {
fetchAvailableServices();
showAddServiceModal = true;
}
function addService(service: Service) {
const newService: BookingService = {
booking_id: bookingId,
service_id: service.id,
service_name: service.name,
service_description: service.description,
price: service.price,
duration_minutes: service.duration_minutes
};
services = [...services, newService];
}
function isServiceAdded(serviceId: string): boolean {
return services.some((s) => s.service_id === serviceId);
}
let maxAvailableDuration = $derived.by(() => {
if (!booking?.start_time || !nextAppointmentStart) return null;
const bookingStart = new SvelteDate(booking.start_time).getTime();
const currentDurationMs = totalDuration * 60 * 1000;
const bookingEnd = bookingStart + currentDurationMs;
const nextStart = new SvelteDate(nextAppointmentStart).getTime();
const availableMs = nextStart - bookingEnd;
return Math.max(0, Math.floor(availableMs / (60 * 1000)));
});
let filteredServices = $derived.by(() => {
return availableServices.filter((s) => {
if (isServiceAdded(s.id)) return false;
if (maxAvailableDuration !== null && s.duration_minutes > maxAvailableDuration) return false;
return true;
});
});
async function handleSave() {
saving = true;
try {
const payload: Record<string, unknown> = {
service_ids: services.map((s) => s.service_id),
service_overrides: services
.filter(
(s) =>
s.override_price !== undefined || s.override_duration_minutes !== undefined
)
.map((s) => ({
service_id: s.service_id,
...(s.override_price !== undefined ? { override_price: s.override_price } : {}),
...(s.override_duration_minutes !== undefined
? { override_duration_minutes: s.override_duration_minutes }
: {})
}))
};
if (notes !== booking?.notes) {
payload.notes = notes || undefined;
}
const response = await fetch(`/api/admin/bookings/${bookingId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
body: JSON.stringify(payload)
});
if (response.ok) {
toast.success('Booking updated successfully');
open = false;
onSaved?.();
} else {
const text = await response.text();
toast.error('Failed to update booking: ' + text);
}
} catch (err) {
console.error('Error saving booking:', err);
toast.error('Network error saving booking');
} finally {
saving = false;
}
}
function openRefundModal(paymentId: string, amountPence: number) {
refundPaymentId = paymentId;
refundAmount = (amountPence / 100).toFixed(2);
refundReason = '';
showRefundModal = true;
}
async function processRefund() {
if (!refundAmount || !refundReason.trim()) {
toast.error('Please enter a refund amount and reason');
return;
}
refundLoading = true;
try {
const amountPence = Math.round(parseFloat(refundAmount) * 100);
if (isNaN(amountPence) || amountPence <= 0) {
toast.error('Invalid refund amount');
return;
}
const response = await fetch(`/api/admin/payments/${refundPaymentId}/refund`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
body: JSON.stringify({
amount: amountPence,
reason: refundReason.trim()
})
});
if (response.ok) {
toast.success('Refund processed');
showRefundModal = false;
fetchBooking();
} else {
const text = await response.text();
toast.error('Failed to process refund: ' + text);
}
} catch (err) {
console.error('Error processing refund:', err);
toast.error('Network error processing refund');
} finally {
refundLoading = false;
}
}
</script>
<Modal.Root bind:open>
<Modal.Content class="max-h-[90vh] max-w-[calc(100%-2rem)] overflow-y-auto sm:max-w-md md:max-w-3xl">
<Modal.Header>
<div class="flex items-center justify-between">
<div>
<Modal.Title class="text-lg font-semibold">Edit Booking</Modal.Title>
{#if booking}
<div class="mt-1 text-sm text-gray-500">ID: {booking.id}</div>
{/if}
</div>
</div>
</Modal.Header>
{#if loading}
<div class="px-4 pb-4 text-center text-sm text-gray-500">Loading booking...</div>
{:else if booking}
<div class="space-y-6 px-4 pb-4">
<!-- Customer Info -->
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
Customer
</h3>
<div class="grid gap-3 md:grid-cols-2">
<div>
<div class="text-xs text-gray-500">Name</div>
<div class="font-medium">{booking.user?.full_name || '—'}</div>
</div>
<div>
<div class="text-xs text-gray-500">Email</div>
<div class="font-medium break-words" title={booking.user?.email}>{booking.user?.email || '—'}</div>
</div>
<div>
<div class="text-xs text-gray-500">Phone</div>
<div class="font-medium">{booking.user?.phone || '—'}</div>
</div>
<div>
<div class="text-xs text-gray-500">Time</div>
<div class="font-medium">
{(() => {
const date = new SvelteDate(booking.start_time);
return date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
})()}
</div>
</div>
</div>
</div>
<!-- Services -->
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
<div class="mb-3 flex items-center justify-between">
<h3 class="text-sm font-semibold tracking-wide text-gray-600 uppercase">
Services ({services.length})
</h3>
<Button size="sm" onclick={openAddServiceModal}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-1 h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
clip-rule="evenodd"
/>
</svg>
Add Service
</Button>
</div>
{#if services.length === 0}
<div class="rounded-md border border-dashed border-gray-300 p-4 text-center text-sm text-gray-500">
No services added yet. Click "Add Service" to begin.
</div>
{:else}
<div class="space-y-2">
{#each services as service, index (index)}
<div class="flex items-center justify-between rounded-md border border-gray-300 bg-white p-3">
<div class="flex-1">
<div class="font-medium">{service.service_name}</div>
<div class="mt-1 text-sm text-gray-600">
{formatDuration(service.override_duration_minutes ?? service.duration_minutes ?? 0)} • £{(service.override_price ?? service.price ?? 0).toFixed(2)}
</div>
{#if service.override_price !== undefined || service.override_duration_minutes !== undefined}
<div class="text-xs text-amber-600">Modified</div>
{/if}
</div>
<div class="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
onclick={() => openOverrideModal(index)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"
/>
</svg>
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-red-600 hover:bg-red-50 hover:text-red-700"
onclick={() => confirmRemove(index)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</Button>
</div>
</div>
{/each}
</div>
<div class="mt-3 flex items-center justify-between border-t border-gray-200 pt-3 text-sm">
<span class="text-gray-600">Total: {formatDuration(totalDuration)}</span>
<span class="font-semibold">£{totalPrice.toFixed(2)}</span>
</div>
{/if}
</div>
<!-- Payments -->
{#if booking?.payments && booking.payments.length > 0}
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
Payments ({booking.payments.length})
</h3>
<div class="space-y-2">
{#each booking.payments as payment, index (index)}
<div class="flex items-center justify-between rounded-md border border-gray-300 bg-white p-3">
<div class="flex-1">
<div class="font-medium">
{payment.payment_type === 'deposit' ? 'Deposit' : payment.payment_type === 'full' ? 'Full Payment' : payment.payment_type}
{#if payment.payment_method}
<span class="text-gray-500"> via {payment.payment_method}</span>
{/if}
</div>
<div class="mt-1 text-sm text-gray-600">
<span
class:text-green-600={payment.status === 'completed'}
class:text-amber-600={payment.status === 'pending'}
class:text-red-600={payment.status === 'failed' || payment.status === 'refunded'}
>
{payment.status}
</span>
<span class="mx-1">|</span>
£{(payment.amount / 100).toFixed(2)}
{#if payment.invoice_number}
<span class="mx-1">|</span>
<span class="text-gray-500">Inv: {payment.invoice_number}</span>
{/if}
</div>
<div class="mt-1 text-xs text-gray-400">
{new SvelteDate(payment.created_at).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })}
</div>
</div>
{#if payment.status === 'completed'}
<Button
size="sm"
variant="outline"
class="text-red-600 hover:bg-red-50 hover:text-red-700"
onclick={() => openRefundModal(payment.id, payment.amount)}
>
Refund
</Button>
{/if}
</div>
{/each}
</div>
</div>
{/if}
<!-- Notes -->
<div>
<label for="edit-notes" class="mb-2 block text-sm font-medium">Notes</label>
<Textarea
id="edit-notes"
bind:value={notes}
placeholder="Add any notes about this booking..."
rows={3}
class="w-full"
/>
{#if notes.trim() !== (booking.notes || '')}
<div class="mt-1 text-xs text-emerald-600">
Notes will be saved
</div>
{/if}
</div>
</div>
{/if}
<Modal.Footer class="flex items-center justify-end gap-2">
<Button variant="outline" onclick={() => (open = false)}>Cancel</Button>
<Button onclick={handleSave} disabled={saving || loading}>
{saving ? 'Saving...' : 'Save Changes'}
</Button>
</Modal.Footer>
</Modal.Content>
</Modal.Root>
<!-- Remove Service Confirmation -->
<AlertDialog.Root bind:open={showRemoveConfirm}>
<AlertDialog.Content class="z-[60]">
<AlertDialog.Header>
<AlertDialog.Title>Remove Service?</AlertDialog.Title>
<AlertDialog.Description>
This will remove {serviceToRemoveName} from the booking.
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
<AlertDialog.Action onclick={removeService} class="bg-red-600 hover:bg-red-700">
Remove
</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
<!-- Service Override Submodal -->
<Modal.Root open={showOverrideModal} onOpenChange={(v) => (showOverrideModal = v)}>
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto">
<Modal.Header>
<Modal.Title class="text-lg font-semibold">Edit Service</Modal.Title>
<Modal.Description>
{overrideServiceIndex >= 0 ? services[overrideServiceIndex]?.service_name : ''}
</Modal.Description>
</Modal.Header>
<div class="space-y-4 px-4 pb-4">
<div class="grid gap-4 md:grid-cols-2">
<div>
<label for="override-price" class="mb-1 block text-xs text-gray-600">
Price Override
</label>
<div class="relative">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<span class="text-gray-500">£</span>
</div>
<Input
id="override-price"
type="text"
inputmode="decimal"
value={overridePrice}
oninput={(e) => handlePriceInput(e.target.value)}
onblur={() => {
if (overridePrice) {
if (!overridePrice.includes('.')) {
const num = parseFloat(overridePrice);
if (!isNaN(num)) {
overridePrice = num.toFixed(2);
}
} else {
const parts = overridePrice.split('.');
if (parts[1].length === 0) {
overridePrice = parts[0] + '.00';
} else if (parts[1].length === 1) {
overridePrice = parts[0] + '.' + parts[1] + '0';
}
}
}
}}
class="no-spin w-full pl-7"
placeholder={overrideOriginalPrice.toFixed(2)}
/>
</div>
<div class="mt-1 flex items-center justify-between">
<div class="text-xs text-gray-500">
Original: £{overrideOriginalPrice.toFixed(2)}
</div>
{#if overridePrice !== '' && parseFloat(overridePrice) !== overrideOriginalPrice}
<div class="text-xs font-medium text-emerald-600">Changed</div>
{/if}
</div>
</div>
<div>
<label for="override-duration" class="mb-1 block text-xs text-gray-600">
Duration Override (min)
</label>
<div class="relative">
<Input
id="override-duration"
type="number"
min="1"
step="1"
value={overrideDuration}
oninput={(e) => handleDurationInput(e.target.value)}
class="no-spin w-full"
placeholder={overrideOriginalDuration.toString()}
/>
</div>
<div class="mt-1 flex items-center justify-between">
<div class="text-xs text-gray-500">
Original: {overrideOriginalDuration} min
</div>
{#if overrideDuration !== '' && parseInt(overrideDuration) !== overrideOriginalDuration}
<div class="text-xs font-medium text-emerald-600">Changed</div>
{/if}
</div>
</div>
</div>
</div>
<Modal.Footer class="flex items-center justify-end gap-2">
<Button variant="outline" onclick={() => (showOverrideModal = false)}>Cancel</Button>
<Button onclick={saveOverride}>Save</Button>
</Modal.Footer>
</Modal.Content>
</Modal.Root>
<!-- Add Service Submodal -->
<Modal.Root open={showAddServiceModal} onOpenChange={(v) => (showAddServiceModal = v)}>
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto">
<Modal.Header>
<Modal.Title class="text-lg font-semibold">Add Service</Modal.Title>
<Modal.Description>
Select a service to add to this booking.
</Modal.Description>
</Modal.Header>
<div class="space-y-4 px-4 pb-4">
{#if maxAvailableDuration !== null}
<div class="rounded-md border border-blue-200 bg-blue-50 p-3 text-sm">
<div class="font-medium text-blue-800">Available time: {maxAvailableDuration} min</div>
<div class="text-xs text-blue-600">
Current total: {totalDuration} min
</div>
</div>
{/if}
{#if loadingServices}
<div class="text-center text-sm text-gray-500">Loading services...</div>
{:else if filteredServices.length === 0}
<div class="rounded-md border border-dashed border-gray-300 p-4 text-center text-sm text-gray-500">
{#if availableServices.length === 0}
No services available.
{:else}
All services have been added or exceed available time.
{/if}
</div>
{:else}
<div class="space-y-2">
{#each filteredServices as service (service.id)}
<button
type="button"
class="w-full rounded-md border border-gray-300 bg-white p-3 text-left transition-colors hover:bg-gray-50"
onclick={() => addService(service)}
>
<div class="font-medium">{service.name}</div>
<div class="mt-1 text-sm text-gray-600">
{service.duration_minutes} min • £{service.price.toFixed(2)}
</div>
</button>
{/each}
</div>
{/if}
</div>
<Modal.Footer class="flex items-center justify-end">
<Button onclick={() => (showAddServiceModal = false)}>Close</Button>
</Modal.Footer>
</Modal.Content>
</Modal.Root>
<!-- Refund Dialog -->
<Modal.Root open={showRefundModal} onOpenChange={(v) => (showRefundModal = v)}>
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto">
<Modal.Header>
<Modal.Title class="text-lg font-semibold">Process Refund</Modal.Title>
<Modal.Description>
Enter the refund amount and reason.
</Modal.Description>
</Modal.Header>
<div class="space-y-4 px-4 pb-4">
<div>
<label for="refund-amount" class="mb-1 block text-xs text-gray-600">
Refund Amount
</label>
<div class="relative">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<span class="text-gray-500">£</span>
</div>
<Input
id="refund-amount"
type="text"
inputmode="decimal"
bind:value={refundAmount}
class="no-spin w-full pl-7"
placeholder="0.00"
/>
</div>
</div>
<div>
<label for="refund-reason" class="mb-1 block text-xs text-gray-600">
Reason (required)
</label>
<Textarea
id="refund-reason"
bind:value={refundReason}
placeholder="Enter reason for refund..."
rows={3}
class="w-full"
/>
</div>
</div>
<Modal.Footer class="flex items-center justify-end gap-2">
<Button variant="outline" onclick={() => (showRefundModal = false)}>Cancel</Button>
<Button onclick={processRefund} disabled={refundLoading || !refundAmount || !refundReason.trim()}>
{refundLoading ? 'Processing...' : 'Confirm Refund'}
</Button>
</Modal.Footer>
</Modal.Content>
</Modal.Root>
<style>
:global(input[type='number']) {
-moz-appearance: textfield;
appearance: textfield;
}
:global(input[type='number']::-webkit-outer-spin-button),
:global(input[type='number']::-webkit-inner-spin-button) {
-webkit-appearance: none;
margin: 0;
}
</style>