feat: allowed more than one assigned person

This commit is contained in:
brian-inspiren 2026-08-06 16:06:35 +08:00
parent 983097ea7c
commit 83c0f449f5
8 changed files with 150 additions and 77 deletions

View File

@ -71,12 +71,10 @@ public function handle()
$salesUser = User::where('name', $this->rowValue($row, 'sales'))->first(); $salesUser = User::where('name', $this->rowValue($row, 'sales'))->first();
$pic = User::where('name', $this->rowValue($row, 'pic'))->first(); $pic = User::where('name', $this->rowValue($row, 'pic'))->first();
if ($pic) { if ($pic) {
ClientUserAssignation::updateOrCreate( ClientUserAssignation::firstOrCreate(
[ [
'client_id' => $client->id, 'client_id' => $client->id,
'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON, 'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON,
],
[
'user_id' => $pic->id, 'user_id' => $pic->id,
] ]
); );

View File

@ -345,12 +345,10 @@ private function linkInvoiceClient(ClientInvoice $invoice, array $validated): Cl
$client = Client::findOrFail($validated['client_id']); $client = Client::findOrFail($validated['client_id']);
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $client), 403); abort_unless($this->hierarchyService->canViewClient(Auth::user(), $client), 403);
ClientUserAssignation::updateOrCreate( ClientUserAssignation::firstOrCreate(
[ [
'client_id' => $client->id, 'client_id' => $client->id,
'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON, 'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON,
],
[
'user_id' => Auth::id(), 'user_id' => Auth::id(),
] ]
); );
@ -621,12 +619,10 @@ public function storeClient(Request $request, ClientInvoice $invoice)
[] []
); );
ClientUserAssignation::updateOrCreate( ClientUserAssignation::firstOrCreate(
[ [
'client_id' => $client->id, 'client_id' => $client->id,
'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON, 'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON,
],
[
'user_id' => Auth::id(), 'user_id' => Auth::id(),
] ]
); );

View File

@ -56,11 +56,17 @@ public function accounts()
) )
->get(); ->get();
$customerMap = $localClients->map(function ($data) { $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); $salesPerson = $data->assignations->firstWhere('role', ClientUserAssignation::ROLE_SALES_PERSON);
$data['industry'] = $data->industry; $data['industry'] = $data->industry;
$data['sql_acc_code'] = implode(',', $data->customers->pluck('sql_acc_code')->toArray()); $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['sales_person'] = $salesPerson?->user?->name;
$data['latest_remaining_amount'] = $data->latestRemainingAmount(); $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], 'customer_id' => ['required', 'string', 'unique:clients,customer_id,'.$localClient->id],
'industry' => ['nullable', 'string'], 'industry' => ['nullable', 'string'],
'sql_acc_code' => ['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'], 'sales_person' => ['nullable', 'integer', 'exists:users,id'],
]); ]);
@ -200,29 +207,60 @@ public function updateAccount(Request $request, $id)
}); });
} }
$assignmentValues = [ $this->syncAssignedPeople($localClient, $validated['assigned_person'] ?? []);
ClientUserAssignation::ROLE_ASSIGNED_PERSON => $validated['assigned_person'] ?? null, $this->syncSingleAssignment(
ClientUserAssignation::ROLE_SALES_PERSON => $validated['sales_person'] ?? null, $localClient,
]; 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]
);
}
return redirect() return redirect()
->route('google-ads.accounts.edit', ['id' => $localClient->customer_id]) ->route('google-ads.accounts.edit', ['id' => $localClient->customer_id])
->with('message-info', 'Account details updated.'); ->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) public function campaigns($id)
{ {
$campaigns = $this->adsService->listCampaigns($id); $campaigns = $this->adsService->listCampaigns($id);
@ -249,11 +287,15 @@ private function hydrateClient(array $account): array
); );
// dd($localClient); // dd($localClient);
$localClient->load(['assignations.user', 'invoices.payments.items.billingItemType','customers']); $localClient->load(['assignations.user', 'invoices.payments.items.billingItemType','customers']);
$assignments = $localClient->assignations $assignments = [
->mapWithKeys(function (ClientUserAssignation $assignation) { ClientUserAssignation::ROLE_ASSIGNED_PERSON => $localClient->assignations
return [$assignation->role => $assignation->user_id]; ->where('role', ClientUserAssignation::ROLE_ASSIGNED_PERSON)
}) ->pluck('user_id')
->toArray(); ->values()
->all(),
ClientUserAssignation::ROLE_SALES_PERSON => $localClient->assignations
->firstWhere('role', ClientUserAssignation::ROLE_SALES_PERSON)?->user_id,
];
$users = User::orderBy('name') $users = User::orderBy('name')
->get(['id', 'name', 'email']) ->get(['id', 'name', 'email'])
@ -420,12 +462,10 @@ public function insertCSVDataToDB()
$salesUser = User::where('name', $row['sales'])->first(); $salesUser = User::where('name', $row['sales'])->first();
$pic = User::where('name', $row['pic'])->first(); $pic = User::where('name', $row['pic'])->first();
if ($pic) { if ($pic) {
ClientUserAssignation::updateOrCreate( ClientUserAssignation::firstOrCreate(
[ [
'client_id' => $client->id, 'client_id' => $client->id,
'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON, 'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON,
],
[
'user_id' => $pic->id, 'user_id' => $pic->id,
] ]
); );

View File

@ -25,11 +25,13 @@ public static function roles(): array
'id' => self::ROLE_ASSIGNED_PERSON, 'id' => self::ROLE_ASSIGNED_PERSON,
'label' => 'Assigned Person', 'label' => 'Assigned Person',
'field' => 'assigned_person', 'field' => 'assigned_person',
'multiple' => true,
], ],
[ [
'id' => self::ROLE_SALES_PERSON, 'id' => self::ROLE_SALES_PERSON,
'label' => 'Sales Person', 'label' => 'Sales Person',
'field' => 'sales_person', 'field' => 'sales_person',
'multiple' => false,
], ],
]; ];
} }

