[TASK-MASTER] feat: FE-003 partners + FE-004 companies extensions
FE-003: Extended partners feature with: - addresses API (CRUD endpoints) - contacts API (CRUD endpoints) - bankAccounts API (CRUD endpoints) - AddressForm, AddressList, ContactForm, ContactList components - usePartnerAddresses, usePartnerContacts, usePartnerBankAccounts hooks FE-004: Extended companies feature with: - settings endpoints (GET/PUT company settings) - branches endpoints (CRUD branches) - CompanySettingsForm, BranchesList components - useCompanySettings, useCompanyBranches hooks Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
158ebcb57b
commit
b5cffbff5f
@ -1,10 +1,13 @@
|
|||||||
import { api } from '@services/api/axios-instance';
|
import { api } from '@services/api/axios-instance';
|
||||||
import type {
|
import type {
|
||||||
Company,
|
Company,
|
||||||
|
CompanySettings,
|
||||||
CreateCompanyDto,
|
CreateCompanyDto,
|
||||||
UpdateCompanyDto,
|
UpdateCompanyDto,
|
||||||
CompanyFilters,
|
CompanyFilters,
|
||||||
CompaniesResponse,
|
CompaniesResponse,
|
||||||
|
Branch,
|
||||||
|
BranchesResponse,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
const BASE_URL = '/api/v1/companies';
|
const BASE_URL = '/api/v1/companies';
|
||||||
@ -52,4 +55,27 @@ export const companiesApi = {
|
|||||||
const response = await api.get<Company[]>(`${BASE_URL}/${parentId}/children`);
|
const response = await api.get<Company[]>(`${BASE_URL}/${parentId}/children`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Get company settings
|
||||||
|
getSettings: async (id: string): Promise<CompanySettings> => {
|
||||||
|
const response = await api.get<{ success: boolean; data: Company }>(`${BASE_URL}/${id}`);
|
||||||
|
return response.data.data?.settings || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update company settings
|
||||||
|
updateSettings: async (id: string, settings: CompanySettings): Promise<CompanySettings> => {
|
||||||
|
const response = await api.put<{ success: boolean; data: Company }>(`${BASE_URL}/${id}`, { settings });
|
||||||
|
return response.data.data?.settings || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get branches for a company (filtered by tenant)
|
||||||
|
getBranches: async (companyId: string): Promise<BranchesResponse> => {
|
||||||
|
const response = await api.get<{ success: boolean; data: Branch[]; total?: number }>(
|
||||||
|
`/api/v1/branches?companyId=${companyId}`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
data: response.data.data || [],
|
||||||
|
total: response.data.total || response.data.data?.length || 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
147
src/features/companies/components/BranchesList.tsx
Normal file
147
src/features/companies/components/BranchesList.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { MapPin, Phone, Mail, Clock, Building2 } from 'lucide-react';
|
||||||
|
import { Badge } from '@components/atoms/Badge';
|
||||||
|
import { Card, CardContent } from '@components/molecules/Card';
|
||||||
|
import { Spinner } from '@components/atoms/Spinner';
|
||||||
|
import type { Branch, BranchType } from '../types';
|
||||||
|
|
||||||
|
const branchTypeLabels: Record<BranchType, string> = {
|
||||||
|
headquarters: 'Matriz',
|
||||||
|
regional: 'Regional',
|
||||||
|
store: 'Tienda',
|
||||||
|
warehouse: 'Almacen',
|
||||||
|
office: 'Oficina',
|
||||||
|
factory: 'Fabrica',
|
||||||
|
};
|
||||||
|
|
||||||
|
const branchTypeColors: Record<BranchType, 'default' | 'primary' | 'success' | 'warning' | 'danger'> = {
|
||||||
|
headquarters: 'primary',
|
||||||
|
regional: 'warning',
|
||||||
|
store: 'success',
|
||||||
|
warehouse: 'default',
|
||||||
|
office: 'default',
|
||||||
|
factory: 'default',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BranchesListProps {
|
||||||
|
branches: Branch[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
emptyMessage?: string;
|
||||||
|
onBranchClick?: (branch: Branch) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BranchesList({
|
||||||
|
branches,
|
||||||
|
isLoading,
|
||||||
|
emptyMessage = 'No hay sucursales registradas',
|
||||||
|
onBranchClick,
|
||||||
|
}: BranchesListProps) {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center p-8">
|
||||||
|
<Spinner size="lg" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!branches || branches.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||||
|
<Building2 className="h-12 w-12 text-gray-300" />
|
||||||
|
<p className="mt-2 text-gray-500">{emptyMessage}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{branches.map((branch) => (
|
||||||
|
<Card
|
||||||
|
key={branch.id}
|
||||||
|
className={onBranchClick ? 'cursor-pointer hover:shadow-md transition-shadow' : ''}
|
||||||
|
onClick={() => onBranchClick?.(branch)}
|
||||||
|
>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h4 className="font-medium text-gray-900">{branch.name}</h4>
|
||||||
|
<Badge variant={branchTypeColors[branch.branchType]}>
|
||||||
|
{branchTypeLabels[branch.branchType]}
|
||||||
|
</Badge>
|
||||||
|
{branch.isMain && (
|
||||||
|
<Badge variant="primary">Principal</Badge>
|
||||||
|
)}
|
||||||
|
{!branch.isActive && (
|
||||||
|
<Badge variant="default">Inactiva</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{branch.shortName && (
|
||||||
|
<p className="mt-0.5 text-sm text-gray-500">
|
||||||
|
Codigo: {branch.code} | {branch.shortName}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!branch.shortName && (
|
||||||
|
<p className="mt-0.5 text-sm text-gray-500">Codigo: {branch.code}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 grid grid-cols-1 gap-2 text-sm text-gray-600 sm:grid-cols-2">
|
||||||
|
{(branch.addressLine1 || branch.city) && (
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<MapPin className="mt-0.5 h-4 w-4 flex-shrink-0 text-gray-400" />
|
||||||
|
<span>
|
||||||
|
{[branch.addressLine1, branch.city, branch.state]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{branch.phone && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Phone className="h-4 w-4 text-gray-400" />
|
||||||
|
<span>{branch.phone}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{branch.email && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Mail className="h-4 w-4 text-gray-400" />
|
||||||
|
<span>{branch.email}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="h-4 w-4 text-gray-400" />
|
||||||
|
<span>{branch.timezone}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{branch.settings && (
|
||||||
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
{branch.settings.allowPos && (
|
||||||
|
<span className="rounded-full bg-green-50 px-2 py-0.5 text-xs text-green-700">
|
||||||
|
POS
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{branch.settings.allowWarehouse && (
|
||||||
|
<span className="rounded-full bg-blue-50 px-2 py-0.5 text-xs text-blue-700">
|
||||||
|
Almacen
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{branch.settings.allowCheckIn && (
|
||||||
|
<span className="rounded-full bg-purple-50 px-2 py-0.5 text-xs text-purple-700">
|
||||||
|
Check-in
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
194
src/features/companies/components/CompanySettingsForm.tsx
Normal file
194
src/features/companies/components/CompanySettingsForm.tsx
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { Button } from '@components/atoms/Button';
|
||||||
|
import { FormField } from '@components/molecules/FormField';
|
||||||
|
import type { CompanySettings } from '../types';
|
||||||
|
|
||||||
|
const settingsSchema = z.object({
|
||||||
|
email: z.string().email('Email invalido').optional().or(z.literal('')),
|
||||||
|
phone: z.string().optional(),
|
||||||
|
website: z.string().url('URL invalida').optional().or(z.literal('')),
|
||||||
|
taxRegime: z.string().optional(),
|
||||||
|
fiscalPosition: z.string().optional(),
|
||||||
|
address: z.string().optional(),
|
||||||
|
city: z.string().optional(),
|
||||||
|
state: z.string().optional(),
|
||||||
|
country: z.string().optional(),
|
||||||
|
zipCode: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type SettingsFormData = z.infer<typeof settingsSchema>;
|
||||||
|
|
||||||
|
interface CompanySettingsFormProps {
|
||||||
|
settings?: CompanySettings;
|
||||||
|
onSubmit: (data: CompanySettings) => Promise<void>;
|
||||||
|
onCancel: () => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CompanySettingsForm({
|
||||||
|
settings,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
isLoading,
|
||||||
|
}: CompanySettingsFormProps) {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isDirty },
|
||||||
|
} = useForm<SettingsFormData>({
|
||||||
|
resolver: zodResolver(settingsSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: settings?.email || '',
|
||||||
|
phone: settings?.phone || '',
|
||||||
|
website: settings?.website || '',
|
||||||
|
taxRegime: settings?.taxRegime || '',
|
||||||
|
fiscalPosition: settings?.fiscalPosition || '',
|
||||||
|
address: settings?.address || '',
|
||||||
|
city: settings?.city || '',
|
||||||
|
state: settings?.state || '',
|
||||||
|
country: settings?.country || '',
|
||||||
|
zipCode: settings?.zipCode || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFormSubmit = async (data: SettingsFormData) => {
|
||||||
|
const cleanData: CompanySettings = {};
|
||||||
|
if (data.email) cleanData.email = data.email;
|
||||||
|
if (data.phone) cleanData.phone = data.phone;
|
||||||
|
if (data.website) cleanData.website = data.website;
|
||||||
|
if (data.taxRegime) cleanData.taxRegime = data.taxRegime;
|
||||||
|
if (data.fiscalPosition) cleanData.fiscalPosition = data.fiscalPosition;
|
||||||
|
if (data.address) cleanData.address = data.address;
|
||||||
|
if (data.city) cleanData.city = data.city;
|
||||||
|
if (data.state) cleanData.state = data.state;
|
||||||
|
if (data.country) cleanData.country = data.country;
|
||||||
|
if (data.zipCode) cleanData.zipCode = data.zipCode;
|
||||||
|
await onSubmit(cleanData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
|
||||||
|
{/* Contact Info */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Informacion de contacto</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<FormField label="Email" error={errors.email?.message}>
|
||||||
|
<input
|
||||||
|
{...register('email')}
|
||||||
|
type="email"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="contacto@empresa.com"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Telefono" error={errors.phone?.message}>
|
||||||
|
<input
|
||||||
|
{...register('phone')}
|
||||||
|
type="tel"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="+52 55 1234 5678"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField label="Sitio web" error={errors.website?.message}>
|
||||||
|
<input
|
||||||
|
{...register('website')}
|
||||||
|
type="url"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="https://www.empresa.com"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fiscal Info */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Informacion fiscal</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<FormField label="Regimen fiscal" error={errors.taxRegime?.message}>
|
||||||
|
<input
|
||||||
|
{...register('taxRegime')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="601 - General de Ley"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Posicion fiscal" error={errors.fiscalPosition?.message}>
|
||||||
|
<input
|
||||||
|
{...register('fiscalPosition')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="General"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Address */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Direccion</h3>
|
||||||
|
|
||||||
|
<FormField label="Direccion" error={errors.address?.message}>
|
||||||
|
<input
|
||||||
|
{...register('address')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="Av. Principal #123, Col. Centro"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||||
|
<FormField label="Ciudad" error={errors.city?.message}>
|
||||||
|
<input
|
||||||
|
{...register('city')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="Ciudad de Mexico"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Estado" error={errors.state?.message}>
|
||||||
|
<input
|
||||||
|
{...register('state')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="CDMX"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Pais" error={errors.country?.message}>
|
||||||
|
<input
|
||||||
|
{...register('country')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="Mexico"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="C.P." error={errors.zipCode?.message}>
|
||||||
|
<input
|
||||||
|
{...register('zipCode')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="06600"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 border-t pt-4">
|
||||||
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isLoading} disabled={!isDirty}>
|
||||||
|
Guardar configuracion
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,2 +1,4 @@
|
|||||||
export * from './CompanyForm';
|
export * from './CompanyForm';
|
||||||
export * from './CompanyFiltersPanel';
|
export * from './CompanyFiltersPanel';
|
||||||
|
export * from './CompanySettingsForm';
|
||||||
|
export * from './BranchesList';
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
export * from './useCompanies';
|
export * from './useCompanies';
|
||||||
|
export * from './useCompanySettings';
|
||||||
|
export * from './useCompanyBranches';
|
||||||
|
|||||||
20
src/features/companies/hooks/useCompanyBranches.ts
Normal file
20
src/features/companies/hooks/useCompanyBranches.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { companiesApi } from '../api';
|
||||||
|
import type { BranchesResponse } from '../types';
|
||||||
|
|
||||||
|
const QUERY_KEY = 'companies';
|
||||||
|
|
||||||
|
// ==================== Company Branches Hook ====================
|
||||||
|
|
||||||
|
export function useCompanyBranches(companyId: string | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [QUERY_KEY, 'branches', companyId],
|
||||||
|
queryFn: () => companiesApi.getBranches(companyId as string),
|
||||||
|
enabled: !!companyId,
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
select: (response: BranchesResponse) => ({
|
||||||
|
branches: response.data,
|
||||||
|
total: response.total,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
38
src/features/companies/hooks/useCompanySettings.ts
Normal file
38
src/features/companies/hooks/useCompanySettings.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { companiesApi } from '../api';
|
||||||
|
import type { CompanySettings } from '../types';
|
||||||
|
|
||||||
|
const QUERY_KEY = 'companies';
|
||||||
|
|
||||||
|
// ==================== Company Settings Hook ====================
|
||||||
|
|
||||||
|
export function useCompanySettings(companyId: string | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [QUERY_KEY, 'settings', companyId],
|
||||||
|
queryFn: () => companiesApi.getSettings(companyId as string),
|
||||||
|
enabled: !!companyId,
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Company Settings Mutations Hook ====================
|
||||||
|
|
||||||
|
export function useCompanySettingsMutations() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: ({ companyId, settings }: { companyId: string; settings: CompanySettings }) =>
|
||||||
|
companiesApi.updateSettings(companyId, settings),
|
||||||
|
onSuccess: (_: unknown, variables: { companyId: string; settings: CompanySettings }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, 'settings', variables.companyId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, 'detail', variables.companyId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateSettings: updateMutation.mutateAsync,
|
||||||
|
isUpdating: updateMutation.isPending,
|
||||||
|
updateError: updateMutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -67,3 +67,49 @@ export interface CompaniesResponse {
|
|||||||
totalPages: number;
|
totalPages: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Branch types for company branches listing
|
||||||
|
export type BranchType = 'headquarters' | 'regional' | 'store' | 'warehouse' | 'office' | 'factory';
|
||||||
|
|
||||||
|
export interface Branch {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
parentId?: string | null;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
shortName?: string | null;
|
||||||
|
branchType: BranchType;
|
||||||
|
phone?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
managerId?: string | null;
|
||||||
|
addressLine1?: string | null;
|
||||||
|
addressLine2?: string | null;
|
||||||
|
city?: string | null;
|
||||||
|
state?: string | null;
|
||||||
|
postalCode?: string | null;
|
||||||
|
country: string;
|
||||||
|
latitude?: number | null;
|
||||||
|
longitude?: number | null;
|
||||||
|
geofenceRadius: number;
|
||||||
|
geofenceEnabled: boolean;
|
||||||
|
timezone: string;
|
||||||
|
currency: string;
|
||||||
|
isActive: boolean;
|
||||||
|
isMain: boolean;
|
||||||
|
operatingHours: Record<string, { open: string; close: string }>;
|
||||||
|
settings: {
|
||||||
|
allowPos?: boolean;
|
||||||
|
allowWarehouse?: boolean;
|
||||||
|
allowCheckIn?: boolean;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
hierarchyPath?: string | null;
|
||||||
|
hierarchyLevel: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BranchesResponse {
|
||||||
|
data: Branch[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|||||||
67
src/features/partners/api/addresses.api.ts
Normal file
67
src/features/partners/api/addresses.api.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { api } from '@services/api/axios-instance';
|
||||||
|
import type {
|
||||||
|
PartnerAddress,
|
||||||
|
CreatePartnerAddressDto,
|
||||||
|
UpdatePartnerAddressDto,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
const BASE_URL = '/api/v1/partners';
|
||||||
|
|
||||||
|
interface AddressesResponse {
|
||||||
|
data: PartnerAddress[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddressResponse {
|
||||||
|
data: PartnerAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addressesApi = {
|
||||||
|
/**
|
||||||
|
* Get all addresses for a partner
|
||||||
|
*/
|
||||||
|
getByPartnerId: async (partnerId: string): Promise<PartnerAddress[]> => {
|
||||||
|
const response = await api.get<AddressesResponse>(`${BASE_URL}/${partnerId}/addresses`);
|
||||||
|
return response.data.data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new address for a partner
|
||||||
|
*/
|
||||||
|
create: async (partnerId: string, data: CreatePartnerAddressDto): Promise<PartnerAddress> => {
|
||||||
|
const response = await api.post<AddressResponse>(`${BASE_URL}/${partnerId}/addresses`, data);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an address
|
||||||
|
*/
|
||||||
|
update: async (
|
||||||
|
partnerId: string,
|
||||||
|
addressId: string,
|
||||||
|
data: UpdatePartnerAddressDto
|
||||||
|
): Promise<PartnerAddress> => {
|
||||||
|
const response = await api.patch<AddressResponse>(
|
||||||
|
`${BASE_URL}/${partnerId}/addresses/${addressId}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an address
|
||||||
|
*/
|
||||||
|
delete: async (partnerId: string, addressId: string): Promise<void> => {
|
||||||
|
await api.delete(`${BASE_URL}/${partnerId}/addresses/${addressId}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an address as default
|
||||||
|
*/
|
||||||
|
setDefault: async (partnerId: string, addressId: string): Promise<PartnerAddress> => {
|
||||||
|
const response = await api.patch<AddressResponse>(
|
||||||
|
`${BASE_URL}/${partnerId}/addresses/${addressId}`,
|
||||||
|
{ isDefault: true }
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
83
src/features/partners/api/bankAccounts.api.ts
Normal file
83
src/features/partners/api/bankAccounts.api.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { api } from '@services/api/axios-instance';
|
||||||
|
import type {
|
||||||
|
PartnerBankAccount,
|
||||||
|
CreatePartnerBankAccountDto,
|
||||||
|
UpdatePartnerBankAccountDto,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
const BASE_URL = '/api/v1/partners';
|
||||||
|
|
||||||
|
interface BankAccountsResponse {
|
||||||
|
data: PartnerBankAccount[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BankAccountResponse {
|
||||||
|
data: PartnerBankAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bankAccountsApi = {
|
||||||
|
/**
|
||||||
|
* Get all bank accounts for a partner
|
||||||
|
*/
|
||||||
|
getByPartnerId: async (partnerId: string): Promise<PartnerBankAccount[]> => {
|
||||||
|
const response = await api.get<BankAccountsResponse>(`${BASE_URL}/${partnerId}/bank-accounts`);
|
||||||
|
return response.data.data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new bank account for a partner
|
||||||
|
*/
|
||||||
|
create: async (
|
||||||
|
partnerId: string,
|
||||||
|
data: CreatePartnerBankAccountDto
|
||||||
|
): Promise<PartnerBankAccount> => {
|
||||||
|
const response = await api.post<BankAccountResponse>(
|
||||||
|
`${BASE_URL}/${partnerId}/bank-accounts`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a bank account
|
||||||
|
*/
|
||||||
|
update: async (
|
||||||
|
partnerId: string,
|
||||||
|
accountId: string,
|
||||||
|
data: UpdatePartnerBankAccountDto
|
||||||
|
): Promise<PartnerBankAccount> => {
|
||||||
|
const response = await api.patch<BankAccountResponse>(
|
||||||
|
`${BASE_URL}/${partnerId}/bank-accounts/${accountId}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a bank account
|
||||||
|
*/
|
||||||
|
delete: async (partnerId: string, accountId: string): Promise<void> => {
|
||||||
|
await api.delete(`${BASE_URL}/${partnerId}/bank-accounts/${accountId}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a bank account
|
||||||
|
*/
|
||||||
|
verify: async (partnerId: string, accountId: string): Promise<PartnerBankAccount> => {
|
||||||
|
const response = await api.post<BankAccountResponse>(
|
||||||
|
`${BASE_URL}/${partnerId}/bank-accounts/${accountId}/verify`
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a bank account as default
|
||||||
|
*/
|
||||||
|
setDefault: async (partnerId: string, accountId: string): Promise<PartnerBankAccount> => {
|
||||||
|
const response = await api.patch<BankAccountResponse>(
|
||||||
|
`${BASE_URL}/${partnerId}/bank-accounts/${accountId}`,
|
||||||
|
{ isDefault: true }
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
67
src/features/partners/api/contacts.api.ts
Normal file
67
src/features/partners/api/contacts.api.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { api } from '@services/api/axios-instance';
|
||||||
|
import type {
|
||||||
|
PartnerContact,
|
||||||
|
CreatePartnerContactDto,
|
||||||
|
UpdatePartnerContactDto,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
const BASE_URL = '/api/v1/partners';
|
||||||
|
|
||||||
|
interface ContactsResponse {
|
||||||
|
data: PartnerContact[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContactResponse {
|
||||||
|
data: PartnerContact;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const contactsApi = {
|
||||||
|
/**
|
||||||
|
* Get all contacts for a partner
|
||||||
|
*/
|
||||||
|
getByPartnerId: async (partnerId: string): Promise<PartnerContact[]> => {
|
||||||
|
const response = await api.get<ContactsResponse>(`${BASE_URL}/${partnerId}/contacts`);
|
||||||
|
return response.data.data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new contact for a partner
|
||||||
|
*/
|
||||||
|
create: async (partnerId: string, data: CreatePartnerContactDto): Promise<PartnerContact> => {
|
||||||
|
const response = await api.post<ContactResponse>(`${BASE_URL}/${partnerId}/contacts`, data);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a contact
|
||||||
|
*/
|
||||||
|
update: async (
|
||||||
|
partnerId: string,
|
||||||
|
contactId: string,
|
||||||
|
data: UpdatePartnerContactDto
|
||||||
|
): Promise<PartnerContact> => {
|
||||||
|
const response = await api.patch<ContactResponse>(
|
||||||
|
`${BASE_URL}/${partnerId}/contacts/${contactId}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a contact
|
||||||
|
*/
|
||||||
|
delete: async (partnerId: string, contactId: string): Promise<void> => {
|
||||||
|
await api.delete(`${BASE_URL}/${partnerId}/contacts/${contactId}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a contact as primary
|
||||||
|
*/
|
||||||
|
setPrimary: async (partnerId: string, contactId: string): Promise<PartnerContact> => {
|
||||||
|
const response = await api.patch<ContactResponse>(
|
||||||
|
`${BASE_URL}/${partnerId}/contacts/${contactId}`,
|
||||||
|
{ isPrimary: true }
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1 +1,4 @@
|
|||||||
export * from './partners.api';
|
export * from './partners.api';
|
||||||
|
export * from './addresses.api';
|
||||||
|
export * from './contacts.api';
|
||||||
|
export * from './bankAccounts.api';
|
||||||
|
|||||||
285
src/features/partners/components/AddressForm.tsx
Normal file
285
src/features/partners/components/AddressForm.tsx
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { Button } from '@components/atoms/Button';
|
||||||
|
import { FormField } from '@components/molecules/FormField';
|
||||||
|
import { Select } from '@components/organisms/Select';
|
||||||
|
import type { PartnerAddress, CreatePartnerAddressDto, UpdatePartnerAddressDto, AddressType } from '../types';
|
||||||
|
|
||||||
|
const addressSchema = z.object({
|
||||||
|
addressType: z.enum(['billing', 'shipping', 'both'] as const),
|
||||||
|
isDefault: z.boolean(),
|
||||||
|
label: z.string().max(100).optional(),
|
||||||
|
street: z.string().min(1, 'La calle es requerida').max(200),
|
||||||
|
exteriorNumber: z.string().max(20).optional(),
|
||||||
|
interiorNumber: z.string().max(20).optional(),
|
||||||
|
neighborhood: z.string().max(100).optional(),
|
||||||
|
city: z.string().min(1, 'La ciudad es requerida').max(100),
|
||||||
|
municipality: z.string().max(100).optional(),
|
||||||
|
state: z.string().min(1, 'El estado es requerido').max(100),
|
||||||
|
postalCode: z.string().min(1, 'El codigo postal es requerido').max(10),
|
||||||
|
country: z.string().max(3).optional(),
|
||||||
|
reference: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof addressSchema>;
|
||||||
|
|
||||||
|
interface AddressFormProps {
|
||||||
|
address?: PartnerAddress;
|
||||||
|
onSubmit: (data: CreatePartnerAddressDto | UpdatePartnerAddressDto) => Promise<void>;
|
||||||
|
onCancel: () => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addressTypeOptions = [
|
||||||
|
{ value: 'billing', label: 'Facturacion' },
|
||||||
|
{ value: 'shipping', label: 'Envio' },
|
||||||
|
{ value: 'both', label: 'Ambos' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mexicanStates = [
|
||||||
|
{ value: 'AGS', label: 'Aguascalientes' },
|
||||||
|
{ value: 'BC', label: 'Baja California' },
|
||||||
|
{ value: 'BCS', label: 'Baja California Sur' },
|
||||||
|
{ value: 'CAM', label: 'Campeche' },
|
||||||
|
{ value: 'CHIS', label: 'Chiapas' },
|
||||||
|
{ value: 'CHIH', label: 'Chihuahua' },
|
||||||
|
{ value: 'CDMX', label: 'Ciudad de Mexico' },
|
||||||
|
{ value: 'COAH', label: 'Coahuila' },
|
||||||
|
{ value: 'COL', label: 'Colima' },
|
||||||
|
{ value: 'DGO', label: 'Durango' },
|
||||||
|
{ value: 'GTO', label: 'Guanajuato' },
|
||||||
|
{ value: 'GRO', label: 'Guerrero' },
|
||||||
|
{ value: 'HGO', label: 'Hidalgo' },
|
||||||
|
{ value: 'JAL', label: 'Jalisco' },
|
||||||
|
{ value: 'MEX', label: 'Estado de Mexico' },
|
||||||
|
{ value: 'MICH', label: 'Michoacan' },
|
||||||
|
{ value: 'MOR', label: 'Morelos' },
|
||||||
|
{ value: 'NAY', label: 'Nayarit' },
|
||||||
|
{ value: 'NL', label: 'Nuevo Leon' },
|
||||||
|
{ value: 'OAX', label: 'Oaxaca' },
|
||||||
|
{ value: 'PUE', label: 'Puebla' },
|
||||||
|
{ value: 'QRO', label: 'Queretaro' },
|
||||||
|
{ value: 'QROO', label: 'Quintana Roo' },
|
||||||
|
{ value: 'SLP', label: 'San Luis Potosi' },
|
||||||
|
{ value: 'SIN', label: 'Sinaloa' },
|
||||||
|
{ value: 'SON', label: 'Sonora' },
|
||||||
|
{ value: 'TAB', label: 'Tabasco' },
|
||||||
|
{ value: 'TAMPS', label: 'Tamaulipas' },
|
||||||
|
{ value: 'TLAX', label: 'Tlaxcala' },
|
||||||
|
{ value: 'VER', label: 'Veracruz' },
|
||||||
|
{ value: 'YUC', label: 'Yucatan' },
|
||||||
|
{ value: 'ZAC', label: 'Zacatecas' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function AddressForm({ address, onSubmit, onCancel, isLoading }: AddressFormProps) {
|
||||||
|
const isEditing = !!address;
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(addressSchema),
|
||||||
|
defaultValues: address
|
||||||
|
? {
|
||||||
|
addressType: address.addressType,
|
||||||
|
isDefault: address.isDefault,
|
||||||
|
label: address.label || '',
|
||||||
|
street: address.street,
|
||||||
|
exteriorNumber: address.exteriorNumber || '',
|
||||||
|
interiorNumber: address.interiorNumber || '',
|
||||||
|
neighborhood: address.neighborhood || '',
|
||||||
|
city: address.city,
|
||||||
|
municipality: address.municipality || '',
|
||||||
|
state: address.state,
|
||||||
|
postalCode: address.postalCode,
|
||||||
|
country: address.country || 'MEX',
|
||||||
|
reference: address.reference || '',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
addressType: 'billing' as AddressType,
|
||||||
|
isDefault: false,
|
||||||
|
label: '',
|
||||||
|
street: '',
|
||||||
|
exteriorNumber: '',
|
||||||
|
interiorNumber: '',
|
||||||
|
neighborhood: '',
|
||||||
|
city: '',
|
||||||
|
municipality: '',
|
||||||
|
state: '',
|
||||||
|
postalCode: '',
|
||||||
|
country: 'MEX',
|
||||||
|
reference: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedType = watch('addressType');
|
||||||
|
const selectedState = watch('state');
|
||||||
|
|
||||||
|
const handleFormSubmit = async (data: FormData) => {
|
||||||
|
const cleanData: CreatePartnerAddressDto | UpdatePartnerAddressDto = {
|
||||||
|
addressType: data.addressType,
|
||||||
|
isDefault: data.isDefault,
|
||||||
|
street: data.street,
|
||||||
|
city: data.city,
|
||||||
|
state: data.state,
|
||||||
|
postalCode: data.postalCode,
|
||||||
|
...(data.label && { label: data.label }),
|
||||||
|
...(data.exteriorNumber && { exteriorNumber: data.exteriorNumber }),
|
||||||
|
...(data.interiorNumber && { interiorNumber: data.interiorNumber }),
|
||||||
|
...(data.neighborhood && { neighborhood: data.neighborhood }),
|
||||||
|
...(data.municipality && { municipality: data.municipality }),
|
||||||
|
...(data.country && { country: data.country }),
|
||||||
|
...(data.reference && { reference: data.reference }),
|
||||||
|
};
|
||||||
|
await onSubmit(cleanData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
|
||||||
|
{/* Address Type */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Tipo de direccion</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<FormField label="Tipo" error={errors.addressType?.message} required>
|
||||||
|
<Select
|
||||||
|
options={addressTypeOptions}
|
||||||
|
value={selectedType}
|
||||||
|
onChange={(value) => setValue('addressType', value as AddressType)}
|
||||||
|
placeholder="Seleccionar tipo..."
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Etiqueta" error={errors.label?.message}>
|
||||||
|
<input
|
||||||
|
{...register('label')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="Ej: Oficina principal"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
{...register('isDefault')}
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">Direccion predeterminada</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Street Address */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Direccion</h3>
|
||||||
|
|
||||||
|
<FormField label="Calle" error={errors.street?.message} required>
|
||||||
|
<input
|
||||||
|
{...register('street')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="Nombre de la calle"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||||
|
<FormField label="No. Exterior" error={errors.exteriorNumber?.message}>
|
||||||
|
<input
|
||||||
|
{...register('exteriorNumber')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="123"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="No. Interior" error={errors.interiorNumber?.message}>
|
||||||
|
<input
|
||||||
|
{...register('interiorNumber')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="A"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Colonia" error={errors.neighborhood?.message}>
|
||||||
|
<input
|
||||||
|
{...register('neighborhood')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="Nombre de la colonia"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Ubicacion</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<FormField label="Ciudad" error={errors.city?.message} required>
|
||||||
|
<input
|
||||||
|
{...register('city')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="Ciudad"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Municipio" error={errors.municipality?.message}>
|
||||||
|
<input
|
||||||
|
{...register('municipality')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="Municipio o delegacion"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<FormField label="Estado" error={errors.state?.message} required>
|
||||||
|
<Select
|
||||||
|
options={mexicanStates}
|
||||||
|
value={selectedState}
|
||||||
|
onChange={(value) => setValue('state', value as string)}
|
||||||
|
placeholder="Seleccionar estado..."
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Codigo Postal" error={errors.postalCode?.message} required>
|
||||||
|
<input
|
||||||
|
{...register('postalCode')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="12345"
|
||||||
|
maxLength={10}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField label="Referencia" error={errors.reference?.message}>
|
||||||
|
<textarea
|
||||||
|
{...register('reference')}
|
||||||
|
rows={2}
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="Entre calles, cerca de..."
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||||
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isLoading}>
|
||||||
|
{isEditing ? 'Guardar cambios' : 'Agregar direccion'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
160
src/features/partners/components/AddressList.tsx
Normal file
160
src/features/partners/components/AddressList.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@components/atoms/Button';
|
||||||
|
import type { PartnerAddress } from '../types';
|
||||||
|
|
||||||
|
interface AddressListProps {
|
||||||
|
addresses: PartnerAddress[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
onAdd: () => void;
|
||||||
|
onEdit: (address: PartnerAddress) => void;
|
||||||
|
onDelete: (addressId: string) => Promise<void>;
|
||||||
|
onSetDefault: (addressId: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addressTypeLabels: Record<string, string> = {
|
||||||
|
billing: 'Facturacion',
|
||||||
|
shipping: 'Envio',
|
||||||
|
both: 'Ambos',
|
||||||
|
};
|
||||||
|
|
||||||
|
function AddressCard({
|
||||||
|
address,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onSetDefault,
|
||||||
|
}: {
|
||||||
|
address: PartnerAddress;
|
||||||
|
onEdit: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
onSetDefault: () => void;
|
||||||
|
}) {
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!confirm('Esta seguro de eliminar esta direccion?')) return;
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await onDelete();
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-gray-200 rounded-lg p-4 hover:border-primary-300 transition-colors">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className="inline-flex items-center rounded-full bg-primary-50 px-2 py-1 text-xs font-medium text-primary-700">
|
||||||
|
{addressTypeLabels[address.addressType]}
|
||||||
|
</span>
|
||||||
|
{address.isDefault && (
|
||||||
|
<span className="inline-flex items-center rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700">
|
||||||
|
Predeterminada
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{address.label && (
|
||||||
|
<span className="text-sm text-gray-500">{address.label}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-900 font-medium">
|
||||||
|
{address.street}
|
||||||
|
{address.exteriorNumber && ` #${address.exteriorNumber}`}
|
||||||
|
{address.interiorNumber && ` Int. ${address.interiorNumber}`}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{address.neighborhood && (
|
||||||
|
<p className="text-sm text-gray-600">Col. {address.neighborhood}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{address.city}
|
||||||
|
{address.municipality && `, ${address.municipality}`}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{address.state}, C.P. {address.postalCode}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{address.reference && (
|
||||||
|
<p className="text-sm text-gray-500 mt-2 italic">
|
||||||
|
Ref: {address.reference}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button size="sm" variant="ghost" onClick={onEdit}>
|
||||||
|
Editar
|
||||||
|
</Button>
|
||||||
|
{!address.isDefault && (
|
||||||
|
<Button size="sm" variant="ghost" onClick={onSetDefault}>
|
||||||
|
Predeterminar
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
onClick={handleDelete}
|
||||||
|
isLoading={isDeleting}
|
||||||
|
>
|
||||||
|
Eliminar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddressList({
|
||||||
|
addresses,
|
||||||
|
isLoading,
|
||||||
|
onAdd,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onSetDefault,
|
||||||
|
}: AddressListProps) {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">
|
||||||
|
Direcciones ({addresses.length})
|
||||||
|
</h3>
|
||||||
|
<Button onClick={onAdd} size="sm">
|
||||||
|
Agregar direccion
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{addresses.length === 0 ? (
|
||||||
|
<div className="text-center py-8 border border-dashed border-gray-300 rounded-lg">
|
||||||
|
<p className="text-gray-500 mb-4">No hay direcciones registradas</p>
|
||||||
|
<Button onClick={onAdd} variant="outline" size="sm">
|
||||||
|
Agregar primera direccion
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{addresses.map((address) => (
|
||||||
|
<AddressCard
|
||||||
|
key={address.id}
|
||||||
|
address={address}
|
||||||
|
onEdit={() => onEdit(address)}
|
||||||
|
onDelete={() => onDelete(address.id)}
|
||||||
|
onSetDefault={() => onSetDefault(address.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
238
src/features/partners/components/ContactForm.tsx
Normal file
238
src/features/partners/components/ContactForm.tsx
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { Button } from '@components/atoms/Button';
|
||||||
|
import { FormField } from '@components/molecules/FormField';
|
||||||
|
import type { PartnerContact, CreatePartnerContactDto, UpdatePartnerContactDto } from '../types';
|
||||||
|
|
||||||
|
const contactSchema = z.object({
|
||||||
|
fullName: z.string().min(2, 'Minimo 2 caracteres').max(200),
|
||||||
|
position: z.string().max(100).optional(),
|
||||||
|
department: z.string().max(100).optional(),
|
||||||
|
email: z.string().email('Email invalido').optional().or(z.literal('')),
|
||||||
|
phone: z.string().max(30).optional(),
|
||||||
|
mobile: z.string().max(30).optional(),
|
||||||
|
extension: z.string().max(30).optional(),
|
||||||
|
isPrimary: z.boolean(),
|
||||||
|
isBillingContact: z.boolean(),
|
||||||
|
isShippingContact: z.boolean(),
|
||||||
|
receivesNotifications: z.boolean(),
|
||||||
|
notes: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof contactSchema>;
|
||||||
|
|
||||||
|
interface ContactFormProps {
|
||||||
|
contact?: PartnerContact;
|
||||||
|
onSubmit: (data: CreatePartnerContactDto | UpdatePartnerContactDto) => Promise<void>;
|
||||||
|
onCancel: () => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContactForm({ contact, onSubmit, onCancel, isLoading }: ContactFormProps) {
|
||||||
|
const isEditing = !!contact;
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(contactSchema),
|
||||||
|
defaultValues: contact
|
||||||
|
? {
|
||||||
|
fullName: contact.fullName,
|
||||||
|
position: contact.position || '',
|
||||||
|
department: contact.department || '',
|
||||||
|
email: contact.email || '',
|
||||||
|
phone: contact.phone || '',
|
||||||
|
mobile: contact.mobile || '',
|
||||||
|
extension: contact.extension || '',
|
||||||
|
isPrimary: contact.isPrimary,
|
||||||
|
isBillingContact: contact.isBillingContact,
|
||||||
|
isShippingContact: contact.isShippingContact,
|
||||||
|
receivesNotifications: contact.receivesNotifications,
|
||||||
|
notes: contact.notes || '',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
fullName: '',
|
||||||
|
position: '',
|
||||||
|
department: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
mobile: '',
|
||||||
|
extension: '',
|
||||||
|
isPrimary: false,
|
||||||
|
isBillingContact: false,
|
||||||
|
isShippingContact: false,
|
||||||
|
receivesNotifications: true,
|
||||||
|
notes: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFormSubmit = async (data: FormData) => {
|
||||||
|
const cleanData: CreatePartnerContactDto | UpdatePartnerContactDto = {
|
||||||
|
fullName: data.fullName,
|
||||||
|
isPrimary: data.isPrimary,
|
||||||
|
isBillingContact: data.isBillingContact,
|
||||||
|
isShippingContact: data.isShippingContact,
|
||||||
|
receivesNotifications: data.receivesNotifications,
|
||||||
|
...(data.position && { position: data.position }),
|
||||||
|
...(data.department && { department: data.department }),
|
||||||
|
...(data.email && { email: data.email }),
|
||||||
|
...(data.phone && { phone: data.phone }),
|
||||||
|
...(data.mobile && { mobile: data.mobile }),
|
||||||
|
...(data.extension && { extension: data.extension }),
|
||||||
|
...(data.notes && { notes: data.notes }),
|
||||||
|
};
|
||||||
|
await onSubmit(cleanData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
|
||||||
|
{/* Basic Info */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Informacion basica</h3>
|
||||||
|
|
||||||
|
<FormField label="Nombre completo" error={errors.fullName?.message} required>
|
||||||
|
<input
|
||||||
|
{...register('fullName')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="Nombre del contacto"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<FormField label="Puesto" error={errors.position?.message}>
|
||||||
|
<input
|
||||||
|
{...register('position')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="Ej: Gerente de compras"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Departamento" error={errors.department?.message}>
|
||||||
|
<input
|
||||||
|
{...register('department')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="Ej: Compras"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Info */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Informacion de contacto</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<FormField label="Email" error={errors.email?.message}>
|
||||||
|
<input
|
||||||
|
{...register('email')}
|
||||||
|
type="email"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="email@ejemplo.com"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Telefono" error={errors.phone?.message}>
|
||||||
|
<input
|
||||||
|
{...register('phone')}
|
||||||
|
type="tel"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="+52 55 1234 5678"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<FormField label="Movil" error={errors.mobile?.message}>
|
||||||
|
<input
|
||||||
|
{...register('mobile')}
|
||||||
|
type="tel"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="+52 55 8765 4321"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Extension" error={errors.extension?.message}>
|
||||||
|
<input
|
||||||
|
{...register('extension')}
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="123"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Roles */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Roles del contacto</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
{...register('isPrimary')}
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">Contacto principal</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
{...register('isBillingContact')}
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">Contacto de facturacion</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
{...register('isShippingContact')}
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">Contacto de envios</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
{...register('receivesNotifications')}
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">Recibe notificaciones</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notes */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Notas</h3>
|
||||||
|
|
||||||
|
<FormField label="Notas" error={errors.notes?.message}>
|
||||||
|
<textarea
|
||||||
|
{...register('notes')}
|
||||||
|
rows={3}
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
placeholder="Notas adicionales sobre el contacto..."
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||||
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isLoading}>
|
||||||
|
{isEditing ? 'Guardar cambios' : 'Agregar contacto'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
192
src/features/partners/components/ContactList.tsx
Normal file
192
src/features/partners/components/ContactList.tsx
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@components/atoms/Button';
|
||||||
|
import type { PartnerContact } from '../types';
|
||||||
|
|
||||||
|
interface ContactListProps {
|
||||||
|
contacts: PartnerContact[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
onAdd: () => void;
|
||||||
|
onEdit: (contact: PartnerContact) => void;
|
||||||
|
onDelete: (contactId: string) => Promise<void>;
|
||||||
|
onSetPrimary: (contactId: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContactCard({
|
||||||
|
contact,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onSetPrimary,
|
||||||
|
}: {
|
||||||
|
contact: PartnerContact;
|
||||||
|
onEdit: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
onSetPrimary: () => void;
|
||||||
|
}) {
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!confirm('Esta seguro de eliminar este contacto?')) return;
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await onDelete();
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const roles: string[] = [];
|
||||||
|
if (contact.isPrimary) roles.push('Principal');
|
||||||
|
if (contact.isBillingContact) roles.push('Facturacion');
|
||||||
|
if (contact.isShippingContact) roles.push('Envios');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-gray-200 rounded-lg p-4 hover:border-primary-300 transition-colors">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<h4 className="text-sm font-semibold text-gray-900">{contact.fullName}</h4>
|
||||||
|
{contact.isPrimary && (
|
||||||
|
<span className="inline-flex items-center rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700">
|
||||||
|
Principal
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{contact.position && (
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{contact.position}
|
||||||
|
{contact.department && ` - ${contact.department}`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{contact.email && (
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
<span className="font-medium">Email:</span>{' '}
|
||||||
|
<a href={`mailto:${contact.email}`} className="text-primary-600 hover:underline">
|
||||||
|
{contact.email}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{contact.phone && (
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
<span className="font-medium">Tel:</span>{' '}
|
||||||
|
<a href={`tel:${contact.phone}`} className="text-primary-600 hover:underline">
|
||||||
|
{contact.phone}
|
||||||
|
</a>
|
||||||
|
{contact.extension && ` ext. ${contact.extension}`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{contact.mobile && (
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
<span className="font-medium">Movil:</span>{' '}
|
||||||
|
<a href={`tel:${contact.mobile}`} className="text-primary-600 hover:underline">
|
||||||
|
{contact.mobile}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{roles.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1 mt-3">
|
||||||
|
{contact.isBillingContact && (
|
||||||
|
<span className="inline-flex items-center rounded-full bg-blue-50 px-2 py-0.5 text-xs font-medium text-blue-700">
|
||||||
|
Facturacion
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{contact.isShippingContact && (
|
||||||
|
<span className="inline-flex items-center rounded-full bg-purple-50 px-2 py-0.5 text-xs font-medium text-purple-700">
|
||||||
|
Envios
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{contact.receivesNotifications && (
|
||||||
|
<span className="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600">
|
||||||
|
Notificaciones
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{contact.notes && (
|
||||||
|
<p className="text-sm text-gray-500 mt-2 italic">
|
||||||
|
{contact.notes}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button size="sm" variant="ghost" onClick={onEdit}>
|
||||||
|
Editar
|
||||||
|
</Button>
|
||||||
|
{!contact.isPrimary && (
|
||||||
|
<Button size="sm" variant="ghost" onClick={onSetPrimary}>
|
||||||
|
Hacer principal
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
onClick={handleDelete}
|
||||||
|
isLoading={isDeleting}
|
||||||
|
>
|
||||||
|
Eliminar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContactList({
|
||||||
|
contacts,
|
||||||
|
isLoading,
|
||||||
|
onAdd,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onSetPrimary,
|
||||||
|
}: ContactListProps) {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">
|
||||||
|
Contactos ({contacts.length})
|
||||||
|
</h3>
|
||||||
|
<Button onClick={onAdd} size="sm">
|
||||||
|
Agregar contacto
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{contacts.length === 0 ? (
|
||||||
|
<div className="text-center py-8 border border-dashed border-gray-300 rounded-lg">
|
||||||
|
<p className="text-gray-500 mb-4">No hay contactos registrados</p>
|
||||||
|
<Button onClick={onAdd} variant="outline" size="sm">
|
||||||
|
Agregar primer contacto
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{contacts.map((contact) => (
|
||||||
|
<ContactCard
|
||||||
|
key={contact.id}
|
||||||
|
contact={contact}
|
||||||
|
onEdit={() => onEdit(contact)}
|
||||||
|
onDelete={() => onDelete(contact.id)}
|
||||||
|
onSetPrimary={() => onSetPrimary(contact.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,3 +2,7 @@ export * from './PartnerForm';
|
|||||||
export * from './PartnerFiltersPanel';
|
export * from './PartnerFiltersPanel';
|
||||||
export * from './PartnerTypeBadge';
|
export * from './PartnerTypeBadge';
|
||||||
export * from './PartnerStatusBadge';
|
export * from './PartnerStatusBadge';
|
||||||
|
export * from './AddressForm';
|
||||||
|
export * from './AddressList';
|
||||||
|
export * from './ContactForm';
|
||||||
|
export * from './ContactList';
|
||||||
|
|||||||
@ -1 +1,4 @@
|
|||||||
export * from './usePartners';
|
export * from './usePartners';
|
||||||
|
export * from './usePartnerAddresses';
|
||||||
|
export * from './usePartnerContacts';
|
||||||
|
export * from './usePartnerBankAccounts';
|
||||||
|
|||||||
88
src/features/partners/hooks/usePartnerAddresses.ts
Normal file
88
src/features/partners/hooks/usePartnerAddresses.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { addressesApi } from '../api/addresses.api';
|
||||||
|
import type {
|
||||||
|
CreatePartnerAddressDto,
|
||||||
|
UpdatePartnerAddressDto,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
const QUERY_KEY = 'partner-addresses';
|
||||||
|
|
||||||
|
// ==================== Addresses List Hook ====================
|
||||||
|
|
||||||
|
export function usePartnerAddresses(partnerId: string | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [QUERY_KEY, partnerId],
|
||||||
|
queryFn: () => addressesApi.getByPartnerId(partnerId as string),
|
||||||
|
enabled: !!partnerId,
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Address Mutations Hook ====================
|
||||||
|
|
||||||
|
export function usePartnerAddressMutations(partnerId: string) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: (data: CreatePartnerAddressDto) => addressesApi.create(partnerId, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, partnerId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: ({ addressId, data }: { addressId: string; data: UpdatePartnerAddressDto }) =>
|
||||||
|
addressesApi.update(partnerId, addressId, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, partnerId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: (addressId: string) => addressesApi.delete(partnerId, addressId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, partnerId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const setDefaultMutation = useMutation({
|
||||||
|
mutationFn: (addressId: string) => addressesApi.setDefault(partnerId, addressId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, partnerId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
create: createMutation.mutateAsync,
|
||||||
|
update: updateMutation.mutateAsync,
|
||||||
|
delete: deleteMutation.mutateAsync,
|
||||||
|
setDefault: setDefaultMutation.mutateAsync,
|
||||||
|
isCreating: createMutation.isPending,
|
||||||
|
isUpdating: updateMutation.isPending,
|
||||||
|
isDeleting: deleteMutation.isPending,
|
||||||
|
isSettingDefault: setDefaultMutation.isPending,
|
||||||
|
createError: createMutation.error,
|
||||||
|
updateError: updateMutation.error,
|
||||||
|
deleteError: deleteMutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Combined Hook ====================
|
||||||
|
|
||||||
|
export function usePartnerAddressesWithMutations(partnerId: string | null | undefined) {
|
||||||
|
const { data, isLoading, error, refetch } = usePartnerAddresses(partnerId);
|
||||||
|
const mutations = usePartnerAddressMutations(partnerId || '');
|
||||||
|
|
||||||
|
return {
|
||||||
|
addresses: data || [],
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
...mutations,
|
||||||
|
// Disable mutations if no partnerId
|
||||||
|
create: partnerId ? mutations.create : async () => { throw new Error('Partner ID required'); },
|
||||||
|
update: partnerId ? mutations.update : async () => { throw new Error('Partner ID required'); },
|
||||||
|
delete: partnerId ? mutations.delete : async () => { throw new Error('Partner ID required'); },
|
||||||
|
setDefault: partnerId ? mutations.setDefault : async () => { throw new Error('Partner ID required'); },
|
||||||
|
};
|
||||||
|
}
|
||||||
98
src/features/partners/hooks/usePartnerBankAccounts.ts
Normal file
98
src/features/partners/hooks/usePartnerBankAccounts.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { bankAccountsApi } from '../api/bankAccounts.api';
|
||||||
|
import type {
|
||||||
|
CreatePartnerBankAccountDto,
|
||||||
|
UpdatePartnerBankAccountDto,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
const QUERY_KEY = 'partner-bank-accounts';
|
||||||
|
|
||||||
|
// ==================== Bank Accounts List Hook ====================
|
||||||
|
|
||||||
|
export function usePartnerBankAccounts(partnerId: string | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [QUERY_KEY, partnerId],
|
||||||
|
queryFn: () => bankAccountsApi.getByPartnerId(partnerId as string),
|
||||||
|
enabled: !!partnerId,
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Bank Account Mutations Hook ====================
|
||||||
|
|
||||||
|
export function usePartnerBankAccountMutations(partnerId: string) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: (data: CreatePartnerBankAccountDto) => bankAccountsApi.create(partnerId, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, partnerId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: ({ accountId, data }: { accountId: string; data: UpdatePartnerBankAccountDto }) =>
|
||||||
|
bankAccountsApi.update(partnerId, accountId, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, partnerId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: (accountId: string) => bankAccountsApi.delete(partnerId, accountId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, partnerId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const verifyMutation = useMutation({
|
||||||
|
mutationFn: (accountId: string) => bankAccountsApi.verify(partnerId, accountId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, partnerId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const setDefaultMutation = useMutation({
|
||||||
|
mutationFn: (accountId: string) => bankAccountsApi.setDefault(partnerId, accountId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, partnerId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
create: createMutation.mutateAsync,
|
||||||
|
update: updateMutation.mutateAsync,
|
||||||
|
delete: deleteMutation.mutateAsync,
|
||||||
|
verify: verifyMutation.mutateAsync,
|
||||||
|
setDefault: setDefaultMutation.mutateAsync,
|
||||||
|
isCreating: createMutation.isPending,
|
||||||
|
isUpdating: updateMutation.isPending,
|
||||||
|
isDeleting: deleteMutation.isPending,
|
||||||
|
isVerifying: verifyMutation.isPending,
|
||||||
|
isSettingDefault: setDefaultMutation.isPending,
|
||||||
|
createError: createMutation.error,
|
||||||
|
updateError: updateMutation.error,
|
||||||
|
deleteError: deleteMutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Combined Hook ====================
|
||||||
|
|
||||||
|
export function usePartnerBankAccountsWithMutations(partnerId: string | null | undefined) {
|
||||||
|
const { data, isLoading, error, refetch } = usePartnerBankAccounts(partnerId);
|
||||||
|
const mutations = usePartnerBankAccountMutations(partnerId || '');
|
||||||
|
|
||||||
|
return {
|
||||||
|
bankAccounts: data || [],
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
...mutations,
|
||||||
|
// Disable mutations if no partnerId
|
||||||
|
create: partnerId ? mutations.create : async () => { throw new Error('Partner ID required'); },
|
||||||
|
update: partnerId ? mutations.update : async () => { throw new Error('Partner ID required'); },
|
||||||
|
delete: partnerId ? mutations.delete : async () => { throw new Error('Partner ID required'); },
|
||||||
|
verify: partnerId ? mutations.verify : async () => { throw new Error('Partner ID required'); },
|
||||||
|
setDefault: partnerId ? mutations.setDefault : async () => { throw new Error('Partner ID required'); },
|
||||||
|
};
|
||||||
|
}
|
||||||
88
src/features/partners/hooks/usePartnerContacts.ts
Normal file
88
src/features/partners/hooks/usePartnerContacts.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { contactsApi } from '../api/contacts.api';
|
||||||
|
import type {
|
||||||
|
CreatePartnerContactDto,
|
||||||
|
UpdatePartnerContactDto,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
const QUERY_KEY = 'partner-contacts';
|
||||||
|
|
||||||
|
// ==================== Contacts List Hook ====================
|
||||||
|
|
||||||
|
export function usePartnerContacts(partnerId: string | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [QUERY_KEY, partnerId],
|
||||||
|
queryFn: () => contactsApi.getByPartnerId(partnerId as string),
|
||||||
|
enabled: !!partnerId,
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Contact Mutations Hook ====================
|
||||||
|
|
||||||
|
export function usePartnerContactMutations(partnerId: string) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: (data: CreatePartnerContactDto) => contactsApi.create(partnerId, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, partnerId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: ({ contactId, data }: { contactId: string; data: UpdatePartnerContactDto }) =>
|
||||||
|
contactsApi.update(partnerId, contactId, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, partnerId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: (contactId: string) => contactsApi.delete(partnerId, contactId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, partnerId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const setPrimaryMutation = useMutation({
|
||||||
|
mutationFn: (contactId: string) => contactsApi.setPrimary(partnerId, contactId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, partnerId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
create: createMutation.mutateAsync,
|
||||||
|
update: updateMutation.mutateAsync,
|
||||||
|
delete: deleteMutation.mutateAsync,
|
||||||
|
setPrimary: setPrimaryMutation.mutateAsync,
|
||||||
|
isCreating: createMutation.isPending,
|
||||||
|
isUpdating: updateMutation.isPending,
|
||||||
|
isDeleting: deleteMutation.isPending,
|
||||||
|
isSettingPrimary: setPrimaryMutation.isPending,
|
||||||
|
createError: createMutation.error,
|
||||||
|
updateError: updateMutation.error,
|
||||||
|
deleteError: deleteMutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Combined Hook ====================
|
||||||
|
|
||||||
|
export function usePartnerContactsWithMutations(partnerId: string | null | undefined) {
|
||||||
|
const { data, isLoading, error, refetch } = usePartnerContacts(partnerId);
|
||||||
|
const mutations = usePartnerContactMutations(partnerId || '');
|
||||||
|
|
||||||
|
return {
|
||||||
|
contacts: data || [],
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
...mutations,
|
||||||
|
// Disable mutations if no partnerId
|
||||||
|
create: partnerId ? mutations.create : async () => { throw new Error('Partner ID required'); },
|
||||||
|
update: partnerId ? mutations.update : async () => { throw new Error('Partner ID required'); },
|
||||||
|
delete: partnerId ? mutations.delete : async () => { throw new Error('Partner ID required'); },
|
||||||
|
setPrimary: partnerId ? mutations.setPrimary : async () => { throw new Error('Partner ID required'); },
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -100,3 +100,168 @@ export interface PartnersResponse {
|
|||||||
totalPages: number;
|
totalPages: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Partner Address Types ====================
|
||||||
|
|
||||||
|
export type AddressType = 'billing' | 'shipping' | 'both';
|
||||||
|
|
||||||
|
export interface PartnerAddress {
|
||||||
|
id: string;
|
||||||
|
partnerId: string;
|
||||||
|
addressType: AddressType;
|
||||||
|
isDefault: boolean;
|
||||||
|
label?: string | null;
|
||||||
|
street: string;
|
||||||
|
exteriorNumber?: string | null;
|
||||||
|
interiorNumber?: string | null;
|
||||||
|
neighborhood?: string | null;
|
||||||
|
city: string;
|
||||||
|
municipality?: string | null;
|
||||||
|
state: string;
|
||||||
|
postalCode: string;
|
||||||
|
country: string;
|
||||||
|
reference?: string | null;
|
||||||
|
latitude?: number | null;
|
||||||
|
longitude?: number | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatePartnerAddressDto {
|
||||||
|
addressType?: AddressType;
|
||||||
|
isDefault?: boolean;
|
||||||
|
label?: string;
|
||||||
|
street: string;
|
||||||
|
exteriorNumber?: string;
|
||||||
|
interiorNumber?: string;
|
||||||
|
neighborhood?: string;
|
||||||
|
city: string;
|
||||||
|
municipality?: string;
|
||||||
|
state: string;
|
||||||
|
postalCode: string;
|
||||||
|
country?: string;
|
||||||
|
reference?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdatePartnerAddressDto {
|
||||||
|
addressType?: AddressType;
|
||||||
|
isDefault?: boolean;
|
||||||
|
label?: string;
|
||||||
|
street?: string;
|
||||||
|
exteriorNumber?: string;
|
||||||
|
interiorNumber?: string;
|
||||||
|
neighborhood?: string;
|
||||||
|
city?: string;
|
||||||
|
municipality?: string;
|
||||||
|
state?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
country?: string;
|
||||||
|
reference?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Partner Contact Types ====================
|
||||||
|
|
||||||
|
export interface PartnerContact {
|
||||||
|
id: string;
|
||||||
|
partnerId: string;
|
||||||
|
fullName: string;
|
||||||
|
position?: string | null;
|
||||||
|
department?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
phone?: string | null;
|
||||||
|
mobile?: string | null;
|
||||||
|
extension?: string | null;
|
||||||
|
isPrimary: boolean;
|
||||||
|
isBillingContact: boolean;
|
||||||
|
isShippingContact: boolean;
|
||||||
|
receivesNotifications: boolean;
|
||||||
|
notes?: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatePartnerContactDto {
|
||||||
|
fullName: string;
|
||||||
|
position?: string;
|
||||||
|
department?: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
mobile?: string;
|
||||||
|
extension?: string;
|
||||||
|
isPrimary?: boolean;
|
||||||
|
isBillingContact?: boolean;
|
||||||
|
isShippingContact?: boolean;
|
||||||
|
receivesNotifications?: boolean;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdatePartnerContactDto {
|
||||||
|
fullName?: string;
|
||||||
|
position?: string;
|
||||||
|
department?: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
mobile?: string;
|
||||||
|
extension?: string;
|
||||||
|
isPrimary?: boolean;
|
||||||
|
isBillingContact?: boolean;
|
||||||
|
isShippingContact?: boolean;
|
||||||
|
receivesNotifications?: boolean;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Partner Bank Account Types ====================
|
||||||
|
|
||||||
|
export type BankAccountType = 'checking' | 'savings';
|
||||||
|
|
||||||
|
export interface PartnerBankAccount {
|
||||||
|
id: string;
|
||||||
|
partnerId: string;
|
||||||
|
bankName: string;
|
||||||
|
bankCode?: string | null;
|
||||||
|
accountNumber: string;
|
||||||
|
clabe?: string | null;
|
||||||
|
accountType: BankAccountType;
|
||||||
|
currency: string;
|
||||||
|
beneficiaryName?: string | null;
|
||||||
|
beneficiaryTaxId?: string | null;
|
||||||
|
swiftCode?: string | null;
|
||||||
|
isDefault: boolean;
|
||||||
|
isVerified: boolean;
|
||||||
|
verifiedAt?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatePartnerBankAccountDto {
|
||||||
|
bankName: string;
|
||||||
|
bankCode?: string;
|
||||||
|
accountNumber: string;
|
||||||
|
clabe?: string;
|
||||||
|
accountType?: BankAccountType;
|
||||||
|
currency?: string;
|
||||||
|
beneficiaryName?: string;
|
||||||
|
beneficiaryTaxId?: string;
|
||||||
|
swiftCode?: string;
|
||||||
|
isDefault?: boolean;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdatePartnerBankAccountDto {
|
||||||
|
bankName?: string;
|
||||||
|
bankCode?: string;
|
||||||
|
accountNumber?: string;
|
||||||
|
clabe?: string;
|
||||||
|
accountType?: BankAccountType;
|
||||||
|
currency?: string;
|
||||||
|
beneficiaryName?: string;
|
||||||
|
beneficiaryTaxId?: string;
|
||||||
|
swiftCode?: string;
|
||||||
|
isDefault?: boolean;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user