@@ -1,17 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import {
|
||||
graphql,
|
||||
PreloadedQuery,
|
||||
usePreloadedQuery,
|
||||
useQueryLoader,
|
||||
useMutation,
|
||||
} from "react-relay";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { Suspense, useEffect, useState, useCallback } from "react";
|
||||
import type { PeopleOverviewPageQuery as PeopleOverviewPageQueryType } from "./__generated__/PeopleOverviewPageQuery.graphql";
|
||||
import { useParams } from "react-router";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useBreadcrumb } from "@/contexts/BreadcrumbContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const peopleOverviewPageQuery = graphql`
|
||||
query PeopleOverviewPageQuery($peopleId: ID!) {
|
||||
@@ -20,13 +27,61 @@ const peopleOverviewPageQuery = graphql`
|
||||
id
|
||||
fullName
|
||||
primaryEmailAddress
|
||||
additionalEmailAddresses
|
||||
kind
|
||||
createdAt
|
||||
updatedAt
|
||||
version
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const updatePeopleMutation = graphql`
|
||||
mutation PeopleOverviewPageUpdatePeopleMutation($input: UpdatePeopleInput!) {
|
||||
updatePeople(input: $input) {
|
||||
id
|
||||
fullName
|
||||
primaryEmailAddress
|
||||
additionalEmailAddresses
|
||||
kind
|
||||
updatedAt
|
||||
version
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function EditableField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
type = "text",
|
||||
helpText,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
type?: string;
|
||||
helpText?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">{label}</Label>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
{helpText && <p className="text-sm text-gray-500">{helpText}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PeopleOverviewPageContent({
|
||||
queryRef,
|
||||
}: {
|
||||
@@ -34,80 +89,198 @@ function PeopleOverviewPageContent({
|
||||
}) {
|
||||
const data = usePreloadedQuery(peopleOverviewPageQuery, queryRef);
|
||||
const { setBreadcrumbSegment } = useBreadcrumb();
|
||||
const [editedFields, setEditedFields] = useState<Set<string>>(new Set());
|
||||
const [formData, setFormData] = useState({
|
||||
fullName: data.node.fullName || '',
|
||||
primaryEmailAddress: data.node.primaryEmailAddress || '',
|
||||
additionalEmailAddresses: data.node.additionalEmailAddresses || [],
|
||||
kind: data.node.kind,
|
||||
});
|
||||
const [commit] = useMutation(updatePeopleMutation);
|
||||
const [_, loadQuery] = useQueryLoader<PeopleOverviewPageQueryType>(peopleOverviewPageQuery);
|
||||
const { toast } = useToast();
|
||||
|
||||
const hasChanges = editedFields.size > 0;
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
commit({
|
||||
variables: {
|
||||
input: {
|
||||
id: data.node.id,
|
||||
expectedVersion: data.node.version,
|
||||
...formData,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Changes saved successfully",
|
||||
variant: "default",
|
||||
});
|
||||
setEditedFields(new Set());
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error.message?.includes('concurrent modification')) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Someone else modified this person. Reloading latest data.",
|
||||
variant: "destructive",
|
||||
});
|
||||
loadQuery({ peopleId: data.node.id! });
|
||||
} else {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error.message || "Failed to save changes",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [commit, data.node.id, data.node.version, formData, loadQuery, toast]);
|
||||
|
||||
const handleFieldChange = (field: keyof typeof formData, value: any) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
setEditedFields(prev => new Set(prev).add(field));
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setFormData({
|
||||
fullName: data.node.fullName || '',
|
||||
primaryEmailAddress: data.node.primaryEmailAddress || '',
|
||||
additionalEmailAddresses: data.node.additionalEmailAddresses || [],
|
||||
kind: data.node.kind,
|
||||
});
|
||||
setEditedFields(new Set());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data.node?.primaryEmailAddress) {
|
||||
setBreadcrumbSegment("peoples/:id", data.node.primaryEmailAddress);
|
||||
if (data.node?.fullName) {
|
||||
setBreadcrumbSegment("peoples/:id", data.node.fullName);
|
||||
}
|
||||
}, [data.node?.primaryEmailAddress, setBreadcrumbSegment]);
|
||||
}, [data.node?.fullName, setBreadcrumbSegment]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4 md:p-6 lg:p-8">
|
||||
<div className="mx-auto max-w-4xl space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-xl font-semibold text-gray-900">
|
||||
{data.node?.fullName}
|
||||
</h1>
|
||||
<p className="text-gray-600">View and manage person details</p>
|
||||
<>
|
||||
<div className="space-y-6 p-4 md:p-6 lg:p-8">
|
||||
<div className="mx-auto max-w-4xl space-y-6">
|
||||
<EditableField
|
||||
label="Full Name"
|
||||
value={formData.fullName}
|
||||
onChange={(value) => handleFieldChange('fullName', value)}
|
||||
/>
|
||||
|
||||
<EditableField
|
||||
label="Primary Email"
|
||||
value={formData.primaryEmailAddress}
|
||||
type="email"
|
||||
onChange={(value) => handleFieldChange('primaryEmailAddress', value)}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Additional Email Addresses</Label>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{formData.additionalEmailAddresses.map((email, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => {
|
||||
const newEmails = [...formData.additionalEmailAddresses];
|
||||
newEmails[index] = e.target.value;
|
||||
handleFieldChange('additionalEmailAddresses', newEmails);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const newEmails = formData.additionalEmailAddresses.filter((_, i) => i !== index);
|
||||
handleFieldChange('additionalEmailAddresses', newEmails);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
handleFieldChange('additionalEmailAddresses', [...formData.additionalEmailAddresses, '']);
|
||||
}}
|
||||
>
|
||||
Add Email
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-medium">Additional Information</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Additional details about the person
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
<Label className="text-sm">Kind</Label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleFieldChange('kind', 'EMPLOYEE')}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||
formData.kind === 'EMPLOYEE'
|
||||
? "bg-blue-100 text-blue-900 ring-2 ring-blue-600 ring-offset-2"
|
||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
Employee
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleFieldChange('kind', 'CONTRACTOR')}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||
formData.kind === 'CONTRACTOR'
|
||||
? "bg-purple-100 text-purple-900 ring-2 ring-purple-600 ring-offset-2"
|
||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
Contractor
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-medium">Personal Information</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Basic information about the person
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="col-span-2">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Full Name
|
||||
</label>
|
||||
<p className="mt-1">{data.node?.fullName}</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Email
|
||||
</label>
|
||||
<p className="mt-1">{data.node?.primaryEmailAddress}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-medium">System Information</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
System-related information about the person
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Created At
|
||||
</label>
|
||||
<p className="mt-1">
|
||||
{new Date(data.node?.createdAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Last Updated
|
||||
</label>
|
||||
<p className="mt-1">
|
||||
{new Date(data.node?.updatedAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasChanges && (
|
||||
<div className="fixed bottom-6 right-6 flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<ee2731bf27a12fb704f79cf4058e2d80>>
|
||||
* @generated SignedSource<<528ad6b0a104964c9d947ef1e9c41b46>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -9,16 +9,20 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type PeopleKind = "CONTRACTOR" | "EMPLOYEE";
|
||||
export type PeopleOverviewPageQuery$variables = {
|
||||
peopleId: string;
|
||||
};
|
||||
export type PeopleOverviewPageQuery$data = {
|
||||
readonly node: {
|
||||
readonly additionalEmailAddresses?: ReadonlyArray<string>;
|
||||
readonly createdAt?: any;
|
||||
readonly fullName?: string;
|
||||
readonly id?: string;
|
||||
readonly kind?: PeopleKind;
|
||||
readonly primaryEmailAddress?: string;
|
||||
readonly updatedAt?: any;
|
||||
readonly version?: number;
|
||||
};
|
||||
};
|
||||
export type PeopleOverviewPageQuery = {
|
||||
@@ -66,15 +70,36 @@ v5 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"name": "additionalEmailAddresses",
|
||||
"storageKey": null
|
||||
},
|
||||
v6 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "kind",
|
||||
"storageKey": null
|
||||
},
|
||||
v7 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
v8 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
v9 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "version",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
@@ -98,7 +123,10 @@ return {
|
||||
(v3/*: any*/),
|
||||
(v4/*: any*/),
|
||||
(v5/*: any*/),
|
||||
(v6/*: any*/)
|
||||
(v6/*: any*/),
|
||||
(v7/*: any*/),
|
||||
(v8/*: any*/),
|
||||
(v9/*: any*/)
|
||||
],
|
||||
"type": "People",
|
||||
"abstractKey": null
|
||||
@@ -138,7 +166,10 @@ return {
|
||||
(v3/*: any*/),
|
||||
(v4/*: any*/),
|
||||
(v5/*: any*/),
|
||||
(v6/*: any*/)
|
||||
(v6/*: any*/),
|
||||
(v7/*: any*/),
|
||||
(v8/*: any*/),
|
||||
(v9/*: any*/)
|
||||
],
|
||||
"type": "People",
|
||||
"abstractKey": null
|
||||
@@ -149,16 +180,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "0cd0dd96cb7a84c2016bfe1f088f8245",
|
||||
"cacheID": "1383ca6e8c8b205082cc68223bec1833",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "PeopleOverviewPageQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query PeopleOverviewPageQuery(\n $peopleId: ID!\n) {\n node(id: $peopleId) {\n __typename\n ... on People {\n id\n fullName\n primaryEmailAddress\n createdAt\n updatedAt\n }\n id\n }\n}\n"
|
||||
"text": "query PeopleOverviewPageQuery(\n $peopleId: ID!\n) {\n node(id: $peopleId) {\n __typename\n ... on People {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n createdAt\n updatedAt\n version\n }\n id\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "16b3f0915f69d8475d745b0c7f13b029";
|
||||
(node as any).hash = "fa229186ad9e51f3a60b23fa88a882aa";
|
||||
|
||||
export default node;
|
||||
|
||||
146
apps/console/src/pages/__generated__/PeopleOverviewPageUpdatePeopleMutation.graphql.ts
generated
Normal file
146
apps/console/src/pages/__generated__/PeopleOverviewPageUpdatePeopleMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* @generated SignedSource<<b950e6a3b56e0545050b3bbe076c2f65>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type PeopleKind = "CONTRACTOR" | "EMPLOYEE";
|
||||
export type UpdatePeopleInput = {
|
||||
additionalEmailAddresses?: ReadonlyArray<string> | null | undefined;
|
||||
expectedVersion: number;
|
||||
fullName?: string | null | undefined;
|
||||
id: string;
|
||||
kind?: PeopleKind | null | undefined;
|
||||
primaryEmailAddress?: string | null | undefined;
|
||||
};
|
||||
export type PeopleOverviewPageUpdatePeopleMutation$variables = {
|
||||
input: UpdatePeopleInput;
|
||||
};
|
||||
export type PeopleOverviewPageUpdatePeopleMutation$data = {
|
||||
readonly updatePeople: {
|
||||
readonly additionalEmailAddresses: ReadonlyArray<string>;
|
||||
readonly fullName: string;
|
||||
readonly id: string;
|
||||
readonly kind: PeopleKind;
|
||||
readonly primaryEmailAddress: string;
|
||||
readonly updatedAt: any;
|
||||
readonly version: number;
|
||||
};
|
||||
};
|
||||
export type PeopleOverviewPageUpdatePeopleMutation = {
|
||||
response: PeopleOverviewPageUpdatePeopleMutation$data;
|
||||
variables: PeopleOverviewPageUpdatePeopleMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
"concreteType": "People",
|
||||
"kind": "LinkedField",
|
||||
"name": "updatePeople",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fullName",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "primaryEmailAddress",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "additionalEmailAddresses",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "kind",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "version",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "PeopleOverviewPageUpdatePeopleMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "PeopleOverviewPageUpdatePeopleMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "85b62bd3c79a01fc437649a9c8a217b3",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "PeopleOverviewPageUpdatePeopleMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation PeopleOverviewPageUpdatePeopleMutation(\n $input: UpdatePeopleInput!\n) {\n updatePeople(input: $input) {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n updatedAt\n version\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "3fba4023eb9fa1520ed88bb9b4501dc7";
|
||||
|
||||
export default node;
|
||||
Reference in New Issue
Block a user