View File

@ -12,6 +12,7 @@ import {
Text, Text,
TextInput, TextInput,
Select, Select,
MultiSelect,
Title, Title,
Divider, Divider,
} from "@mantine/core"; } from "@mantine/core";
@ -23,6 +24,7 @@ interface AssignmentRole {
id: number; id: number;
label: string; label: string;
field: "assigned_person" | "sales_person"; field: "assigned_person" | "sales_person";
multiple?: boolean;
} }
interface SelectOption { interface SelectOption {
@ -35,7 +37,7 @@ interface AccountFormData {
customer_id: string; customer_id: string;
industry: string; industry: string;
sql_acc_code: string; sql_acc_code: string;
assigned_person: string; assigned_person: string[];
sales_person: string; sales_person: string;
} }
@ -43,7 +45,7 @@ interface Props {
id: string; id: string;
client: Client; client: Client;
clientAssignmentRoles: AssignmentRole[]; clientAssignmentRoles: AssignmentRole[];
clientAssignments: Record<number, number | null>; clientAssignments: Record<number, number | number[] | null>;
assignmentUsers: SelectOption[]; assignmentUsers: SelectOption[];
} }
@ -59,13 +61,24 @@ export default function Page({
customer_id: client.customer_id ?? id, customer_id: client.customer_id ?? id,
industry: client.industry ?? "", industry: client.industry ?? "",
sql_acc_code: client.sql_acc_code ?? "", sql_acc_code: client.sql_acc_code ?? "",
assigned_person: "", assigned_person: [],
sales_person: "", sales_person: "",
}; };
clientAssignmentRoles.forEach((role) => { clientAssignmentRoles.forEach((role) => {
const value = clientAssignments?.[role.id]; 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<AccountFormData>(initialData); const form = useForm<AccountFormData>(initialData);
@ -77,7 +90,9 @@ export default function Page({
...data, ...data,
industry: data.industry || null, industry: data.industry || null,
sql_acc_code: data.sql_acc_code || 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, sales_person: parseInt(data.sales_person) || null,
})); }));
@ -169,19 +184,33 @@ export default function Page({
breakpoints={[{ maxWidth: "md", cols: 1 }]} breakpoints={[{ maxWidth: "md", cols: 1 }]}
spacing="md" spacing="md"
> >
{clientAssignmentRoles.map((role) => ( {clientAssignmentRoles.map((role) =>
<Select role.multiple || role.field === "assigned_person" ? (
key={role.id} <MultiSelect
label={role.label} key={role.id}
placeholder={`Select ${role.label}`} label={role.label}
data={assignmentUsers} placeholder={`Select ${role.label}`}
value={form.data[role.field]} data={assignmentUsers}
onChange={(value) => form.setData(role.field, value ?? "")} value={form.data.assigned_person}
error={form.errors[role.field]} onChange={(value) => form.setData("assigned_person", value)}
clearable error={form.errors.assigned_person}
searchable clearable
/> searchable
))} />
) : (
<Select
key={role.id}
label={role.label}
placeholder={`Select ${role.label}`}
data={assignmentUsers}
value={form.data.sales_person}
onChange={(value) => form.setData("sales_person", value ?? "")}
error={form.errors.sales_person}
clearable
searchable
/>
),
)}
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
</Paper> </Paper>

View File

@ -132,20 +132,20 @@ export default function TicketDetails({
return <Badge color={getStatusColor(value)}>{value}</Badge>; return <Badge color={getStatusColor(value)}>{value}</Badge>;
}, },
}, },
{ // {
accessorKey: 'latest_remaining_amount', // accessorKey: 'latest_remaining_amount',
header: 'Remaining Amount', // header: 'Remaining Amount',
size: 180, // size: 180,
Cell: ({ cell }: any) => { // Cell: ({ cell }: any) => {
const amount = parseAmount(cell.getValue()); // const amount = parseAmount(cell.getValue());
return ( // return (
<Text color={amount < 0 ? 'red' : 'green'} weight={700}> // <Text color={amount < 0 ? 'red' : 'green'} weight={700}>
{currencyFormatter.format(amount)} // {currencyFormatter.format(amount)}
</Text> // </Text>
); // );
}, // },
}, // },
{ {
accessorKey: 'latest_start_date', accessorKey: 'latest_start_date',
header: 'Latest Start Date', header: 'Latest Start Date',

View File

@ -85,7 +85,7 @@ interface Props {
can_modify?: boolean; can_modify?: boolean;
}; };
clientAssignmentRoles: AssignmentRole[]; clientAssignmentRoles: AssignmentRole[];
clientAssignments: Record<number, number | null>; clientAssignments: Record<number, number | number[] | null>;
assignmentUsers: SelectOption[]; assignmentUsers: SelectOption[];
clientInvoices: ClientInvoice[]; clientInvoices: ClientInvoice[];
clientAdjustments: ClientInvoiceAdjustment[]; clientAdjustments: ClientInvoiceAdjustment[];
@ -97,6 +97,7 @@ interface AssignmentRole {
id: number; id: number;
label: string; label: string;
field: string; field: string;
multiple?: boolean;
} }
interface SelectOption { interface SelectOption {
@ -611,15 +612,22 @@ export default function TicketDetails({
}, [activities]); }, [activities]);
const lookupAssignmentLabel = (roleId: number) => { const lookupAssignmentLabel = (roleId: number) => {
const userId = clientAssignments?.[roleId]; const userIds = clientAssignments?.[roleId];
if (!userId) { if (!userIds || (Array.isArray(userIds) && userIds.length === 0)) {
return '—'; return '—';
} }
return ( const ids = Array.isArray(userIds) ? userIds : [userIds];
assignmentUsers.find((option) => option.value === String(userId)) const labels = ids
?.label ?? '—' .map(
); (userId) =>
assignmentUsers.find(
(option) => option.value === String(userId),
)?.label,
)
.filter(Boolean);
return labels.length > 0 ? labels.join(', ') : '—';
}; };
const accountSummary = [ const accountSummary = [

View File

@ -62,8 +62,8 @@ export interface Client {
latest_start_date?: string | null; latest_start_date?: string | null;
latest_end_date?: string | null; latest_end_date?: string | null;
latest_remaining_days?: number | null; latest_remaining_days?: number | null;
assigned_person?: string; assigned_person?: string | null;
sales_person?: string; sales_person?: string | null;
} }
export interface Role { export interface Role {