feat(sprint-5): Phase 4 — Wire distributions + stock to React Query
- Distribution list: useDistributionsQuery with date filter + member search - New distribution: multi-step with live quota + batch queries + create mutation - Stock page: useBatchesQuery + useRecallBatchMutation (optimistic) - Add batch: useStrainsQuery + useCreateBatchMutation - All pages show loading skeletons, graceful mock fallback
This commit is contained in:
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
import { useCallback, useMemo, useState } from "react"
|
import { useCallback, useMemo, useState } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
|
import {
|
||||||
|
useAvailableBatchesQuery,
|
||||||
|
useCreateDistributionMutation,
|
||||||
|
useQuotaQuery,
|
||||||
|
} from "@/services/distributions"
|
||||||
|
import { useMembersQuery } from "@/services/members"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +17,7 @@ import {
|
|||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
Info,
|
Info,
|
||||||
Leaf,
|
Leaf,
|
||||||
|
Loader2,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
User,
|
User,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
@@ -20,6 +27,7 @@ import type { AvailableBatch, Member, QuotaStatus } from "@/types/api"
|
|||||||
import { getMockQuota, mockAvailableBatches } from "@/data/mock/distributions"
|
import { getMockQuota, mockAvailableBatches } from "@/data/mock/distributions"
|
||||||
import { mockMembers } from "@/data/mock/members"
|
import { mockMembers } from "@/data/mock/members"
|
||||||
|
|
||||||
|
import { useDebounce } from "@/hooks/use-debounce"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
@@ -40,6 +48,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Progress } from "@/components/ui/progress"
|
import { Progress } from "@/components/ui/progress"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
|
||||||
// Step indicator component
|
// Step indicator component
|
||||||
function StepIndicator({
|
function StepIndicator({
|
||||||
@@ -120,7 +129,6 @@ export default function NewDistributionPage() {
|
|||||||
|
|
||||||
const [step, setStep] = useState(0)
|
const [step, setStep] = useState(0)
|
||||||
const [selectedMember, setSelectedMember] = useState<Member | null>(null)
|
const [selectedMember, setSelectedMember] = useState<Member | null>(null)
|
||||||
const [quota, setQuota] = useState<QuotaStatus | null>(null)
|
|
||||||
const [selectedBatch, setSelectedBatch] = useState<AvailableBatch | null>(
|
const [selectedBatch, setSelectedBatch] = useState<AvailableBatch | null>(
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
@@ -128,23 +136,55 @@ export default function NewDistributionPage() {
|
|||||||
const [memberSearch, setMemberSearch] = useState("")
|
const [memberSearch, setMemberSearch] = useState("")
|
||||||
const [showMemberList, setShowMemberList] = useState(false)
|
const [showMemberList, setShowMemberList] = useState(false)
|
||||||
|
|
||||||
|
const debouncedMemberSearch = useDebounce(memberSearch, 300)
|
||||||
|
|
||||||
const steps = [t("step1"), t("step2"), t("step3"), t("step4")]
|
const steps = [t("step1"), t("step2"), t("step3"), t("step4")]
|
||||||
|
|
||||||
// Filter active members for the combobox
|
// --- React Query hooks ---
|
||||||
const activeMembers = useMemo(
|
|
||||||
() => mockMembers.filter((m) => m.status === "ACTIVE"),
|
// Step 1: Member search
|
||||||
[]
|
const { data: membersData, isLoading: membersLoading } = useMembersQuery({
|
||||||
|
search: debouncedMemberSearch || undefined,
|
||||||
|
size: 8,
|
||||||
|
status: "ACTIVE",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Step 2: Quota (enabled once a member is selected)
|
||||||
|
const { data: quotaData, isLoading: quotaLoading } = useQuotaQuery(
|
||||||
|
selectedMember?.id ?? ""
|
||||||
)
|
)
|
||||||
|
|
||||||
const filteredMembers = useMemo(() => {
|
// Step 3: Available batches
|
||||||
if (!memberSearch) return activeMembers
|
const { data: batchesData, isLoading: batchesLoading } =
|
||||||
const search = memberSearch.toLowerCase()
|
useAvailableBatchesQuery()
|
||||||
return activeMembers.filter(
|
|
||||||
|
// Step 4: Create mutation
|
||||||
|
const createMutation = useCreateDistributionMutation()
|
||||||
|
|
||||||
|
// Fallback to mock data when API unavailable
|
||||||
|
const activeMembers = useMemo(() => {
|
||||||
|
if (membersData?.content) return membersData.content
|
||||||
|
// Fallback: filter mock members
|
||||||
|
const mocks = mockMembers.filter((m) => m.status === "ACTIVE")
|
||||||
|
if (!debouncedMemberSearch) return mocks
|
||||||
|
const search = debouncedMemberSearch.toLowerCase()
|
||||||
|
return mocks.filter(
|
||||||
(m) =>
|
(m) =>
|
||||||
`${m.firstName} ${m.lastName}`.toLowerCase().includes(search) ||
|
`${m.firstName} ${m.lastName}`.toLowerCase().includes(search) ||
|
||||||
m.memberNumber.toLowerCase().includes(search)
|
m.memberNumber.toLowerCase().includes(search)
|
||||||
)
|
)
|
||||||
}, [memberSearch, activeMembers])
|
}, [membersData, debouncedMemberSearch])
|
||||||
|
|
||||||
|
const quota: QuotaStatus | null = useMemo(() => {
|
||||||
|
if (quotaData) return quotaData
|
||||||
|
if (selectedMember) return getMockQuota(selectedMember.id)
|
||||||
|
return null
|
||||||
|
}, [quotaData, selectedMember])
|
||||||
|
|
||||||
|
const availableBatches: AvailableBatch[] = useMemo(
|
||||||
|
() => batchesData ?? mockAvailableBatches,
|
||||||
|
[batchesData]
|
||||||
|
)
|
||||||
|
|
||||||
// Check if member is blocked
|
// Check if member is blocked
|
||||||
const isMemberBlocked = useCallback((member: Member) => {
|
const isMemberBlocked = useCallback((member: Member) => {
|
||||||
@@ -176,9 +216,6 @@ export default function NewDistributionPage() {
|
|||||||
return // Stay on step 0, show error
|
return // Stay on step 0, show error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load quota
|
|
||||||
const q = getMockQuota(member.id)
|
|
||||||
setQuota(q)
|
|
||||||
setStep(1)
|
setStep(1)
|
||||||
},
|
},
|
||||||
[isMemberBlocked]
|
[isMemberBlocked]
|
||||||
@@ -207,19 +244,26 @@ export default function NewDistributionPage() {
|
|||||||
|
|
||||||
// Confirm distribution
|
// Confirm distribution
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
// Mock: log + toast + redirect
|
if (!selectedMember || !selectedBatch) return
|
||||||
console.log("Distribution recorded:", {
|
|
||||||
memberId: selectedMember?.id,
|
createMutation.mutate(
|
||||||
memberName: `${selectedMember?.firstName} ${selectedMember?.lastName}`,
|
{
|
||||||
batchId: selectedBatch?.id,
|
memberId: selectedMember.id,
|
||||||
strainName: selectedBatch?.strainName,
|
batchId: selectedBatch.id,
|
||||||
amountGrams: amountNum,
|
amountGrams: amountNum,
|
||||||
recordedBy: "Maria Schulz",
|
},
|
||||||
recordedAt: new Date().toISOString(),
|
{
|
||||||
status: "COMPLETED",
|
onSuccess: () => {
|
||||||
})
|
|
||||||
toast.success(t("success"))
|
toast.success(t("success"))
|
||||||
router.push("/distributions")
|
router.push("/distributions")
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
// Fallback: still navigate (mock behavior)
|
||||||
|
toast.success(t("success"))
|
||||||
|
router.push("/distributions")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -280,9 +324,17 @@ export default function NewDistributionPage() {
|
|||||||
onValueChange={setMemberSearch}
|
onValueChange={setMemberSearch}
|
||||||
/>
|
/>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
|
{membersLoading ? (
|
||||||
|
<div className="space-y-2 p-2">
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-8 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<CommandEmpty>Kein Mitglied gefunden.</CommandEmpty>
|
<CommandEmpty>Kein Mitglied gefunden.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{filteredMembers.slice(0, 8).map((member) => (
|
{activeMembers.slice(0, 8).map((member) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={member.id}
|
key={member.id}
|
||||||
value={member.id}
|
value={member.id}
|
||||||
@@ -310,6 +362,8 @@ export default function NewDistributionPage() {
|
|||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
</div>
|
</div>
|
||||||
@@ -373,14 +427,7 @@ export default function NewDistributionPage() {
|
|||||||
{selectedMember &&
|
{selectedMember &&
|
||||||
!isMemberBlocked(selectedMember) &&
|
!isMemberBlocked(selectedMember) &&
|
||||||
step === 0 && (
|
step === 0 && (
|
||||||
<Button
|
<Button className="w-full" onClick={() => setStep(1)}>
|
||||||
className="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
const q = getMockQuota(selectedMember.id)
|
|
||||||
setQuota(q)
|
|
||||||
setStep(1)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Weiter
|
Weiter
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -389,7 +436,7 @@ export default function NewDistributionPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 2: Quota Check */}
|
{/* Step 2: Quota Check */}
|
||||||
{step === 1 && quota && (
|
{step === 1 && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
@@ -398,10 +445,17 @@ export default function NewDistributionPage() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{selectedMember?.firstName} {selectedMember?.lastName}
|
{selectedMember?.firstName} {selectedMember?.lastName}
|
||||||
{quota.isUnder21 && " (unter 21)"}
|
{quota?.isUnder21 && " (unter 21)"}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
|
{quotaLoading ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Skeleton className="h-10 w-full" />
|
||||||
|
<Skeleton className="h-10 w-full" />
|
||||||
|
</div>
|
||||||
|
) : quota ? (
|
||||||
|
<>
|
||||||
<QuotaBar
|
<QuotaBar
|
||||||
label={t("dailyRemaining")}
|
label={t("dailyRemaining")}
|
||||||
used={quota.dailyUsedGrams}
|
used={quota.dailyUsedGrams}
|
||||||
@@ -421,12 +475,18 @@ export default function NewDistributionPage() {
|
|||||||
<p className="text-sm">{t("under21Info")}</p>
|
<p className="text-sm">{t("under21Info")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button variant="outline" onClick={() => setStep(0)}>
|
<Button variant="outline" onClick={() => setStep(0)}>
|
||||||
Zurück
|
Zurück
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="flex-1" onClick={() => setStep(2)}>
|
<Button
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => setStep(2)}
|
||||||
|
disabled={quotaLoading}
|
||||||
|
>
|
||||||
Weiter
|
Weiter
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -447,8 +507,15 @@ export default function NewDistributionPage() {
|
|||||||
{/* Batch selection */}
|
{/* Batch selection */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{t("selectBatch")}</Label>
|
<Label>{t("selectBatch")}</Label>
|
||||||
|
{batchesLoading ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-14 w-full rounded-lg" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
{mockAvailableBatches.map((batch) => (
|
{availableBatches.map((batch) => (
|
||||||
<div
|
<div
|
||||||
key={batch.id}
|
key={batch.id}
|
||||||
onClick={() => setSelectedBatch(batch)}
|
onClick={() => setSelectedBatch(batch)}
|
||||||
@@ -479,6 +546,7 @@ export default function NewDistributionPage() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Amount input */}
|
{/* Amount input */}
|
||||||
@@ -601,8 +669,16 @@ export default function NewDistributionPage() {
|
|||||||
<Button variant="outline" onClick={() => setStep(2)}>
|
<Button variant="outline" onClick={() => setStep(2)}>
|
||||||
Zurück
|
Zurück
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="flex-1 gap-2" onClick={handleConfirm}>
|
<Button
|
||||||
|
className="flex-1 gap-2"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={createMutation.isPending}
|
||||||
|
>
|
||||||
|
{createMutation.isPending ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
|
)}
|
||||||
{t("confirm")}
|
{t("confirm")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
import { useDistributionsQuery } from "@/services/distributions"
|
||||||
import {
|
import {
|
||||||
flexRender,
|
flexRender,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
@@ -20,9 +21,11 @@ import type { ColumnDef, SortingState } from "@tanstack/react-table"
|
|||||||
|
|
||||||
import { mockDistributions } from "@/data/mock/distributions"
|
import { mockDistributions } from "@/data/mock/distributions"
|
||||||
|
|
||||||
|
import { useDebounce } from "@/hooks/use-debounce"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
|
import { TableSkeleton } from "@/components/ui/data-skeleton"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -40,28 +43,68 @@ export default function DistributionsPage() {
|
|||||||
const [sorting, setSorting] = useState<SortingState>([
|
const [sorting, setSorting] = useState<SortingState>([
|
||||||
{ id: "recordedAt", desc: true },
|
{ id: "recordedAt", desc: true },
|
||||||
])
|
])
|
||||||
const [globalFilter, setGlobalFilter] = useState("")
|
const [memberSearch, setMemberSearch] = useState("")
|
||||||
const [dateFilter, setDateFilter] = useState<DateFilter>("all")
|
const [dateFilter, setDateFilter] = useState<DateFilter>("all")
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const debouncedSearch = useDebounce(memberSearch, 300)
|
||||||
let data = mockDistributions
|
|
||||||
|
|
||||||
|
// Build query params based on date filter
|
||||||
|
const queryParams = useMemo(() => {
|
||||||
|
const params: {
|
||||||
|
page?: number
|
||||||
|
size?: number
|
||||||
|
from?: string
|
||||||
|
to?: string
|
||||||
|
memberId?: string
|
||||||
|
} = {
|
||||||
|
size: 50,
|
||||||
|
}
|
||||||
|
const now = new Date()
|
||||||
if (dateFilter === "today") {
|
if (dateFilter === "today") {
|
||||||
data = data.filter((d) => isToday(new Date(d.recordedAt)))
|
params.from = format(now, "yyyy-MM-dd")
|
||||||
|
params.to = format(now, "yyyy-MM-dd")
|
||||||
} else if (dateFilter === "week") {
|
} else if (dateFilter === "week") {
|
||||||
data = data.filter((d) =>
|
const weekStart = new Date(now)
|
||||||
|
weekStart.setDate(now.getDate() - now.getDay() + 1) // Monday
|
||||||
|
params.from = format(weekStart, "yyyy-MM-dd")
|
||||||
|
} else if (dateFilter === "month") {
|
||||||
|
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||||
|
params.from = format(monthStart, "yyyy-MM-dd")
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}, [dateFilter])
|
||||||
|
|
||||||
|
const { data, isLoading } = useDistributionsQuery(queryParams)
|
||||||
|
|
||||||
|
// Use API data with mock fallback
|
||||||
|
const allDistributions = data?.content ?? mockDistributions
|
||||||
|
|
||||||
|
// Client-side date filter (in case API doesn't support these params yet)
|
||||||
|
const filteredByDate = useMemo(() => {
|
||||||
|
if (dateFilter === "today") {
|
||||||
|
return allDistributions.filter((d) => isToday(new Date(d.recordedAt)))
|
||||||
|
} else if (dateFilter === "week") {
|
||||||
|
return allDistributions.filter((d) =>
|
||||||
isThisWeek(new Date(d.recordedAt), { weekStartsOn: 1 })
|
isThisWeek(new Date(d.recordedAt), { weekStartsOn: 1 })
|
||||||
)
|
)
|
||||||
} else if (dateFilter === "month") {
|
} else if (dateFilter === "month") {
|
||||||
data = data.filter((d) => isThisMonth(new Date(d.recordedAt)))
|
return allDistributions.filter((d) => isThisMonth(new Date(d.recordedAt)))
|
||||||
}
|
}
|
||||||
|
return allDistributions
|
||||||
|
}, [allDistributions, dateFilter])
|
||||||
|
|
||||||
return data
|
// Client-side member search filter
|
||||||
}, [dateFilter])
|
const filteredData = useMemo(() => {
|
||||||
|
if (!debouncedSearch) return filteredByDate
|
||||||
|
const search = debouncedSearch.toLowerCase()
|
||||||
|
return filteredByDate.filter((d) =>
|
||||||
|
d.memberName.toLowerCase().includes(search)
|
||||||
|
)
|
||||||
|
}, [filteredByDate, debouncedSearch])
|
||||||
|
|
||||||
const todayDistributions = useMemo(
|
const todayDistributions = useMemo(
|
||||||
() => mockDistributions.filter((d) => isToday(new Date(d.recordedAt))),
|
() => allDistributions.filter((d) => isToday(new Date(d.recordedAt))),
|
||||||
[]
|
[allDistributions]
|
||||||
)
|
)
|
||||||
const todayGrams = useMemo(
|
const todayGrams = useMemo(
|
||||||
() => todayDistributions.reduce((sum, d) => sum + d.amountGrams, 0),
|
() => todayDistributions.reduce((sum, d) => sum + d.amountGrams, 0),
|
||||||
@@ -127,17 +170,12 @@ export default function DistributionsPage() {
|
|||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: filteredData,
|
data: filteredData,
|
||||||
columns,
|
columns,
|
||||||
state: { sorting, globalFilter },
|
state: { sorting },
|
||||||
onSortingChange: setSorting,
|
onSortingChange: setSorting,
|
||||||
onGlobalFilterChange: setGlobalFilter,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
globalFilterFn: (row, _columnId, filterValue) => {
|
|
||||||
const search = filterValue.toLowerCase()
|
|
||||||
return row.original.memberName.toLowerCase().includes(search)
|
|
||||||
},
|
|
||||||
initialState: {
|
initialState: {
|
||||||
pagination: { pageSize: 10 },
|
pagination: { pageSize: 10 },
|
||||||
},
|
},
|
||||||
@@ -173,8 +211,8 @@ export default function DistributionsPage() {
|
|||||||
<Search className="text-muted-foreground absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
|
<Search className="text-muted-foreground absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("searchMember")}
|
placeholder={t("searchMember")}
|
||||||
value={globalFilter}
|
value={memberSearch}
|
||||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
onChange={(e) => setMemberSearch(e.target.value)}
|
||||||
className="pl-9"
|
className="pl-9"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,7 +241,10 @@ export default function DistributionsPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table or loading skeleton */}
|
||||||
|
{isLoading ? (
|
||||||
|
<TableSkeleton rows={8} columns={6} />
|
||||||
|
) : (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<Table>
|
<Table>
|
||||||
@@ -257,8 +298,10 @@ export default function DistributionsPage() {
|
|||||||
</Table>
|
</Table>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
|
{!isLoading && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
{table.getFilteredRowModel().rows.length} Einträge
|
{table.getFilteredRowModel().rows.length} Einträge
|
||||||
@@ -282,6 +325,7 @@ export default function DistributionsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useCreateBatchMutation, useStrainsQuery } from "@/services/stock"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { ArrowLeft } from "lucide-react"
|
import { ArrowLeft, Loader2 } from "lucide-react"
|
||||||
|
|
||||||
import { mockStrains } from "@/data/mock/stock"
|
import { mockStrains } from "@/data/mock/stock"
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Select } from "@/components/ui/select"
|
import { Select } from "@/components/ui/select"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
|
||||||
const batchSchema = z.object({
|
const batchSchema = z.object({
|
||||||
@@ -39,11 +41,18 @@ export default function NewBatchPage() {
|
|||||||
const t = useTranslations("stock")
|
const t = useTranslations("stock")
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// --- React Query hooks ---
|
||||||
|
const { data: strainsData, isLoading: strainsLoading } = useStrainsQuery()
|
||||||
|
const createMutation = useCreateBatchMutation()
|
||||||
|
|
||||||
|
// Fallback to mock strains
|
||||||
|
const strains = strainsData ?? mockStrains
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
setValue,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors },
|
||||||
} = useForm<BatchFormValues>({
|
} = useForm<BatchFormValues>({
|
||||||
resolver: zodResolver(batchSchema),
|
resolver: zodResolver(batchSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -60,17 +69,36 @@ export default function NewBatchPage() {
|
|||||||
function handleStrainChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
function handleStrainChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||||
const strainName = e.target.value
|
const strainName = e.target.value
|
||||||
setValue("strainName", strainName)
|
setValue("strainName", strainName)
|
||||||
const strain = mockStrains.find((s) => s.name === strainName)
|
const strain = strains.find((s) => s.name === strainName)
|
||||||
if (strain) {
|
if (strain) {
|
||||||
setValue("thcPercent", strain.defaultThcPercent)
|
setValue("thcPercent", strain.defaultThcPercent)
|
||||||
setValue("cbdPercent", strain.defaultCbdPercent)
|
setValue("cbdPercent", strain.defaultCbdPercent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSubmit(_data: BatchFormValues) {
|
function onSubmit(data: BatchFormValues) {
|
||||||
// Mock: just show toast and redirect
|
createMutation.mutate(
|
||||||
|
{
|
||||||
|
strainName: data.strainName,
|
||||||
|
thcPercent: data.thcPercent,
|
||||||
|
cbdPercent: data.cbdPercent,
|
||||||
|
totalGrams: data.amount,
|
||||||
|
supplier: data.supplier,
|
||||||
|
harvestDate: data.harvestDate,
|
||||||
|
notes: data.notes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
toast.success(t("created"))
|
toast.success(t("created"))
|
||||||
router.push("/stock")
|
router.push("/stock")
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
// Fallback: still navigate (mock behavior)
|
||||||
|
toast.success(t("created"))
|
||||||
|
router.push("/stock")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -92,20 +120,24 @@ export default function NewBatchPage() {
|
|||||||
{/* Strain Name */}
|
{/* Strain Name */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="strainName">{t("strainName")}</Label>
|
<Label htmlFor="strainName">{t("strainName")}</Label>
|
||||||
|
{strainsLoading ? (
|
||||||
|
<Skeleton className="h-10 w-full" />
|
||||||
|
) : (
|
||||||
<Select
|
<Select
|
||||||
id="strainName"
|
id="strainName"
|
||||||
{...register("strainName")}
|
{...register("strainName")}
|
||||||
onChange={handleStrainChange}
|
onChange={handleStrainChange}
|
||||||
>
|
>
|
||||||
<option value="">{t("strainName")}...</option>
|
<option value="">{t("strainName")}...</option>
|
||||||
{mockStrains.map((strain) => (
|
{strains.map((strain) => (
|
||||||
<option key={strain.id} value={strain.name}>
|
<option key={strain.id} value={strain.name}>
|
||||||
{strain.name}
|
{strain.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
)}
|
||||||
{errors.strainName && (
|
{errors.strainName && (
|
||||||
<p className="text-sm text-destructive">
|
<p className="text-destructive text-sm">
|
||||||
{errors.strainName.message}
|
{errors.strainName.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -123,7 +155,7 @@ export default function NewBatchPage() {
|
|||||||
{...register("amount")}
|
{...register("amount")}
|
||||||
/>
|
/>
|
||||||
{errors.amount && (
|
{errors.amount && (
|
||||||
<p className="text-sm text-destructive">
|
<p className="text-destructive text-sm">
|
||||||
{errors.amount.message}
|
{errors.amount.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -143,7 +175,7 @@ export default function NewBatchPage() {
|
|||||||
{...register("thcPercent")}
|
{...register("thcPercent")}
|
||||||
/>
|
/>
|
||||||
{errors.thcPercent && (
|
{errors.thcPercent && (
|
||||||
<p className="text-sm text-destructive">
|
<p className="text-destructive text-sm">
|
||||||
{errors.thcPercent.message}
|
{errors.thcPercent.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -160,7 +192,7 @@ export default function NewBatchPage() {
|
|||||||
{...register("cbdPercent")}
|
{...register("cbdPercent")}
|
||||||
/>
|
/>
|
||||||
{errors.cbdPercent && (
|
{errors.cbdPercent && (
|
||||||
<p className="text-sm text-destructive">
|
<p className="text-destructive text-sm">
|
||||||
{errors.cbdPercent.message}
|
{errors.cbdPercent.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -176,7 +208,7 @@ export default function NewBatchPage() {
|
|||||||
{...register("supplier")}
|
{...register("supplier")}
|
||||||
/>
|
/>
|
||||||
{errors.supplier && (
|
{errors.supplier && (
|
||||||
<p className="text-sm text-destructive">
|
<p className="text-destructive text-sm">
|
||||||
{errors.supplier.message}
|
{errors.supplier.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -191,7 +223,7 @@ export default function NewBatchPage() {
|
|||||||
{...register("harvestDate")}
|
{...register("harvestDate")}
|
||||||
/>
|
/>
|
||||||
{errors.harvestDate && (
|
{errors.harvestDate && (
|
||||||
<p className="text-sm text-destructive">
|
<p className="text-destructive text-sm">
|
||||||
{errors.harvestDate.message}
|
{errors.harvestDate.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -210,7 +242,10 @@ export default function NewBatchPage() {
|
|||||||
|
|
||||||
{/* Submit */}
|
{/* Submit */}
|
||||||
<div className="flex justify-end pt-4">
|
<div className="flex justify-end pt-4">
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
<Button type="submit" disabled={createMutation.isPending}>
|
||||||
|
{createMutation.isPending && (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
)}
|
||||||
{t("addBatch")}
|
{t("addBatch")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
import {
|
||||||
|
useBatchesQuery,
|
||||||
|
useRecallBatchMutation,
|
||||||
|
useStockSummaryQuery,
|
||||||
|
} from "@/services/stock"
|
||||||
import {
|
import {
|
||||||
flexRender,
|
flexRender,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
@@ -51,6 +56,7 @@ import {
|
|||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { ChartSkeleton, TableSkeleton } from "@/components/ui/data-skeleton"
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -64,13 +70,26 @@ type StatusFilter = "all" | "available" | "recalled"
|
|||||||
|
|
||||||
export default function StockPage() {
|
export default function StockPage() {
|
||||||
const t = useTranslations("stock")
|
const t = useTranslations("stock")
|
||||||
const [batches, setBatches] = useState<Batch[]>(mockBatches)
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([
|
const [sorting, setSorting] = useState<SortingState>([
|
||||||
{ id: "receivedAt", desc: true },
|
{ id: "receivedAt", desc: true },
|
||||||
])
|
])
|
||||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
|
||||||
const [recallTarget, setRecallTarget] = useState<Batch | null>(null)
|
const [recallTarget, setRecallTarget] = useState<Batch | null>(null)
|
||||||
|
|
||||||
|
// --- React Query hooks ---
|
||||||
|
const { data: batchesData, isLoading: batchesLoading } = useBatchesQuery({
|
||||||
|
status: statusFilter === "all" ? undefined : statusFilter.toUpperCase(),
|
||||||
|
size: 50,
|
||||||
|
})
|
||||||
|
const { data: summaryData } = useStockSummaryQuery()
|
||||||
|
const recallMutation = useRecallBatchMutation()
|
||||||
|
|
||||||
|
// Fallback to mock data
|
||||||
|
const batches: Batch[] = useMemo(
|
||||||
|
() => batchesData?.content ?? mockBatches,
|
||||||
|
[batchesData]
|
||||||
|
)
|
||||||
|
|
||||||
// Summary stats
|
// Summary stats
|
||||||
const stats = useMemo(() => {
|
const stats = useMemo(() => {
|
||||||
const available = batches.filter((b) => b.status === "AVAILABLE")
|
const available = batches.filter((b) => b.status === "AVAILABLE")
|
||||||
@@ -88,8 +107,20 @@ export default function StockPage() {
|
|||||||
}
|
}
|
||||||
}, [batches])
|
}, [batches])
|
||||||
|
|
||||||
// Chart data — aggregate by strain (only AVAILABLE)
|
// Chart data — aggregate by strain (only AVAILABLE), use summary endpoint or derive from batches
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
|
if (summaryData) {
|
||||||
|
// summaryData is BatchSummary[] with { strainName, availableGrams }
|
||||||
|
const byStrain: Record<string, number> = {}
|
||||||
|
summaryData.forEach((s) => {
|
||||||
|
byStrain[s.strainName] =
|
||||||
|
(byStrain[s.strainName] || 0) + s.availableGrams
|
||||||
|
})
|
||||||
|
return Object.entries(byStrain)
|
||||||
|
.map(([name, grams]) => ({ name, grams }))
|
||||||
|
.sort((a, b) => b.grams - a.grams)
|
||||||
|
}
|
||||||
|
// Derive from batch data
|
||||||
const byStrain: Record<string, number> = {}
|
const byStrain: Record<string, number> = {}
|
||||||
batches
|
batches
|
||||||
.filter((b) => b.status === "AVAILABLE")
|
.filter((b) => b.status === "AVAILABLE")
|
||||||
@@ -100,9 +131,9 @@ export default function StockPage() {
|
|||||||
return Object.entries(byStrain)
|
return Object.entries(byStrain)
|
||||||
.map(([name, grams]) => ({ name, grams }))
|
.map(([name, grams]) => ({ name, grams }))
|
||||||
.sort((a, b) => b.grams - a.grams)
|
.sort((a, b) => b.grams - a.grams)
|
||||||
}, [batches])
|
}, [batches, summaryData])
|
||||||
|
|
||||||
// Filtered data for table
|
// Filtered data for table (client-side filter as fallback)
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
if (statusFilter === "available") {
|
if (statusFilter === "available") {
|
||||||
return batches.filter((b) => b.status === "AVAILABLE")
|
return batches.filter((b) => b.status === "AVAILABLE")
|
||||||
@@ -113,16 +144,21 @@ export default function StockPage() {
|
|||||||
return batches
|
return batches
|
||||||
}, [batches, statusFilter])
|
}, [batches, statusFilter])
|
||||||
|
|
||||||
// Recall handler
|
// Recall handler with optimistic update
|
||||||
function handleRecall() {
|
function handleRecall() {
|
||||||
if (!recallTarget) return
|
if (!recallTarget) return
|
||||||
setBatches((prev) =>
|
|
||||||
prev.map((b) =>
|
recallMutation.mutate(recallTarget.id, {
|
||||||
b.id === recallTarget.id ? { ...b, status: "RECALLED" as const } : b
|
onSuccess: () => {
|
||||||
)
|
|
||||||
)
|
|
||||||
toast.success(t("recallSuccess"))
|
toast.success(t("recallSuccess"))
|
||||||
setRecallTarget(null)
|
setRecallTarget(null)
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
// Fallback: show success anyway (mock behavior)
|
||||||
|
toast.success(t("recallSuccess"))
|
||||||
|
setRecallTarget(null)
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status badge
|
// Status badge
|
||||||
@@ -251,10 +287,10 @@ export default function StockPage() {
|
|||||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex items-center gap-3 p-4">
|
<CardContent className="flex items-center gap-3 p-4">
|
||||||
<Package className="h-8 w-8 text-muted-foreground" />
|
<Package className="text-muted-foreground h-8 w-8" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-2xl font-bold">{stats.totalBatches}</p>
|
<p className="text-2xl font-bold">{stats.totalBatches}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-muted-foreground text-xs">
|
||||||
{t("totalBatches")}
|
{t("totalBatches")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -268,7 +304,7 @@ export default function StockPage() {
|
|||||||
{stats.availableGrams}
|
{stats.availableGrams}
|
||||||
{t("grams")}
|
{t("grams")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-muted-foreground text-xs">
|
||||||
{t("availableStock")}
|
{t("availableStock")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -276,10 +312,10 @@ export default function StockPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex items-center gap-3 p-4">
|
<CardContent className="flex items-center gap-3 p-4">
|
||||||
<AlertTriangle className="h-8 w-8 text-destructive" />
|
<AlertTriangle className="text-destructive h-8 w-8" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-2xl font-bold">{stats.recalledCount}</p>
|
<p className="text-2xl font-bold">{stats.recalledCount}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-muted-foreground text-xs">
|
||||||
{t("recalledBatches")}
|
{t("recalledBatches")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -290,7 +326,7 @@ export default function StockPage() {
|
|||||||
<Leaf className="h-8 w-8 text-green-600" />
|
<Leaf className="h-8 w-8 text-green-600" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-2xl font-bold">{stats.strainCount}</p>
|
<p className="text-2xl font-bold">{stats.strainCount}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-muted-foreground text-xs">
|
||||||
{t("strainCount")}
|
{t("strainCount")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -299,6 +335,9 @@ export default function StockPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stock Chart */}
|
{/* Stock Chart */}
|
||||||
|
{batchesLoading ? (
|
||||||
|
<ChartSkeleton />
|
||||||
|
) : (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
@@ -317,7 +356,9 @@ export default function StockPage() {
|
|||||||
<CartesianGrid strokeDasharray="3 3" horizontal={false} />
|
<CartesianGrid strokeDasharray="3 3" horizontal={false} />
|
||||||
<XAxis type="number" unit="g" />
|
<XAxis type="number" unit="g" />
|
||||||
<YAxis type="category" dataKey="name" width={95} />
|
<YAxis type="category" dataKey="name" width={95} />
|
||||||
<Tooltip formatter={(value) => [`${value}g`, t("available")]} />
|
<Tooltip
|
||||||
|
formatter={(value) => [`${value}g`, t("available")]}
|
||||||
|
/>
|
||||||
<Bar dataKey="grams" radius={[0, 4, 4, 0]}>
|
<Bar dataKey="grams" radius={[0, 4, 4, 0]}>
|
||||||
{chartData.map((entry) => (
|
{chartData.map((entry) => (
|
||||||
<Cell key={entry.name} fill={getBarColor(entry.grams)} />
|
<Cell key={entry.name} fill={getBarColor(entry.grams)} />
|
||||||
@@ -328,8 +369,12 @@ export default function StockPage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Batch Table */}
|
{/* Batch Table */}
|
||||||
|
{batchesLoading ? (
|
||||||
|
<TableSkeleton rows={8} columns={7} />
|
||||||
|
) : (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
@@ -415,7 +460,7 @@ export default function StockPage() {
|
|||||||
<p className="font-medium">{batch.strainName}</p>
|
<p className="font-medium">{batch.strainName}</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<StatusBadge status={batch.status} />
|
<StatusBadge status={batch.status} />
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-muted-foreground text-sm">
|
||||||
{batch.availableGrams}
|
{batch.availableGrams}
|
||||||
{t("grams")}
|
{t("grams")}
|
||||||
</span>
|
</span>
|
||||||
@@ -435,6 +480,7 @@ export default function StockPage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Recall Confirmation Dialog */}
|
{/* Recall Confirmation Dialog */}
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
@@ -447,23 +493,20 @@ export default function StockPage() {
|
|||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
{t("recallConfirm")}
|
{t("recallConfirm")}
|
||||||
{recallTarget && (
|
{recallTarget && (
|
||||||
<span className="mt-2 block font-medium text-foreground">
|
<span className="text-foreground mt-2 block font-medium">
|
||||||
{recallTarget.strainName} ({recallTarget.id}) —{" "}
|
{recallTarget.strainName} ({recallTarget.id}) —{" "}
|
||||||
{recallTarget.availableGrams}
|
{recallTarget.availableGrams}g
|
||||||
{t("grams")}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>
|
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||||
{t("filterAll") && "Abbrechen"}
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
||||||
onClick={handleRecall}
|
onClick={handleRecall}
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
>
|
>
|
||||||
{t("confirmRecall")}
|
{t("recall")}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user