Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/features/admin/items/ui/item-form-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export function ItemFormDialog({ mode, open, item, onOpenChange }: ItemFormDialo
</Button>
<Button
type="submit"
className="bg-black text-white hover:bg-zinc-800"
className="bg-zinc-950 text-white hover:bg-zinc-800"
disabled={isPending}
>
{isPending
Expand Down
2 changes: 1 addition & 1 deletion src/features/admin/items/ui/item-list-page-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function ItemListPageView({ items }: ItemListPageViewProps) {
</div>
<Button
type="button"
className="bg-black text-white hover:bg-zinc-800"
className="bg-zinc-950 text-white hover:bg-zinc-800"
onClick={() => setCreateOpen(true)}
>
<CirclePlus className="h-4 w-4" />
Expand Down
27 changes: 3 additions & 24 deletions src/features/admin/items/ui/item-table.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import { useEffect, useRef, useState } from "react";
import { Pencil, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Expand All @@ -21,31 +20,11 @@ type ItemTableProps = {
};

function ItemNameCell({ name }: { name: string }) {
const textRef = useRef<HTMLSpanElement>(null);
const [isTruncated, setIsTruncated] = useState(false);

useEffect(() => {
const element = textRef.current;
if (!element) {
return;
}

setIsTruncated(element.scrollWidth > element.clientWidth);
}, [name]);

const text = (
<span ref={textRef} className="block max-w-64 truncate">
{name}
</span>
);

if (!isTruncated) {
return text;
}

return (
<Tooltip>
<TooltipTrigger asChild>{text}</TooltipTrigger>
<TooltipTrigger asChild>
<span className="block max-w-64 truncate">{name}</span>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={8}>
{name}
</TooltipContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function LocationListPageView({ locations }: LocationListPageViewProps) {
<div className="flex justify-end">
<Button
type="button"
className="h-[52px] rounded-[10px] bg-black px-4 text-sm font-normal text-white hover:bg-zinc-800"
className="h-[52px] rounded-[10px] bg-zinc-950 px-4 text-sm font-normal text-white hover:bg-zinc-800"
onClick={() => dispatchDialog({ type: "open-create-root" })}
>
<CirclePlus className="h-4 w-4" />
Expand Down
21 changes: 13 additions & 8 deletions src/features/admin/tasks/server/queries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { requireAdminUser } from "@/lib/auth/guards";
import { APP_ROLES } from "@/lib/auth/roles";
import { createClient } from "@/lib/supabase/server";
import { getLeafLocationsWithRootGroup } from "@/features/tasks/model/location-options";
import type { Tables } from "@/types/schema.gen";
import { buildQuarterHourOptions, normalizeTimeValue, sortAdminTasks } from "../model/mappers";
import type { AdminTaskListPageData, TaskFormOption, TaskListQueryState } from "../model/types";
Expand All @@ -20,6 +21,16 @@ function toTaskFormOption(rows: { id: string; name: string }[], group: string):
.sort((left, right) => left.label.localeCompare(right.label, "ja"));
}

// Admin 側の場所フィルター仕様。今は User と同じく末端だけを候補にするが、
// 将来 Admin だけ中間階層を見せたい場合はこの変換だけを変更する。
function toLocationFormOptions(rows: LocationRow[]): TaskFormOption[] {
return getLeafLocationsWithRootGroup(rows).map(({ location, rootGroup }) => ({
value: location.location_id,
label: location.name,
group: rootGroup,
}));
}

export async function getAdminTaskListPageData(
queryState: TaskListQueryState,
): Promise<AdminTaskListPageData> {
Expand Down Expand Up @@ -63,7 +74,7 @@ export async function getAdminTaskListPageData(
const [tasksResult, itemsResult, locationsResult, leadersResult] = await Promise.all([
tasksQuery,
supabase.from("items").select("item_id,name").is("deleted", null),
supabase.from("locations").select("location_id,name").is("deleted", null),
supabase.from("locations").select("location_id,name,parent_location_id").is("deleted", null),
supabase
.from("users")
.select("user_id,name,role")
Expand Down Expand Up @@ -132,13 +143,7 @@ export async function getAdminTaskListPageData(
leaderRows.map((leader) => ({ id: leader.user_id, name: leader.name })),
"指揮者",
),
locations: toTaskFormOption(
locationRows.map((location) => ({
id: location.location_id,
name: location.name,
})),
"場所",
),
locations: toLocationFormOptions(locationRows),
timeOptions: buildQuarterHourOptions(),
},
};
Expand Down
81 changes: 70 additions & 11 deletions src/features/admin/tasks/ui/task-filter-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { Calendar, Filter, ListTodo, MapPin, Package, User, X } from "lucide-react";
import type { ReactNode } from "react";
import { Fragment, type ReactNode } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { EVENT_DAY_OPTIONS, STATUS_OPTIONS } from "../model/mappers";
import type { TaskFilterOptions, TaskFilterState } from "../model/types";

type FilterOption = { value: string; label: string; group: string };

type TaskFilterBarProps = {
filters: TaskFilterState;
filterOptions: TaskFilterOptions;
Expand All @@ -23,20 +27,40 @@ type SelectFilterProps = {
value: string;
icon?: ReactNode;
placeholder: string;
options: { value: string; label: string }[];
options: FilterOption[];
className?: string;
showGroups?: boolean;
onValueChange: (value: string) => void;
};

function SelectFilter({ value, icon, placeholder, options, onValueChange }: SelectFilterProps) {
function groupOptions(options: FilterOption[]): Map<string, FilterOption[]> {
return options.reduce((groups, option) => {
const groupOptions = groups.get(option.group) ?? [];
groupOptions.push(option);
groups.set(option.group, groupOptions);
return groups;
}, new Map<string, FilterOption[]>());
}

function SelectFilter({
value,
icon,
placeholder,
options,
className = "w-[148px]",
showGroups = false,
onValueChange,
}: SelectFilterProps) {
const selectedLabel = options.find((option) => option.value === value)?.label;
const optionGroups = groupOptions(options);

return (
<Select
value={value || "all"}
onValueChange={(nextValue) => onValueChange(nextValue === "all" ? "" : nextValue)}
>
<SelectTrigger
className="h-9 min-w-37 bg-white text-sm"
className={`h-9 bg-white text-sm ${className}`}
aria-label={selectedLabel ? `${placeholder}: ${selectedLabel}` : placeholder}
>
{icon && (
Expand All @@ -46,13 +70,40 @@ function SelectFilter({ value, icon, placeholder, options, onValueChange }: Sele
)}
<SelectValue placeholder={placeholder}>{value ? selectedLabel : placeholder}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectContent className="max-w-[280px]">
<SelectItem value="all">すべて</SelectItem>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
{showGroups
? Array.from(optionGroups.entries()).map(([groupName, groupItems]) =>
groupName ? (
<SelectGroup key={groupName}>
<SelectLabel>{groupName}</SelectLabel>
{groupItems.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span className="block max-w-[232px] truncate" title={option.label}>
{option.label}
</span>
</SelectItem>
))}
</SelectGroup>
) : (
<Fragment key="ungrouped-location-options">
{groupItems.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span className="block max-w-[232px] truncate" title={option.label}>
{option.label}
</span>
</SelectItem>
))}
</Fragment>
),
)
: options.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span className="block max-w-[232px] truncate" title={option.label}>
{option.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
);
Expand Down Expand Up @@ -187,27 +238,33 @@ export function TaskFilterBar({ filters, filterOptions, onChange }: TaskFilterBa
icon={<Package className="h-4 w-4 text-zinc-500" />}
placeholder="物品選択"
options={filterOptions.items}
className="w-[180px]"
onValueChange={(value) => onChange({ ...filters, itemId: value })}
/>
<SelectFilter
value={filters.leaderUserId}
icon={<User className="h-4 w-4 text-zinc-500" />}
placeholder="指揮者"
options={filterOptions.leaders}
className="w-[160px]"
onValueChange={(value) => onChange({ ...filters, leaderUserId: value })}
/>
<SelectFilter
value={filters.fromLocationId}
icon={<MapPin className="h-4 w-4 text-zinc-500" />}
placeholder="From"
options={filterOptions.locations}
className="w-[180px]"
showGroups
onValueChange={(value) => onChange({ ...filters, fromLocationId: value })}
/>
<SelectFilter
value={filters.toLocationId}
icon={<MapPin className="h-4 w-4 text-zinc-500" />}
placeholder="To"
options={filterOptions.locations}
className="w-[180px]"
showGroups
onValueChange={(value) => onChange({ ...filters, toLocationId: value })}
/>
</div>
Expand All @@ -223,7 +280,9 @@ export function TaskFilterBar({ filters, filterOptions, onChange }: TaskFilterBa
className="gap-1 rounded-full border-zinc-900 bg-white px-3 py-1 text-zinc-900 font-normal"
>
{tag.icon}
<span>{tag.label}</span>
<span className="max-w-[192px] truncate" title={tag.label}>
{tag.label}
</span>
<button
type="button"
onClick={() =>
Expand Down
62 changes: 41 additions & 21 deletions src/features/admin/tasks/ui/task-form-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { zodResolver } from "@hookform/resolvers/zod";
import { AlertCircle, Clock3, MapPin, NotebookPen, Package, Triangle } from "lucide-react";
import { useEffect, useMemo, useState, useTransition } from "react";
import { Fragment, useEffect, useMemo, useState, useTransition } from "react";
import { type UseFormReturn, useForm, useWatch } from "react-hook-form";
import { Button } from "@/components/ui/button";
import {
Expand Down Expand Up @@ -229,16 +229,26 @@ function TaskLocationSection({
<SelectValue placeholder="選択してください" />
</SelectTrigger>
<SelectContent>
{Object.entries(locationGroups).map(([groupName, options]) => (
<SelectGroup key={groupName}>
<SelectLabel>{groupName}</SelectLabel>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
))}
{Object.entries(locationGroups).map(([groupName, options]) =>
groupName ? (
<SelectGroup key={groupName}>
<SelectLabel>{groupName}</SelectLabel>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
) : (
<Fragment key="ungrouped-from-locations">
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</Fragment>
),
)}
</SelectContent>
</Select>
<FieldError message={form.formState.errors.fromLocationId?.message} />
Expand All @@ -262,16 +272,26 @@ function TaskLocationSection({
<SelectValue placeholder="選択してください" />
</SelectTrigger>
<SelectContent>
{Object.entries(locationGroups).map(([groupName, options]) => (
<SelectGroup key={groupName}>
<SelectLabel>{groupName}</SelectLabel>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
))}
{Object.entries(locationGroups).map(([groupName, options]) =>
groupName ? (
<SelectGroup key={groupName}>
<SelectLabel>{groupName}</SelectLabel>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
) : (
<Fragment key="ungrouped-to-locations">
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</Fragment>
),
)}
</SelectContent>
</Select>
<FieldError message={form.formState.errors.toLocationId?.message} />
Expand Down
2 changes: 1 addition & 1 deletion src/features/admin/tasks/ui/task-list-page-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function TaskListPageView({ tasks, filterOptions }: TaskListPageViewProps
<div className="flex justify-end">
<Button
type="button"
className="bg-black text-white hover:bg-zinc-800"
className="bg-zinc-950 text-white hover:bg-zinc-800"
disabled={isPending}
onClick={() => setCreateOpen(true)}
>
Expand Down
Loading
Loading