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();
$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,
]
);

View File

@ -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(),
]
);

View File

@ -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,
]
);

View File

@ -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,
],
];
}

View File

@ -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<number, number | null>;
clientAssignments: Record<number, number | number[] | null>;
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<AccountFormData>(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) => (
<Select
key={role.id}
label={role.label}
placeholder={`Select ${role.label}`}
data={assignmentUsers}
value={form.data[role.field]}
onChange={(value) => form.setData(role.field, value ?? "")}
error={form.errors[role.field]}
clearable
searchable
/>
))}
{clientAssignmentRoles.map((role) =>
role.multiple || role.field === "assigned_person" ? (
<MultiSelect
key={role.id}
label={role.label}
placeholder={`Select ${role.label}`}
data={assignmentUsers}
value={form.data.assigned_person}
onChange={(value) => form.setData("assigned_person", value)}
error={form.errors.assigned_person}
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>
</Stack>
</Paper>

View File

@ -132,20 +132,20 @@ export default function TicketDetails({
return <Badge color={getStatusColor(value)}>{value}</Badge>;
},
},
{
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 (
<Text color={amount < 0 ? 'red' : 'green'} weight={700}>
{currencyFormatter.format(amount)}
</Text>
);
},
},
// return (
// <Text color={amount < 0 ? 'red' : 'green'} weight={700}>
// {currencyFormatter.format(amount)}
// </Text>
// );
// },
// },
{
accessorKey: 'latest_start_date',
header: 'Latest Start Date',

View File

@ -85,7 +85,7 @@ interface Props {
can_modify?: boolean;
};
clientAssignmentRoles: AssignmentRole[];
clientAssignments: Record<number, number | null>;
clientAssignments: Record<number, number | number[] | null>;
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 = [

View File

@ -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 {