From 83c0f449f5fbac27a9793a36bf589e93829de13a Mon Sep 17 00:00:00 2001 From: brian-inspiren Date: Thu, 6 Aug 2026 16:06:35 +0800 Subject: [PATCH] feat: allowed more than one assigned person --- app/Console/Commands/CreateClientInvoice.php | 4 +- .../Controllers/ClientInvoiceController.php | 8 +- app/Http/Controllers/GoogleAdsController.php | 96 +++++++++++++------ app/Models/ClientUserAssignation.php | 2 + resources/js/pages/campaigns/account/edit.tsx | 65 +++++++++---- resources/js/pages/campaigns/index.tsx | 26 ++--- resources/js/pages/campaigns/show.tsx | 22 +++-- resources/js/types/index.d.ts | 4 +- 8 files changed, 150 insertions(+), 77 deletions(-) diff --git a/app/Console/Commands/CreateClientInvoice.php b/app/Console/Commands/CreateClientInvoice.php index 9f2aadf..130977a 100644 --- a/app/Console/Commands/CreateClientInvoice.php +++ b/app/Console/Commands/CreateClientInvoice.php @@ -71,12 +71,10 @@ public function handle() $salesUser = User::where('name', $this->rowValue($row, 'sales'))->first(); $pic = User::where('name', $this->rowValue($row, 'pic'))->first(); if ($pic) { - ClientUserAssignation::updateOrCreate( + ClientUserAssignation::firstOrCreate( [ 'client_id' => $client->id, 'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON, - ], - [ 'user_id' => $pic->id, ] ); diff --git a/app/Http/Controllers/ClientInvoiceController.php b/app/Http/Controllers/ClientInvoiceController.php index 9a3519b..8e42874 100644 --- a/app/Http/Controllers/ClientInvoiceController.php +++ b/app/Http/Controllers/ClientInvoiceController.php @@ -345,12 +345,10 @@ private function linkInvoiceClient(ClientInvoice $invoice, array $validated): Cl $client = Client::findOrFail($validated['client_id']); abort_unless($this->hierarchyService->canViewClient(Auth::user(), $client), 403); - ClientUserAssignation::updateOrCreate( + ClientUserAssignation::firstOrCreate( [ 'client_id' => $client->id, 'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON, - ], - [ 'user_id' => Auth::id(), ] ); @@ -621,12 +619,10 @@ public function storeClient(Request $request, ClientInvoice $invoice) [] ); - ClientUserAssignation::updateOrCreate( + ClientUserAssignation::firstOrCreate( [ 'client_id' => $client->id, 'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON, - ], - [ 'user_id' => Auth::id(), ] ); diff --git a/app/Http/Controllers/GoogleAdsController.php b/app/Http/Controllers/GoogleAdsController.php index dbc24b1..1b48925 100644 --- a/app/Http/Controllers/GoogleAdsController.php +++ b/app/Http/Controllers/GoogleAdsController.php @@ -56,11 +56,17 @@ public function accounts() ) ->get(); $customerMap = $localClients->map(function ($data) { - $assignedPerson = $data->assignations->firstWhere('role', ClientUserAssignation::ROLE_ASSIGNED_PERSON); + $assignedPeople = $data->assignations + ->where('role', ClientUserAssignation::ROLE_ASSIGNED_PERSON) + ->map(fn (ClientUserAssignation $assignation) => $assignation->user?->name) + ->filter() + ->values(); $salesPerson = $data->assignations->firstWhere('role', ClientUserAssignation::ROLE_SALES_PERSON); $data['industry'] = $data->industry; $data['sql_acc_code'] = implode(',', $data->customers->pluck('sql_acc_code')->toArray()); - $data['assigned_person'] = $assignedPerson?->user?->name; + $data['assigned_person'] = $assignedPeople->isNotEmpty() + ? $assignedPeople->implode(', ') + : null; $data['sales_person'] = $salesPerson?->user?->name; $data['latest_remaining_amount'] = $data->latestRemainingAmount(); @@ -174,7 +180,8 @@ public function updateAccount(Request $request, $id) 'customer_id' => ['required', 'string', 'unique:clients,customer_id,'.$localClient->id], 'industry' => ['nullable', 'string'], 'sql_acc_code' => ['nullable', 'string'], - 'assigned_person' => ['nullable', 'integer', 'exists:users,id'], + 'assigned_person' => ['nullable', 'array'], + 'assigned_person.*' => ['integer', 'exists:users,id'], 'sales_person' => ['nullable', 'integer', 'exists:users,id'], ]); @@ -200,29 +207,60 @@ public function updateAccount(Request $request, $id) }); } - $assignmentValues = [ - ClientUserAssignation::ROLE_ASSIGNED_PERSON => $validated['assigned_person'] ?? null, - ClientUserAssignation::ROLE_SALES_PERSON => $validated['sales_person'] ?? null, - ]; - - foreach ($assignmentValues as $role => $userId) { - if ($userId === null) { - $localClient->assignations()->where('role', $role)->delete(); - - continue; - } - - $localClient->assignations()->updateOrCreate( - ['role' => $role], - ['user_id' => $userId] - ); - } + $this->syncAssignedPeople($localClient, $validated['assigned_person'] ?? []); + $this->syncSingleAssignment( + $localClient, + ClientUserAssignation::ROLE_SALES_PERSON, + $validated['sales_person'] ?? null, + ); return redirect() ->route('google-ads.accounts.edit', ['id' => $localClient->customer_id]) ->with('message-info', 'Account details updated.'); } + private function syncAssignedPeople(Client $client, array $userIds): void + { + $userIds = collect($userIds) + ->filter(fn ($userId) => is_numeric($userId)) + ->map(fn ($userId) => (int) $userId) + ->unique() + ->values() + ->all(); + + $query = $client->assignations() + ->where('role', ClientUserAssignation::ROLE_ASSIGNED_PERSON); + + if ($userIds === []) { + $query->delete(); + + return; + } + + $query->whereNotIn('user_id', $userIds)->delete(); + + foreach ($userIds as $userId) { + $client->assignations()->firstOrCreate([ + 'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON, + 'user_id' => $userId, + ]); + } + } + + private function syncSingleAssignment(Client $client, int $role, ?int $userId): void + { + $client->assignations()->where('role', $role)->delete(); + + if ($userId === null) { + return; + } + + $client->assignations()->create([ + 'role' => $role, + 'user_id' => $userId, + ]); + } + public function campaigns($id) { $campaigns = $this->adsService->listCampaigns($id); @@ -249,11 +287,15 @@ private function hydrateClient(array $account): array ); // dd($localClient); $localClient->load(['assignations.user', 'invoices.payments.items.billingItemType','customers']); - $assignments = $localClient->assignations - ->mapWithKeys(function (ClientUserAssignation $assignation) { - return [$assignation->role => $assignation->user_id]; - }) - ->toArray(); + $assignments = [ + ClientUserAssignation::ROLE_ASSIGNED_PERSON => $localClient->assignations + ->where('role', ClientUserAssignation::ROLE_ASSIGNED_PERSON) + ->pluck('user_id') + ->values() + ->all(), + ClientUserAssignation::ROLE_SALES_PERSON => $localClient->assignations + ->firstWhere('role', ClientUserAssignation::ROLE_SALES_PERSON)?->user_id, + ]; $users = User::orderBy('name') ->get(['id', 'name', 'email']) @@ -420,12 +462,10 @@ public function insertCSVDataToDB() $salesUser = User::where('name', $row['sales'])->first(); $pic = User::where('name', $row['pic'])->first(); if ($pic) { - ClientUserAssignation::updateOrCreate( + ClientUserAssignation::firstOrCreate( [ 'client_id' => $client->id, 'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON, - ], - [ 'user_id' => $pic->id, ] ); diff --git a/app/Models/ClientUserAssignation.php b/app/Models/ClientUserAssignation.php index a924a4c..c15e44f 100644 --- a/app/Models/ClientUserAssignation.php +++ b/app/Models/ClientUserAssignation.php @@ -25,11 +25,13 @@ public static function roles(): array 'id' => self::ROLE_ASSIGNED_PERSON, 'label' => 'Assigned Person', 'field' => 'assigned_person', + 'multiple' => true, ], [ 'id' => self::ROLE_SALES_PERSON, 'label' => 'Sales Person', 'field' => 'sales_person', + 'multiple' => false, ], ]; } diff --git a/resources/js/pages/campaigns/account/edit.tsx b/resources/js/pages/campaigns/account/edit.tsx index fe09de5..71882b3 100644 --- a/resources/js/pages/campaigns/account/edit.tsx +++ b/resources/js/pages/campaigns/account/edit.tsx @@ -12,6 +12,7 @@ import { Text, TextInput, Select, + MultiSelect, Title, Divider, } from "@mantine/core"; @@ -23,6 +24,7 @@ interface AssignmentRole { id: number; label: string; field: "assigned_person" | "sales_person"; + multiple?: boolean; } interface SelectOption { @@ -35,7 +37,7 @@ interface AccountFormData { customer_id: string; industry: string; sql_acc_code: string; - assigned_person: string; + assigned_person: string[]; sales_person: string; } @@ -43,7 +45,7 @@ interface Props { id: string; client: Client; clientAssignmentRoles: AssignmentRole[]; - clientAssignments: Record; + clientAssignments: Record; assignmentUsers: SelectOption[]; } @@ -59,13 +61,24 @@ export default function Page({ customer_id: client.customer_id ?? id, industry: client.industry ?? "", sql_acc_code: client.sql_acc_code ?? "", - assigned_person: "", + assigned_person: [], sales_person: "", }; clientAssignmentRoles.forEach((role) => { const value = clientAssignments?.[role.id]; - initialData[role.field] = value ? String(value) : ""; + + if (role.field === "assigned_person") { + initialData.assigned_person = Array.isArray(value) + ? value.map(String) + : value + ? [String(value)] + : []; + + return; + } + + initialData.sales_person = value ? String(value) : ""; }); const form = useForm(initialData); @@ -77,7 +90,9 @@ export default function Page({ ...data, industry: data.industry || null, sql_acc_code: data.sql_acc_code || null, - assigned_person: parseInt(data.assigned_person) || null, + assigned_person: data.assigned_person + .map((userId) => parseInt(userId)) + .filter(Boolean), sales_person: parseInt(data.sales_person) || null, })); @@ -169,19 +184,33 @@ export default function Page({ breakpoints={[{ maxWidth: "md", cols: 1 }]} spacing="md" > - {clientAssignmentRoles.map((role) => ( - form.setData("sales_person", value ?? "")} + error={form.errors.sales_person} + clearable + searchable + /> + ), + )} diff --git a/resources/js/pages/campaigns/index.tsx b/resources/js/pages/campaigns/index.tsx index ad30b6b..e348411 100644 --- a/resources/js/pages/campaigns/index.tsx +++ b/resources/js/pages/campaigns/index.tsx @@ -132,20 +132,20 @@ export default function TicketDetails({ return {value}; }, }, - { - accessorKey: 'latest_remaining_amount', - header: 'Remaining Amount', - size: 180, - Cell: ({ cell }: any) => { - const amount = parseAmount(cell.getValue()); + // { + // accessorKey: 'latest_remaining_amount', + // header: 'Remaining Amount', + // size: 180, + // Cell: ({ cell }: any) => { + // const amount = parseAmount(cell.getValue()); - return ( - - {currencyFormatter.format(amount)} - - ); - }, - }, + // return ( + // + // {currencyFormatter.format(amount)} + // + // ); + // }, + // }, { accessorKey: 'latest_start_date', header: 'Latest Start Date', diff --git a/resources/js/pages/campaigns/show.tsx b/resources/js/pages/campaigns/show.tsx index c192487..f77e819 100644 --- a/resources/js/pages/campaigns/show.tsx +++ b/resources/js/pages/campaigns/show.tsx @@ -85,7 +85,7 @@ interface Props { can_modify?: boolean; }; clientAssignmentRoles: AssignmentRole[]; - clientAssignments: Record; + clientAssignments: Record; assignmentUsers: SelectOption[]; clientInvoices: ClientInvoice[]; clientAdjustments: ClientInvoiceAdjustment[]; @@ -97,6 +97,7 @@ interface AssignmentRole { id: number; label: string; field: string; + multiple?: boolean; } interface SelectOption { @@ -611,15 +612,22 @@ export default function TicketDetails({ }, [activities]); const lookupAssignmentLabel = (roleId: number) => { - const userId = clientAssignments?.[roleId]; - if (!userId) { + const userIds = clientAssignments?.[roleId]; + if (!userIds || (Array.isArray(userIds) && userIds.length === 0)) { return '—'; } - return ( - assignmentUsers.find((option) => option.value === String(userId)) - ?.label ?? '—' - ); + const ids = Array.isArray(userIds) ? userIds : [userIds]; + const labels = ids + .map( + (userId) => + assignmentUsers.find( + (option) => option.value === String(userId), + )?.label, + ) + .filter(Boolean); + + return labels.length > 0 ? labels.join(', ') : '—'; }; const accountSummary = [ diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 932a54c..00df950 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -62,8 +62,8 @@ export interface Client { latest_start_date?: string | null; latest_end_date?: string | null; latest_remaining_days?: number | null; - assigned_person?: string; - sales_person?: string; + assigned_person?: string | null; + sales_person?: string | null; } export interface Role {