From e2cb4499bbdf472f7925f139875e285bda35d83e Mon Sep 17 00:00:00 2001 From: Nyavokevin <42602932+nyavokevin@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:58:25 +0300 Subject: [PATCH] detail client --- .../Controllers/Api/ContactController.php | 23 + .../app/Repositories/ContactRepository.php | 7 + .../ContactRepositoryInterface.php | 2 + thanasoft-back/routes/api.php | 2 + thanasoft-front/CLIENT_DETAIL_NEW.md | 259 +++++++ .../Organism/CRM/ClientDetailPresentation.vue | 157 +++- .../Organism/CRM/ContactPresentation.vue | 2 +- .../CRM/client/ClientDetailContent.vue | 84 +++ .../CRM/client/ClientDetailSidebar.vue | 74 ++ .../components/atoms/client/ClientAvatar.vue | 64 ++ .../src/components/atoms/client/InfoCard.vue | 24 + .../components/atoms/client/StatusBadge.vue | 27 + .../atoms/client/TabNavigationItem.vue | 69 ++ .../molecules/client/ClientAddressTab.vue | 60 ++ .../molecules/client/ClientContactsTab.vue | 148 ++++ .../molecules/client/ClientInfoTab.vue | 674 ++++++++++++++++++ .../molecules/client/ClientNotesTab.vue | 23 + .../molecules/client/ClientOverview.vue | 153 ++++ .../molecules/client/ClientProfileCard.vue | 76 ++ .../molecules/client/ClientTabNavigation.vue | 54 ++ .../molecules/client/ListContact.vue | 198 ++++- .../molecules/contact/ContactModal.vue | 418 +++++++++++ .../templates/CRM/ClientDetailTemplate.vue | 20 +- thanasoft-front/src/stores/clientStore.ts | 7 + thanasoft-front/src/stores/contactStore.ts | 69 +- .../src/views/pages/CRM/ClientDetailNew.vue | 578 +++++++++++++++ .../src/views/pages/CRM/ClientDetails.vue | 67 +- 27 files changed, 3245 insertions(+), 94 deletions(-) create mode 100644 thanasoft-front/CLIENT_DETAIL_NEW.md create mode 100644 thanasoft-front/src/components/Organism/CRM/client/ClientDetailContent.vue create mode 100644 thanasoft-front/src/components/Organism/CRM/client/ClientDetailSidebar.vue create mode 100644 thanasoft-front/src/components/atoms/client/ClientAvatar.vue create mode 100644 thanasoft-front/src/components/atoms/client/InfoCard.vue create mode 100644 thanasoft-front/src/components/atoms/client/StatusBadge.vue create mode 100644 thanasoft-front/src/components/atoms/client/TabNavigationItem.vue create mode 100644 thanasoft-front/src/components/molecules/client/ClientAddressTab.vue create mode 100644 thanasoft-front/src/components/molecules/client/ClientContactsTab.vue create mode 100644 thanasoft-front/src/components/molecules/client/ClientInfoTab.vue create mode 100644 thanasoft-front/src/components/molecules/client/ClientNotesTab.vue create mode 100644 thanasoft-front/src/components/molecules/client/ClientOverview.vue create mode 100644 thanasoft-front/src/components/molecules/client/ClientProfileCard.vue create mode 100644 thanasoft-front/src/components/molecules/client/ClientTabNavigation.vue create mode 100644 thanasoft-front/src/components/molecules/contact/ContactModal.vue create mode 100644 thanasoft-front/src/views/pages/CRM/ClientDetailNew.vue diff --git a/thanasoft-back/app/Http/Controllers/Api/ContactController.php b/thanasoft-back/app/Http/Controllers/Api/ContactController.php index c7af595..92b0cbc 100644 --- a/thanasoft-back/app/Http/Controllers/Api/ContactController.php +++ b/thanasoft-back/app/Http/Controllers/Api/ContactController.php @@ -164,4 +164,27 @@ class ContactController extends Controller ], 500); } } + + + public function getContactsByClient(string $clientId): JsonResponse + { + try { + $intId = (int) $clientId; + $contacts = $this->contactRepository->getByClientId($intId); + return response()->json([ + 'data' => ContactResource::collection($contacts), + ], 200); + } catch (\Exception $e) { + Log::error('Error fetching contacts by client: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_id' => $clientId, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des contacts du client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } } diff --git a/thanasoft-back/app/Repositories/ContactRepository.php b/thanasoft-back/app/Repositories/ContactRepository.php index f346beb..8b4295d 100644 --- a/thanasoft-back/app/Repositories/ContactRepository.php +++ b/thanasoft-back/app/Repositories/ContactRepository.php @@ -55,4 +55,11 @@ class ContactRepository extends BaseRepository implements ContactRepositoryInter return $query->paginate($perPage); } + + public function getByClientId(int $clientId) + { + return $this->model->newQuery() + ->where('client_id', $clientId) + ->get(); + } } diff --git a/thanasoft-back/app/Repositories/ContactRepositoryInterface.php b/thanasoft-back/app/Repositories/ContactRepositoryInterface.php index e37a0fb..f8b4984 100644 --- a/thanasoft-back/app/Repositories/ContactRepositoryInterface.php +++ b/thanasoft-back/app/Repositories/ContactRepositoryInterface.php @@ -7,4 +7,6 @@ namespace App\Repositories; interface ContactRepositoryInterface extends BaseRepositoryInterface { function paginate(int $perPage = 15, array $filters = []); + + function getByClientId(int $clientId); } diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php index b354b1b..7fdde85 100644 --- a/thanasoft-back/routes/api.php +++ b/thanasoft-back/routes/api.php @@ -41,8 +41,10 @@ Route::middleware('auth:sanctum')->group(function () { Route::apiResource('client-groups', ClientGroupController::class); Route::apiResource('client-locations', ClientLocationController::class); + // Contact management Route::apiResource('contacts', ContactController::class); + Route::get('clients/{clientId}/contacts', [ContactController::class, 'getContactsByClient']); Route::apiResource('client-categories', ClientCategoryController::class); diff --git a/thanasoft-front/CLIENT_DETAIL_NEW.md b/thanasoft-front/CLIENT_DETAIL_NEW.md new file mode 100644 index 0000000..35533a1 --- /dev/null +++ b/thanasoft-front/CLIENT_DETAIL_NEW.md @@ -0,0 +1,259 @@ +# New Client Detail Page - Modern Design + +## Overview +A completely redesigned client detail page with modern UI/UX, inspired by the Settings page structure with tabs, client avatar/logo, and better ergonomics. + +## Features + +### 🎨 **Modern Layout** +- **Left Sidebar**: Sticky client card with navigation +- **Right Content Area**: Tabbed content with clean organization + +### 👤 **Client Profile Card** +- **Avatar/Logo**: + - Shows client initials if no image + - Click edit button to upload logo + - Large, prominent display +- **Quick Stats**: + - Number of contacts + - Active/Inactive status +- **Client Info**: Name and type + +### 📑 **5 Tab Sections** + +#### 1. **Aperçu (Overview)** +Default view with key information: +- Contact info card (email, phone) +- Business info card (SIRET, VAT) +- Address card +- Recent contacts (first 3) +- Edit button to modify client + +#### 2. **Informations (Information)** +Complete client details: +- Name, type +- SIRET, VAT number +- Email, phone +- Commercial + +#### 3. **Contacts** +Full contact list with: +- Contact table with avatars +- Email, phone, position +- Primary contact badge +- "Add Contact" button +- Empty state if no contacts + +#### 4. **Adresse (Address)** +Billing address details: +- Address line 1 & 2 +- Postal code, city, country + +#### 5. **Notes** +Client notes section + +## File Location +``` +src/views/pages/CRM/ClientDetailNew.vue +``` + +## Usage + +### Update Router +Add the new route in your router configuration: + +```javascript +// router/index.js +{ + path: '/clients/:id/detail', + name: 'ClientDetailNew', + component: () => import('@/views/pages/CRM/ClientDetailNew.vue') +} +``` + +### Replace Old ClientDetail +To use this as the main client detail page: + +```javascript +// Change existing route +{ + path: '/clients/:id', + name: 'ClientDetail', + component: () => import('@/views/pages/CRM/ClientDetailNew.vue') // Changed from ClientDetails.vue +} +``` + +## Component Structure + +### Template Sections +1. **Header** - Back button to clients list +2. **Loading State** - Spinner while fetching data +3. **Error State** - Alert for errors +4. **Main Content** + - Left: Client card + navigation + - Right: Tab content + +### Script Features +```javascript +// Reactive data +const activeTab = ref('overview') // Current tab +const clientAvatar = ref(null) // Client logo/avatar +const contacts_client = ref([]) // Client contacts + +// Methods +getInitials(name) // Generate initials from name +formatAddress(client) // Format full address string +triggerFileInput() // Open file selector +handleAvatarUpload() // Handle logo upload +``` + +### Styling Features +- **Sticky sidebar** - Stays visible when scrolling +- **Active tab highlighting** - Gradient background +- **Smooth transitions** - Hover effects +- **Responsive design** - Works on mobile +- **Clean cards** - Soft shadows and borders + +## Design Principles + +### Color Scheme +- **Primary Actions**: Gradient purple-pink (`#7928ca` to `#ff0080`) +- **Success**: Green for active status and badges +- **Info**: Blue for contact info +- **Warning**: Yellow for business info +- **Danger**: Red for inactive status + +### Typography +- **Headers**: Bold, clear hierarchy +- **Body**: `text-sm` for readability +- **Labels**: Uppercase with spacing + +### Icons +Using Font Awesome: +- 📊 `fa-eye` - Overview +- ℹ️ `fa-info-circle` - Information +- 👥 `fa-users` - Contacts +- 📍 `fa-map-marker-alt` - Address +- 📝 `fa-sticky-note` - Notes + +## Avatar/Logo Upload + +### Current Implementation +```javascript +handleAvatarUpload(event) { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + clientAvatar.value = e.target.result; + // TODO: Upload to server + }; + reader.readAsDataURL(file); + } +} +``` + +### TODO: Connect to Backend +To implement server upload: + +```javascript +const handleAvatarUpload = async (event) => { + const file = event.target.files[0]; + if (!file) return; + + const formData = new FormData(); + formData.append('avatar', file); + formData.append('client_id', clientStore.currentClient.id); + + try { + const response = await api.post('/clients/upload-avatar', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }); + + clientAvatar.value = response.data.avatar_url; + // Update store + await clientStore.fetchClient(client_id); + } catch (error) { + console.error('Upload failed:', error); + } +}; +``` + +## Comparison: Old vs New + +### Old Design +- Single page layout +- All info visible at once +- No image/avatar support +- Basic styling +- No tab organization + +### New Design ✨ +- Modern tabbed interface +- Client avatar/logo with upload +- Sticky sidebar navigation +- Card-based organization +- Better mobile responsive +- Visual hierarchy +- Quick stats +- Empty states +- Badge indicators + +## Customization + +### Change Tab Order +```vue + +
+``` + +### Add New Tab +1. Add nav item in sidebar +2. Add content section with `v-show` +3. Update `activeTab` ref + +```vue + ++ {{ client.billing_address_line1 || "-" }} +
++ {{ client.billing_address_line2 || "-" }} +
++ {{ client.billing_postal_code || "-" }} +
++ {{ client.billing_city || "-" }} +
++ {{ client.billing_country_code || "-" }} +
+| + Contact + | ++ Email + | ++ Téléphone + | ++ Poste + | ++ |
|---|---|---|---|---|
|
+
+
+
+
+
+ {{ getInitials(contact.full_name) }}
+
+
+
+ {{ contact.full_name }}++ Contact principal + + |
+
+ + {{ contact.email || "-" }} + + |
+
+ + {{ contact.phone || contact.mobile || "-" }} + + |
+
+ + {{ contact.position || "-" }} + + |
+ + + | +
Aucun contact pour ce client
++ {{ notes }} +
+Aucune note pour ce client
++ {{ formattedAddress }} +
++ {{ contact.position || "Contact" }} +
++ Aucun contact enregistré +
++ {{ clientType }} +
+ + +Contacts
+Statut
+No contacts
+ {{ clientStore.currentClient.type_label || 'Client' }} +
+ + +Contacts
+Statut
++ {{ formatAddress(clientStore.currentClient) }} +
+{{ contact.position || 'Contact' }}
++ Aucun contact enregistré +
+{{ clientStore.currentClient.name }}
+{{ clientStore.currentClient.type_label || '-' }}
+{{ clientStore.currentClient.siret || '-' }}
+{{ clientStore.currentClient.vat_number || '-' }}
+{{ clientStore.currentClient.phone || '-' }}
+{{ clientStore.currentClient.commercial || '-' }}
+| + Contact + | ++ Email + | ++ Téléphone + | ++ Poste + | ++ |
|---|---|---|---|---|
|
+
+
+
+
+
+ {{ getInitials(contact.full_name) }}
+
+
+
+ {{ contact.full_name }}++ Contact principal + + |
+
+ + {{ contact.email || '-' }} + + |
+
+ + {{ contact.phone || contact.mobile || '-' }} + + |
+
+ + {{ contact.position || '-' }} + + |
+ + + | +
Aucun contact pour ce client
++ {{ clientStore.currentClient.billing_address_line1 || '-' }} +
++ {{ clientStore.currentClient.billing_address_line2 || '-' }} +
++ {{ clientStore.currentClient.billing_postal_code || '-' }} +
++ {{ clientStore.currentClient.billing_city || '-' }} +
++ {{ clientStore.currentClient.billing_country_code || '-' }} +
++ {{ clientStore.currentClient.notes }} +
++ Aucune note pour ce client +
+