This commit is contained in:
phaichayon
2026-06-23 22:13:08 +07:00
parent 99a4087099
commit c1ecd5ea50
32 changed files with 3503 additions and 150 deletions

View File

@@ -6,7 +6,7 @@ import { toast } from 'sonner';
import { Icons } from '@/components/icons';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Textarea } from '@/components/ui/textarea';
import { CrmTextarea } from '@/features/crm/components/crm-form-controls';
import {
approvalByIdOptions,
approvalsQueryOptions
@@ -80,11 +80,11 @@ export function QuotationApprovalTab({
{canSubmitNow ? (
<div className='space-y-3 rounded-lg border p-4'>
<div className='text-sm font-medium'>Submit quotation into approval flow</div>
<Textarea
<CrmTextarea
value={remark}
onChange={(event) => setRemark(event.target.value)}
placeholder='Optional remark for approvers'
rows={3}
size='md'
/>
<Button
isLoading={submitApproval.isPending}

View File

@@ -33,7 +33,13 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import {
CrmCurrencyInput,
CrmDateInput,
CrmNumberInput,
CrmPercentageInput,
CrmTextarea,
} from "@/features/crm/components/crm-form-controls";
import {
createQuotationAttachmentMutation,
createQuotationCustomerMutation,
@@ -168,12 +174,11 @@ function ItemDialog({
placeholder="Description"
/>
<div className="grid gap-4 md:grid-cols-2">
<Input
<CrmNumberInput
value={values.quantity}
onChange={(e) =>
setValues((s) => ({ ...s, quantity: e.target.value }))
onChange={(value) =>
setValues((s) => ({ ...s, quantity: value }))
}
type="number"
placeholder="Quantity"
/>
<Select
@@ -199,21 +204,21 @@ function ItemDialog({
</Select>
</div>
<div className="grid gap-4 md:grid-cols-2">
<Input
<CrmCurrencyInput
value={values.unitPrice}
onChange={(e) =>
setValues((s) => ({ ...s, unitPrice: e.target.value }))
onChange={(value) =>
setValues((s) => ({ ...s, unitPrice: value }))
}
type="number"
placeholder="Unit price"
currencyLabel="THB"
/>
<Input
<CrmCurrencyInput
value={values.discount}
onChange={(e) =>
setValues((s) => ({ ...s, discount: e.target.value }))
onChange={(value) =>
setValues((s) => ({ ...s, discount: value }))
}
type="number"
placeholder="Discount"
currencyLabel="THB"
/>
</div>
<div className="grid gap-4 md:grid-cols-2">
@@ -238,21 +243,21 @@ function ItemDialog({
))}
</SelectContent>
</Select>
<Input
<CrmPercentageInput
value={values.taxRate}
onChange={(e) =>
setValues((s) => ({ ...s, taxRate: e.target.value }))
onChange={(value) =>
setValues((s) => ({ ...s, taxRate: value }))
}
type="number"
placeholder="Tax rate %"
/>
</div>
<Textarea
<CrmTextarea
value={values.notes}
onChange={(e) =>
setValues((s) => ({ ...s, notes: e.target.value }))
}
placeholder="Notes"
size="md"
/>
</div>
<DialogFooter>
@@ -418,11 +423,11 @@ function TopicDialog({
onChange={(e) => setTitle(e.target.value)}
placeholder="Topic title"
/>
<Textarea
<CrmTextarea
value={itemsText}
onChange={(e) => setItemsText(e.target.value)}
placeholder="One line per topic item"
rows={6}
size="lg"
/>
</div>
<DialogFooter>
@@ -500,12 +505,12 @@ function FollowupDialog({
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Input
type="date"
<CrmDateInput
value={values.followupDate}
onChange={(e) =>
setValues((s) => ({ ...s, followupDate: e.target.value }))
setValues((s) => ({ ...s, followupDate: e }))
}
className="w-full"
/>
<Select
value={values.followupType}
@@ -552,12 +557,12 @@ function FollowupDialog({
}
placeholder="Outcome"
/>
<Input
type="date"
<CrmDateInput
value={values.nextFollowupDate}
onChange={(e) =>
setValues((s) => ({ ...s, nextFollowupDate: e.target.value }))
setValues((s) => ({ ...s, nextFollowupDate: e }))
}
className="w-full"
/>
<Input
value={values.nextAction}
@@ -566,12 +571,13 @@ function FollowupDialog({
}
placeholder="Next action"
/>
<Textarea
<CrmTextarea
value={values.notes}
onChange={(e) =>
setValues((s) => ({ ...s, notes: e.target.value }))
}
placeholder="Notes"
size="md"
/>
</div>
<DialogFooter>
@@ -668,21 +674,21 @@ function AttachmentDialog({
}
placeholder="application/pdf"
/>
<Input
<CrmNumberInput
value={values.fileSize}
onChange={(e) =>
setValues((s) => ({ ...s, fileSize: e.target.value }))
onChange={(value) =>
setValues((s) => ({ ...s, fileSize: value }))
}
type="number"
placeholder="File size bytes"
/>
</div>
<Textarea
<CrmTextarea
value={values.description}
onChange={(e) =>
setValues((s) => ({ ...s, description: e.target.value }))
}
placeholder="Description"
size="md"
/>
</div>
<DialogFooter>

View File

@@ -10,6 +10,13 @@ import {
ProjectPartiesEditor,
type ProjectPartyEditorItem
} from '@/features/crm/components/project-parties-editor';
import {
CrmCurrencyInput,
CrmDateInput,
CrmNumberInput,
CrmPercentageInput,
CrmTextarea
} from '@/features/crm/components/crm-form-controls';
import { Input } from '@/components/ui/input';
import {
Select,
@@ -27,7 +34,6 @@ import {
SheetTitle
} from '@/components/ui/sheet';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { quotationCustomersOptions } from '../api/queries';
import { createQuotationMutation, updateQuotationMutation } from '../api/mutations';
import type { QuotationMutationPayload, QuotationRecord, QuotationReferenceData } from '../api/types';
@@ -99,16 +105,21 @@ function toFormState(
function Field({
label,
required = false,
children,
className
}: {
label: string;
required?: boolean;
children: React.ReactNode;
className?: string;
}) {
return (
<div className={className}>
<div className='mb-2 text-sm font-medium'>{label}</div>
<div className='mb-2 text-sm font-medium'>
{label}
{required ? ' *' : ''}
</div>
{children}
</div>
);
@@ -308,7 +319,7 @@ export function QuotationFormSheet({
</Select>
</Field>
<Field label='Quotation Type'>
<Field label='Quotation Type' required>
<Select value={state.quotationType} onValueChange={(value) => setField('quotationType', value)}>
<SelectTrigger>
<SelectValue placeholder='Select type' />
@@ -323,12 +334,12 @@ export function QuotationFormSheet({
</Select>
</Field>
<Field label='Quotation Date'>
<Input type='date' value={state.quotationDate} onChange={(e) => setField('quotationDate', e.target.value)} />
<Field label='Quotation Date' required>
<CrmDateInput value={state.quotationDate} onChange={(value) => setField('quotationDate', value)} />
</Field>
<Field label='Valid Until'>
<Input type='date' value={state.validUntil} onChange={(e) => setField('validUntil', e.target.value)} />
<CrmDateInput value={state.validUntil} onChange={(value) => setField('validUntil', value)} />
</Field>
<Field label='Branch'>
@@ -378,7 +389,7 @@ export function QuotationFormSheet({
</Field>
<Field label='Exchange Rate'>
<Input value={state.exchangeRate} onChange={(e) => setField('exchangeRate', e.target.value)} type='number' step='0.0001' />
<CrmNumberInput value={state.exchangeRate} onChange={(value) => setField('exchangeRate', value)} step='0.0001' />
</Field>
<Field label='Project Name'>
@@ -414,11 +425,11 @@ export function QuotationFormSheet({
</Field>
<Field label='Chance %'>
<Input value={state.chancePercent} onChange={(e) => setField('chancePercent', e.target.value)} type='number' min='0' max='100' />
<CrmPercentageInput value={state.chancePercent} onChange={(value) => setField('chancePercent', value)} />
</Field>
<Field label='Discount'>
<Input value={state.discount} onChange={(e) => setField('discount', e.target.value)} type='number' step='0.01' />
<CrmCurrencyInput value={state.discount} onChange={(value) => setField('discount', value)} currencyLabel='THB' />
</Field>
<Field label='Discount Type'>
@@ -438,7 +449,7 @@ export function QuotationFormSheet({
</Field>
<Field label='Tax Rate %'>
<Input value={state.taxRate} onChange={(e) => setField('taxRate', e.target.value)} type='number' step='0.01' />
<CrmPercentageInput value={state.taxRate} onChange={(value) => setField('taxRate', value)} />
</Field>
<Field label='Sent Via'>
@@ -472,13 +483,13 @@ export function QuotationFormSheet({
<div className='md:col-span-2'>
<Field label='Notes'>
<Textarea value={state.notes} onChange={(e) => setField('notes', e.target.value)} placeholder='Internal notes' />
<CrmTextarea value={state.notes} onChange={(e) => setField('notes', e.target.value)} placeholder='Internal notes' size='md' />
</Field>
</div>
<div className='md:col-span-2'>
<Field label='Revision Remark'>
<Textarea value={state.revisionRemark} onChange={(e) => setField('revisionRemark', e.target.value)} placeholder='Revision note or approval context' />
<CrmTextarea value={state.revisionRemark} onChange={(e) => setField('revisionRemark', e.target.value)} placeholder='Revision note or approval context' size='md' />
</Field>
</div>