Files
004.erp.generic/frontend/src/components/crm/ClientsSection.jsx
mariomitte 4e3bf6dab0
Some checks failed
ERP CI/CD Pipeline / test (push) Has been cancelled
ERP CI/CD Pipeline / Deploy (server git pull + compose) (push) Has been cancelled
patch oko teksta PDFa i UI tablica
2026-07-12 03:25:03 +02:00

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>
);
}