169 lines
9.5 KiB
JavaScript
169 lines
9.5 KiB
JavaScript
// src/components/crm/ClientsSection.jsx
|
|
// Prikaz liste klijenata i njihovih dizalica unutar dashboard sekcije.
|
|
|
|
import { useStore } from '@nanostores/preact';
|
|
import { useEffect, useState } from 'preact/hooks';
|
|
import { animated, useTransition } from '@react-spring/web';
|
|
import { $clients, $clientsLoading, fetchClients } from '../../stores/clientStore';
|
|
import { $cranes } from '../../stores/fleetDashboardStore';
|
|
import AnimatedDataTable from '../ui/AnimatedDataTable';
|
|
import ServiceRecordCreateButton from '../fleet/ServiceRecordCreateButton';
|
|
import { showToast } from '../../stores/toastStore';
|
|
|
|
export default function ClientsSection({ onSetServiceContext = null }) {
|
|
const clients = useStore($clients);
|
|
const loading = useStore($clientsLoading);
|
|
const cranes = useStore($cranes);
|
|
const [expandedId, setExpandedId] = useState(null);
|
|
const [hasMounted, setHasMounted] = useState(false);
|
|
const [selectedClientCraneId, setSelectedClientCraneId] = useState(null);
|
|
|
|
useEffect(() => {
|
|
setHasMounted(true);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!hasMounted) return;
|
|
if (clients.length === 0) fetchClients();
|
|
}, [hasMounted, clients.length]);
|
|
|
|
function cranesForClient(clientId) {
|
|
return cranes.filter((item) => String(item.client) === String(clientId));
|
|
}
|
|
|
|
const clientTransitions = useTransition(clients, {
|
|
keys: (client) => client.id,
|
|
from: { opacity: 0, transform: 'translate3d(0,8px,0)' },
|
|
enter: { opacity: 1, transform: 'translate3d(0,0,0)' },
|
|
leave: { opacity: 0, transform: 'translate3d(0,-8px,0)' },
|
|
trail: 30,
|
|
config: { tension: 230, friction: 26 },
|
|
});
|
|
|
|
function handleSetServiceContext() {
|
|
if (!selectedClientCraneId) {
|
|
showToast('Prvo označite redak dizalice.', 'warning');
|
|
return;
|
|
}
|
|
onSetServiceContext?.(selectedClientCraneId);
|
|
}
|
|
|
|
return (
|
|
<article className="overflow-hidden rounded-xl border border-border-hairline bg-canvas-elevated shadow-sm">
|
|
<div className="flex items-center justify-between border-b border-border-hairline px-4 py-3">
|
|
<h3 className="font-semibold text-text-main">Klijenti</h3>
|
|
<div className="flex items-center gap-3">
|
|
<ServiceRecordCreateButton
|
|
onClick={handleSetServiceContext}
|
|
label="Postavi servisni kontekst"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => fetchClients()}
|
|
className="text-sm font-medium text-indigo-600 hover:text-indigo-800"
|
|
>
|
|
Osvježi
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{!hasMounted && (
|
|
<p className="px-4 py-6 text-center text-sm text-text-muted">Učitavanje…</p>
|
|
)}
|
|
|
|
{hasMounted && loading && (
|
|
<p className="px-4 py-6 text-center text-sm text-text-muted">Učitavanje…</p>
|
|
)}
|
|
|
|
{hasMounted && !loading && clients.length === 0 && (
|
|
<p className="px-4 py-6 text-center text-sm text-text-muted">
|
|
Nema klijenata. Dodajte klijente u Django admin ili koristite CRM API.
|
|
</p>
|
|
)}
|
|
|
|
{hasMounted && !loading && clients.length > 0 && (
|
|
<div className="divide-y divide-border-hairline">
|
|
{clientTransitions((style, client) => {
|
|
const cvs = cranesForClient(client.id);
|
|
const isOpen = expandedId === client.id;
|
|
const craneColumns = [
|
|
{ key: 'registration', label: 'Oznaka', className: 'py-2 pr-4 text-left' },
|
|
{ key: 'model', label: 'Make / Model', className: 'py-2 pr-4 text-left' },
|
|
{ key: 'km', label: 'Km', className: 'py-2 pr-4 text-left' },
|
|
{ key: 'nextService', label: 'Sljedeći servis', className: 'py-2 pr-4 text-left' },
|
|
{ key: 'type', label: 'Vrsta', className: 'py-2 text-left' },
|
|
];
|
|
return (
|
|
<animated.div key={client.id} style={style}>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setExpandedId(isOpen ? null : client.id);
|
|
setSelectedClientCraneId(null);
|
|
}}
|
|
className="flex w-full items-center justify-between px-4 py-3 text-left hover:bg-canvas-deep"
|
|
>
|
|
<div>
|
|
<span className="font-medium text-text-main">{client.name}</span>
|
|
<span className="ml-2 text-xs text-text-muted">{client.tax_id}</span>
|
|
<span className="ml-3 text-xs text-text-muted">{client.city}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="rounded-full bg-indigo-50 px-2 py-0.5 text-xs text-indigo-700">
|
|
{cvs.length} dizalica
|
|
</span>
|
|
<span className="text-text-muted">{isOpen ? '▲' : '▼'}</span>
|
|
</div>
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div className="border-t border-border-hairline bg-canvas-base/50 px-4 py-3">
|
|
{cvs.length === 0 ? (
|
|
<p className="text-sm text-text-muted">
|
|
Nema dizalica dodijeljenih ovom klijentu.
|
|
Dodajte dizalicu u Django admin i povežite je s klijentom.
|
|
</p>
|
|
) : (
|
|
<AnimatedDataTable
|
|
columns={craneColumns}
|
|
rows={cvs}
|
|
rowKey={(item) => item.id}
|
|
headClassName="text-xs uppercase tracking-wide text-text-muted"
|
|
bodyClassName="divide-y divide-border-hairline"
|
|
rowClassName="hover:bg-canvas-deep"
|
|
isRowSelected={(item) => String(item.id) === String(selectedClientCraneId)}
|
|
tableClassName="min-w-full text-sm"
|
|
wrapperClassName="overflow-x-auto"
|
|
renderRow={(item) => (
|
|
<>
|
|
<td className="py-2 pr-4 font-medium cursor-pointer" onClick={() => setSelectedClientCraneId(item.id)}>{item.registration_number}</td>
|
|
<td className="py-2 pr-4 cursor-pointer" onClick={() => setSelectedClientCraneId(item.id)}>{item.make} {item.model} {item.year ? `(${item.year})` : ''}</td>
|
|
<td className="py-2 pr-4 cursor-pointer" onClick={() => setSelectedClientCraneId(item.id)}>{item.current_mileage?.toLocaleString('hr-HR')} km</td>
|
|
<td className="py-2 pr-4 cursor-pointer" onClick={() => setSelectedClientCraneId(item.id)}>
|
|
{item.service_interval_km
|
|
? `${(Math.floor(item.current_mileage / item.service_interval_km) + 1) * item.service_interval_km} km`
|
|
: '—'}
|
|
</td>
|
|
<td className="py-2 cursor-pointer" onClick={() => setSelectedClientCraneId(item.id)}>
|
|
<span className={item.is_company_vehicle
|
|
? 'rounded-full bg-indigo-50 px-2 py-0.5 text-xs text-indigo-700'
|
|
: 'rounded-full bg-emerald-50 px-2 py-0.5 text-xs text-emerald-700'
|
|
}>
|
|
{item.is_company_vehicle ? 'Fleet' : 'Kupac'}
|
|
</span>
|
|
</td>
|
|
</>
|
|
)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
</animated.div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</article>
|
|
);
|
|
}
|