mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 14:42:05 +00:00
fix(services): satisfy registry maintainability gate
This commit is contained in:
parent
9c360fc95b
commit
2fb5d4a965
2 changed files with 203 additions and 165 deletions
|
|
@ -179,125 +179,15 @@ export function ServiceEditorDrawer({
|
||||||
<Box className="flex-1 overflow-y-auto p-5">
|
<Box className="flex-1 overflow-y-auto p-5">
|
||||||
<Stack spacing={2.5}>
|
<Stack spacing={2.5}>
|
||||||
{Boolean(formError) && <AlertMessage message={formError} />}
|
{Boolean(formError) && <AlertMessage message={formError} />}
|
||||||
<Typography variant="subtitle2" className="font-semibold">
|
<ServiceEditorFields
|
||||||
Service Information
|
mode={mode}
|
||||||
</Typography>
|
form={form}
|
||||||
<TextField
|
trades={trades}
|
||||||
label="Service Name"
|
templates={templates}
|
||||||
value={form.name}
|
canChangeActiveState={canChangeActiveState}
|
||||||
required
|
onUpdate={onUpdate}
|
||||||
fullWidth
|
onToggleType={onToggleType}
|
||||||
onChange={(event) => onUpdate("name", event.target.value)}
|
|
||||||
placeholder="e.g., HVAC Inspection"
|
|
||||||
/>
|
/>
|
||||||
<FormControl fullWidth required>
|
|
||||||
<InputLabel id="service-trade-label">Trade</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="service-trade-label"
|
|
||||||
label="Trade"
|
|
||||||
value={form.trade}
|
|
||||||
onChange={(event) => onUpdate("trade", event.target.value)}
|
|
||||||
>
|
|
||||||
{trades.map((trade) => (
|
|
||||||
<MenuItem key={String(trade.id)} value={trade.value}>
|
|
||||||
{trade.value}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl fullWidth required>
|
|
||||||
<InputLabel id="service-icon-label">Icon</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="service-icon-label"
|
|
||||||
label="Icon"
|
|
||||||
value={form.iconKey}
|
|
||||||
onChange={(event) => onUpdate("iconKey", event.target.value)}
|
|
||||||
renderValue={(value) => {
|
|
||||||
const icon = ICONS.find((item) => item.key === value);
|
|
||||||
return icon ? (
|
|
||||||
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
|
|
||||||
<icon.Icon size={18} />
|
|
||||||
<span>{icon.label}</span>
|
|
||||||
</Stack>
|
|
||||||
) : (
|
|
||||||
"Pick an icon"
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{ICONS.map(({ key, label, Icon }) => (
|
|
||||||
<MenuItem key={key} value={key}>
|
|
||||||
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
|
|
||||||
<Icon size={18} />
|
|
||||||
<span>{label}</span>
|
|
||||||
</Stack>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<FormHelperText>Pre-selected from the Trade; you can change it.</FormHelperText>
|
|
||||||
</FormControl>
|
|
||||||
<Typography variant="subtitle2" className="font-semibold">
|
|
||||||
Supported Work Order Types
|
|
||||||
</Typography>
|
|
||||||
<Stack direction="row" sx={{ flexWrap: "wrap", gap: 1 }}>
|
|
||||||
{SERVICE_WORK_ORDER_TYPES.map((type) => (
|
|
||||||
<FormControlLabel
|
|
||||||
key={type}
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={form.supportedWorkOrderTypes.includes(type)}
|
|
||||||
onChange={() => onToggleType(type)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={type}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
{mode === "edit" && canChangeActiveState && (
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
checked={form.isActive ?? true}
|
|
||||||
onChange={(event) => onUpdate("isActive", event.target.checked)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Active"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Divider />
|
|
||||||
<Typography variant="subtitle2" className="font-semibold">
|
|
||||||
Completion Document
|
|
||||||
</Typography>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
checked={form.requiresCompletionDocument}
|
|
||||||
onChange={(event) => {
|
|
||||||
onUpdate("requiresCompletionDocument", event.target.checked);
|
|
||||||
if (!event.target.checked) onUpdate("completionDocTemplateId", null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Requires completion document"
|
|
||||||
/>
|
|
||||||
{form.requiresCompletionDocument && (
|
|
||||||
<Autocomplete
|
|
||||||
options={templates}
|
|
||||||
value={
|
|
||||||
templates.find((template) => template.id === form.completionDocTemplateId) ?? null
|
|
||||||
}
|
|
||||||
onChange={(_, template) =>
|
|
||||||
onUpdate("completionDocTemplateId", template?.id ?? null)
|
|
||||||
}
|
|
||||||
getOptionLabel={(template) => template.name}
|
|
||||||
isOptionEqualToValue={(option, value) => String(option.id) === String(value.id)}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField {...params} label="Linked Template" placeholder="Select a template" />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Manage the document's own content on the Completion Document Templates page.
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
@ -312,6 +202,141 @@ export function ServiceEditorDrawer({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EditorFieldsProps = Pick<
|
||||||
|
EditorProps,
|
||||||
|
"mode" | "form" | "trades" | "templates" | "canChangeActiveState" | "onUpdate" | "onToggleType"
|
||||||
|
>;
|
||||||
|
|
||||||
|
function ServiceEditorFields({
|
||||||
|
mode,
|
||||||
|
form,
|
||||||
|
trades,
|
||||||
|
templates,
|
||||||
|
canChangeActiveState,
|
||||||
|
onUpdate,
|
||||||
|
onToggleType,
|
||||||
|
}: EditorFieldsProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography variant="subtitle2" className="font-semibold">
|
||||||
|
Service Information
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
label="Service Name"
|
||||||
|
value={form.name}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
onChange={(event) => onUpdate("name", event.target.value)}
|
||||||
|
placeholder="e.g., HVAC Inspection"
|
||||||
|
/>
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel id="service-trade-label">Trade</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="service-trade-label"
|
||||||
|
label="Trade"
|
||||||
|
value={form.trade}
|
||||||
|
onChange={(event) => onUpdate("trade", event.target.value)}
|
||||||
|
>
|
||||||
|
{trades.map((trade) => (
|
||||||
|
<MenuItem key={String(trade.id)} value={trade.value}>
|
||||||
|
{trade.value}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel id="service-icon-label">Icon</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="service-icon-label"
|
||||||
|
label="Icon"
|
||||||
|
value={form.iconKey}
|
||||||
|
onChange={(event) => onUpdate("iconKey", event.target.value)}
|
||||||
|
renderValue={(value) => {
|
||||||
|
const icon = ICONS.find((item) => item.key === value);
|
||||||
|
return icon ? (
|
||||||
|
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
|
||||||
|
<icon.Icon size={18} />
|
||||||
|
<span>{icon.label}</span>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
"Pick an icon"
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ICONS.map(({ key, label, Icon }) => (
|
||||||
|
<MenuItem key={key} value={key}>
|
||||||
|
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
|
||||||
|
<Icon size={18} />
|
||||||
|
<span>{label}</span>
|
||||||
|
</Stack>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<FormHelperText>Pre-selected from the Trade; you can change it.</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<Typography variant="subtitle2" className="font-semibold">
|
||||||
|
Supported Work Order Types
|
||||||
|
</Typography>
|
||||||
|
<Stack direction="row" sx={{ flexWrap: "wrap", gap: 1 }}>
|
||||||
|
{SERVICE_WORK_ORDER_TYPES.map((type) => (
|
||||||
|
<FormControlLabel
|
||||||
|
key={type}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={form.supportedWorkOrderTypes.includes(type)}
|
||||||
|
onChange={() => onToggleType(type)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={type}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
{mode === "edit" && canChangeActiveState && (
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={form.isActive ?? true}
|
||||||
|
onChange={(event) => onUpdate("isActive", event.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Active"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Divider />
|
||||||
|
<Typography variant="subtitle2" className="font-semibold">
|
||||||
|
Completion Document
|
||||||
|
</Typography>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={form.requiresCompletionDocument}
|
||||||
|
onChange={(event) => {
|
||||||
|
onUpdate("requiresCompletionDocument", event.target.checked);
|
||||||
|
if (!event.target.checked) onUpdate("completionDocTemplateId", null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Requires completion document"
|
||||||
|
/>
|
||||||
|
{form.requiresCompletionDocument && (
|
||||||
|
<Autocomplete
|
||||||
|
options={templates}
|
||||||
|
value={templates.find((template) => template.id === form.completionDocTemplateId) ?? null}
|
||||||
|
onChange={(_, template) => onUpdate("completionDocTemplateId", template?.id ?? null)}
|
||||||
|
getOptionLabel={(template) => template.name}
|
||||||
|
isOptionEqualToValue={(option, value) => String(option.id) === String(value.id)}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} label="Linked Template" placeholder="Select a template" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Manage the document's own content on the Completion Document Templates page.
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function AlertMessage({ message }: { message: string }) {
|
function AlertMessage({ message }: { message: string }) {
|
||||||
return (
|
return (
|
||||||
<Box className="rounded border border-red-200 bg-red-50 p-2 text-sm text-red-700">
|
<Box className="rounded border border-red-200 bg-red-50 p-2 text-sm text-red-700">
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import {
|
||||||
useServicesRegistryController,
|
useServicesRegistryController,
|
||||||
type ServiceStatus,
|
type ServiceStatus,
|
||||||
} from "@/domain/services/use-cases/use-services-registry-controller";
|
} from "@/domain/services/use-cases/use-services-registry-controller";
|
||||||
|
import type { Service } from "@/domain/services/types/service";
|
||||||
import {
|
import {
|
||||||
DeactivateServiceDialog,
|
DeactivateServiceDialog,
|
||||||
ServiceDetailsDrawer,
|
ServiceDetailsDrawer,
|
||||||
|
|
@ -100,53 +101,7 @@ export function ServicesRegistryView() {
|
||||||
onAdd={controller.canManage ? controller.openCreate : undefined}
|
onAdd={controller.canManage ? controller.openCreate : undefined}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Box className="overflow-x-auto rounded-lg border border-border bg-background">
|
<ServicesTable services={controller.filteredServices} onSelect={controller.setSelected} />
|
||||||
<Table aria-label="Services">
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>Service</TableCell>
|
|
||||||
<TableCell>Trade</TableCell>
|
|
||||||
<TableCell>Completion Document</TableCell>
|
|
||||||
<TableCell>Status</TableCell>
|
|
||||||
<TableCell />
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{controller.filteredServices.map((service) => (
|
|
||||||
<TableRow
|
|
||||||
hover
|
|
||||||
key={String(service.id)}
|
|
||||||
onClick={() => controller.setSelected(service)}
|
|
||||||
sx={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
<TableCell>
|
|
||||||
<Stack direction="row" spacing={1.25} sx={{ alignItems: "center" }}>
|
|
||||||
<Box className="flex size-8 items-center justify-center rounded-md bg-primary/10 text-primary">
|
|
||||||
<ServiceIcon iconKey={service.iconKey} />
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="body2" className="font-semibold">
|
|
||||||
{service.name}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
{service.supportedWorkOrderTypes.join(" · ")}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{service.trade}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{service.requiresCompletionDocument
|
|
||||||
? service.completionDocTemplate?.name || "No template linked yet"
|
|
||||||
: "Not required"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{service.isActive ? "Active" : "Inactive"}</TableCell>
|
|
||||||
<TableCell align="right">›</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ServiceDetailsDrawer
|
<ServiceDetailsDrawer
|
||||||
|
|
@ -182,6 +137,64 @@ export function ServicesRegistryView() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ServicesTable({
|
||||||
|
services,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
services: Service[];
|
||||||
|
onSelect: (service: Service) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box className="overflow-x-auto rounded-lg border border-border bg-background">
|
||||||
|
<Table aria-label="Services">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Service</TableCell>
|
||||||
|
<TableCell>Trade</TableCell>
|
||||||
|
<TableCell>Completion Document</TableCell>
|
||||||
|
<TableCell>Status</TableCell>
|
||||||
|
<TableCell />
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{services.map((service) => (
|
||||||
|
<TableRow
|
||||||
|
hover
|
||||||
|
key={String(service.id)}
|
||||||
|
onClick={() => onSelect(service)}
|
||||||
|
sx={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<Stack direction="row" spacing={1.25} sx={{ alignItems: "center" }}>
|
||||||
|
<Box className="flex size-8 items-center justify-center rounded-md bg-primary/10 text-primary">
|
||||||
|
<ServiceIcon iconKey={service.iconKey} />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" className="font-semibold">
|
||||||
|
{service.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{service.supportedWorkOrderTypes.join(" · ")}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{service.trade}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{service.requiresCompletionDocument
|
||||||
|
? service.completionDocTemplate?.name || "No template linked yet"
|
||||||
|
: "Not required"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{service.isActive ? "Active" : "Inactive"}</TableCell>
|
||||||
|
<TableCell align="right">›</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function EmptyServices({
|
function EmptyServices({
|
||||||
search,
|
search,
|
||||||
status,
|
status,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue