2026-06-29 11:01:39 +02:00
|
|
|
import React from 'react';
|
|
|
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
import { useCompany } from '@/api/hooks';
|
|
|
|
|
import { Tabs, TabItem } from '@/components/shared/Tabs';
|
|
|
|
|
import { Card } from '@/components/ui/Card';
|
|
|
|
|
import { Button } from '@/components/ui/Button';
|
|
|
|
|
import { Badge } from '@/components/ui/Badge';
|
|
|
|
|
import { EmptyState } from '@/components/ui/EmptyState';
|
|
|
|
|
import { Skeleton } from '@/components/ui/Skeleton';
|
|
|
|
|
import { Avatar } from '@/components/ui/Avatar';
|
2026-07-01 16:54:32 +02:00
|
|
|
import { TagPicker } from '@/components/tags/TagPicker';
|
2026-06-29 11:01:39 +02:00
|
|
|
|
|
|
|
|
export function CompanyDetailPage() {
|
|
|
|
|
const { id } = useParams<{ id: string }>();
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const { data: company, isLoading, isError } = useCompany(id);
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-6 max-w-7xl mx-auto" data-testid="company-detail-page">
|
|
|
|
|
<Skeleton className="h-8 w-64 mb-6" />
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
<Skeleton className="h-32" />
|
|
|
|
|
<Skeleton className="h-32" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isError || !company) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-6 max-w-7xl mx-auto" data-testid="company-detail-page">
|
|
|
|
|
<EmptyState
|
|
|
|
|
title={t('companies.notFound')}
|
|
|
|
|
action={<Button onClick={() => navigate('/companies')}>{t('common.back')}</Button>}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const contacts = company.contacts ?? [];
|
|
|
|
|
|
|
|
|
|
const overviewTab: TabItem = {
|
|
|
|
|
key: 'overview',
|
|
|
|
|
label: t('companies.overview'),
|
|
|
|
|
content: (
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
<Card title={t('companies.name')}>
|
|
|
|
|
<p className="text-secondary-900">{company.name}</p>
|
|
|
|
|
</Card>
|
|
|
|
|
<Card title={t('companies.accountNumber')}>
|
|
|
|
|
<p className="text-secondary-900">{company.account_number || '—'}</p>
|
|
|
|
|
</Card>
|
|
|
|
|
<Card title={t('companies.industry')}>
|
|
|
|
|
{company.industry ? <Badge variant="primary">{company.industry}</Badge> : <p className="text-secondary-500">—</p>}
|
|
|
|
|
</Card>
|
|
|
|
|
<Card title={t('companies.phone')}>
|
|
|
|
|
<p className="text-secondary-900">{company.phone || '—'}</p>
|
|
|
|
|
</Card>
|
|
|
|
|
<Card title={t('companies.email')}>
|
|
|
|
|
{company.email ? (
|
|
|
|
|
<a href={`mailto:${company.email}`} className="text-primary-600 hover:text-primary-700">{company.email}</a>
|
|
|
|
|
) : <p className="text-secondary-500">—</p>}
|
|
|
|
|
</Card>
|
|
|
|
|
<Card title={t('companies.website')}>
|
|
|
|
|
{company.website ? (
|
|
|
|
|
<a href={company.website} target="_blank" rel="noopener noreferrer" className="text-primary-600 hover:text-primary-700">{company.website}</a>
|
|
|
|
|
) : <p className="text-secondary-500">—</p>}
|
|
|
|
|
</Card>
|
|
|
|
|
{company.description && (
|
|
|
|
|
<Card title={t('companies.description')}>
|
|
|
|
|
<p className="text-secondary-700 text-sm whitespace-pre-wrap">{company.description}</p>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const contactsTab: TabItem = {
|
|
|
|
|
key: 'contacts',
|
|
|
|
|
label: t('companies.contacts'),
|
|
|
|
|
badge: contacts.length,
|
|
|
|
|
content: contacts.length === 0 ? (
|
|
|
|
|
<EmptyState title={t('companies.noContacts')} />
|
|
|
|
|
) : (
|
|
|
|
|
<ul className="space-y-2" role="list">
|
|
|
|
|
{contacts.map((contact) => (
|
|
|
|
|
<li key={contact.id}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => navigate(`/contacts/${contact.id}`)}
|
|
|
|
|
className="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-secondary-50 text-left min-h-touch"
|
|
|
|
|
>
|
|
|
|
|
<Avatar name={`${contact.first_name} ${contact.last_name}`} size="sm" />
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<p className="font-medium text-secondary-900">{contact.first_name} {contact.last_name}</p>
|
|
|
|
|
<p className="text-sm text-secondary-500 truncate">{contact.email}{contact.position ? ` · ${contact.position}` : ''}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-01 16:54:32 +02:00
|
|
|
const tagsTab: TabItem = {
|
|
|
|
|
key: 'tags',
|
|
|
|
|
label: t('tags.title'),
|
|
|
|
|
content: (
|
|
|
|
|
<TagPicker entityType="company" entityId={company.id} />
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-29 11:01:39 +02:00
|
|
|
const filesTab: TabItem = {
|
|
|
|
|
key: 'files',
|
|
|
|
|
label: t('companies.files'),
|
|
|
|
|
content: <EmptyState title={t('companies.noFiles')} />,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const activityTab: TabItem = {
|
|
|
|
|
key: 'activity',
|
|
|
|
|
label: t('companies.activity'),
|
|
|
|
|
content: <EmptyState title={t('companies.noActivity')} />,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-6 max-w-7xl mx-auto" data-testid="company-detail-page">
|
|
|
|
|
<div className="flex items-center justify-between mb-6">
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<Button variant="ghost" onClick={() => navigate('/companies')} aria-label={t('common.back')}>
|
|
|
|
|
← {t('common.back')}
|
|
|
|
|
</Button>
|
|
|
|
|
<h1 className="text-2xl font-bold text-secondary-900">{company.name}</h1>
|
|
|
|
|
</div>
|
|
|
|
|
<Button onClick={() => navigate(`/companies/${company.id}/edit`)}>
|
|
|
|
|
{t('common.edit')}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-07-01 16:54:32 +02:00
|
|
|
<Tabs tabs={[overviewTab, contactsTab, tagsTab, filesTab, activityTab]} defaultKey="overview" />
|
2026-06-29 11:01:39 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|