feat: added option to group by albums for missing locations
Signed-off-by: Varun Raj <varun@skcript.com>
This commit is contained in:
1 parent
5b2b779a04
commit
bb6ffeb456
12 files changed
+333
-123
No files matched your search
@@ -20,10 +20,13 @@ import { addDays } from "date-fns";
|
||||
import { useConfig } from "@/contexts/ConfigContext";
|
||||
import LazyGridImage from "@/components/ui/lazy-grid-image";
|
||||
|
||||
export default function MissingLocationAssets() {
|
||||
interface IProps {
|
||||
groupBy: "date" | "album";
|
||||
}
|
||||
export default function MissingLocationAssets({ groupBy }: IProps) {
|
||||
const { exImmichUrl } = useConfig();
|
||||
|
||||
const { startDate, selectedIds, assets, updateContext } =
|
||||
const { startDate, albumId, selectedIds, assets, updateContext } =
|
||||
useMissingLocationContext();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -32,12 +35,13 @@ export default function MissingLocationAssets() {
|
||||
const [index, setIndex] = useState(-1);
|
||||
|
||||
const fetchAssets = async () => {
|
||||
if (!startDate) return;
|
||||
if (!startDate && !albumId) return;
|
||||
setLoading(true);
|
||||
updateContext({
|
||||
assets: [],
|
||||
});
|
||||
return listMissingLocationAssets({ startDate })
|
||||
const filters = groupBy === "date" ? { startDate } : { albumId }
|
||||
return listMissingLocationAssets(filters)
|
||||
.then((assets) => updateContext({ assets }))
|
||||
.catch(setErrorMessage)
|
||||
.finally(() => setLoading(false));
|
||||
@@ -88,8 +92,8 @@ export default function MissingLocationAssets() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (startDate) fetchAssets();
|
||||
}, [startDate]);
|
||||
if (startDate || albumId) fetchAssets();
|
||||
}, [startDate, albumId]);
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
@@ -99,7 +103,7 @@ export default function MissingLocationAssets() {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!startDate)
|
||||
if (!startDate && !albumId)
|
||||
return (
|
||||
<div className="flex flex-col gap-2 h-full justify-center items-center w-full">
|
||||
<CalendarArrowUp />
|
||||
|
||||
@@ -8,24 +8,34 @@ import React, { useMemo } from "react";
|
||||
interface IProps {
|
||||
record: IMissingLocationDatesResponse;
|
||||
onSelect: (date: string) => void;
|
||||
groupBy: "date" | "album";
|
||||
}
|
||||
export default function MissingLocationDateItem({ record, onSelect }: IProps) {
|
||||
export default function MissingLocationDateItem({ record, onSelect, groupBy }: IProps) {
|
||||
const { startDate } = useMissingLocationContext()
|
||||
|
||||
const dateLabel = useMemo(() => {
|
||||
if (!record.date) return "Unknown"
|
||||
return formatDate(parseDate(record.date, "yyyy-MM-dd").toISOString(), "do MMM yyyy")
|
||||
}, [record.date])
|
||||
if (groupBy === "album") {
|
||||
return record.label
|
||||
}
|
||||
if (!record.label) return "Unknown"
|
||||
try {
|
||||
return formatDate(parseDate(record.label, "yyyy-MM-dd").toISOString(), "do MMM yyyy")
|
||||
} catch (e) {
|
||||
return record.label
|
||||
}
|
||||
}, [record.label, groupBy])
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => onSelect(record.date)}
|
||||
key={record.date}
|
||||
onClick={() => onSelect(record.value)}
|
||||
key={record.label}
|
||||
className={
|
||||
cn("flex gap-1 flex-col p-2 py-1 rounded-lg hover:dark:bg-zinc-800 border border-transparent hover:bg-zinc-100 px-4",
|
||||
startDate === record.date ? "bg-zinc-100 dark:bg-zinc-800 border-gray-300 dark:border-zinc-700" : "")
|
||||
cn("flex gap-1 flex-col p-1 rounded-lg hover:dark:bg-zinc-800 border border-transparent hover:bg-zinc-100",
|
||||
startDate === record.label ? "bg-zinc-100 dark:bg-zinc-800 border-gray-300 dark:border-zinc-700" : "")
|
||||
}
|
||||
>
|
||||
<p className="font-mono text-sm">{dateLabel}</p>
|
||||
<p className="text-sm truncate">{dateLabel}</p>
|
||||
<p className="text-xs text-foreground/50">{record.asset_count} Orphan Assets</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { use, useEffect, useState } from "react";
|
||||
import { usePotentialAlbumContext } from "@/contexts/PotentialAlbumContext";
|
||||
import { IMissingLocationDatesResponse, listMissingLocationDates } from "@/handlers/api/asset.handler";
|
||||
import { IMissingLocationDatesResponse, listMissingLocationAlbums, listMissingLocationDates } from "@/handlers/api/asset.handler";
|
||||
import MissingLocationDateItem from "./MissingLocationDateItem";
|
||||
import { useMissingLocationContext } from "@/contexts/MissingLocationContext";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -8,7 +8,11 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrig
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SortAsc, SortDesc } from "lucide-react";
|
||||
|
||||
export default function MissingLocationDates() {
|
||||
interface IMissingLocationDatesProps {
|
||||
groupBy: string;
|
||||
|
||||
}
|
||||
export default function MissingLocationDates({ groupBy }: IMissingLocationDatesProps) {
|
||||
const { updateContext } = useMissingLocationContext();
|
||||
const router = useRouter();
|
||||
const [dateRecords, setDateRecords] = React.useState<
|
||||
@@ -20,7 +24,9 @@ export default function MissingLocationDates() {
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
return listMissingLocationDates({
|
||||
const func = groupBy === "album" ? listMissingLocationAlbums : listMissingLocationDates
|
||||
setDateRecords([])
|
||||
return func({
|
||||
sortBy: filters.sortBy,
|
||||
sortOrder: filters.sortOrder,
|
||||
})
|
||||
@@ -30,23 +36,41 @@ export default function MissingLocationDates() {
|
||||
};
|
||||
|
||||
const handleSelect = (date: string) => {
|
||||
updateContext({ startDate: date });
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { startDate: date },
|
||||
});
|
||||
if (groupBy === "album") {
|
||||
updateContext({ albumId: date });
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
albumId: date,
|
||||
groupBy: "album",
|
||||
startDate: undefined
|
||||
},
|
||||
});
|
||||
} else {
|
||||
updateContext({ startDate: date });
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
startDate: date,
|
||||
groupBy: "date",
|
||||
albumId: undefined
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [filters]);
|
||||
}, [filters, groupBy]);
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto min-w-[200px] py-4 max-h-[calc(100vh-60px)] min-h-[calc(100vh-60px)] dark:bg-zinc-900 bg-gray-200 flex flex-col gap-2 px-2">
|
||||
<div className="min-w-[200px] max-w-[200px] sticky top-0 py-4 max-h-[calc(100vh-60px)] min-h-[calc(100vh-60px)] border-r border-gray-200 dark:border-zinc-800 flex flex-col gap-2 px-2">
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<Select
|
||||
{groupBy === "album" ? (
|
||||
<p className="text-sm text-gray-500 dark:text-zinc-400">Asset Count</p>
|
||||
) : (<Select
|
||||
defaultValue={filters.sortBy}
|
||||
onValueChange={(value) => setFilters({ ...filters, sortBy: value })}
|
||||
>
|
||||
@@ -60,21 +84,27 @@ export default function MissingLocationDates() {
|
||||
<SelectItem value="asset_count">Asset Count</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Select>)}
|
||||
<div>
|
||||
<Button variant="default" size="sm" onClick={() => setFilters({ ...filters, sortOrder: filters.sortOrder === "asc" ? "desc" : "asc" })}>
|
||||
<Button variant="ghost" size="sm" onClick={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
sortOrder: filters.sortOrder === "asc" ? "desc" : "asc"
|
||||
})}>
|
||||
{filters.sortOrder === "asc" ? <SortAsc size={16} /> : <SortDesc size={16} />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{dateRecords.map((record) => (
|
||||
<MissingLocationDateItem
|
||||
key={record.date}
|
||||
record={record}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
))}
|
||||
<div className="overflow-y-auto flex flex-col gap-2">
|
||||
{dateRecords.map((record) => (
|
||||
<MissingLocationDateItem
|
||||
key={record.value}
|
||||
record={record}
|
||||
groupBy={groupBy as "date" | "album"}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -30,14 +30,14 @@ export default function TagMissingLocationDialog({
|
||||
const [mapPosition,setMapPosition] = useState<IPlace>({
|
||||
latitude: 48.0,
|
||||
longitude: 16.0,
|
||||
name: "home1"
|
||||
name: "home"
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Tag Missing Location</Button>
|
||||
<Button size={"sm"}>Tag Missing Location</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="!w-[600px] !max-w-[600px]">
|
||||
<DialogHeader>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import React from 'react'
|
||||
|
||||
interface FloatingBarProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function FloatingBar({ children }: FloatingBarProps) {
|
||||
return (
|
||||
<div className='absolute bottom-10 left-0 right-0'>
|
||||
<div className='max-w-lg mx-auto py-2 px-2 rounded-lg flex justify-between items-center bg-white dark:bg-zinc-900 border-t border-zinc-200 dark:border-zinc-800'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -29,6 +29,7 @@ export const SHARE_ALBUMS_PATH = BASE_API_ENDPOINT + "/albums/share";
|
||||
|
||||
// Assets
|
||||
export const LIST_MISSING_LOCATION_DATES_PATH = BASE_API_ENDPOINT + "/assets/missing-location-dates";
|
||||
export const LIST_MISSING_LOCATION_ALBUMS_PATH = BASE_API_ENDPOINT + "/assets/missing-location-albums";
|
||||
export const LIST_MISSING_LOCATION_ASSETS_PATH = BASE_API_ENDPOINT + "/assets/missing-location-assets";
|
||||
export const UPDATE_ASSETS_PATH = BASE_PROXY_ENDPOINT + "/assets";
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createContext, useContext } from "react";
|
||||
|
||||
export interface IMissingLocationConfig {
|
||||
startDate?: string;
|
||||
albumId?: string;
|
||||
selectedIds: string[];
|
||||
assets: IAsset[];
|
||||
}
|
||||
@@ -12,6 +13,7 @@ export interface MissingLocationContext extends IMissingLocationConfig {
|
||||
}
|
||||
const MissingLocationContext = createContext<MissingLocationContext>({
|
||||
startDate: undefined,
|
||||
albumId: undefined,
|
||||
selectedIds: [],
|
||||
assets: [],
|
||||
updateContext: () => { },
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ASSET_GEO_HEATMAP_PATH,
|
||||
FIND_ASSETS,
|
||||
LIST_ALBUMS_PATH,
|
||||
LIST_MISSING_LOCATION_ALBUMS_PATH,
|
||||
LIST_MISSING_LOCATION_ASSETS_PATH,
|
||||
LIST_MISSING_LOCATION_DATES_PATH,
|
||||
UPDATE_ASSETS_PATH,
|
||||
@@ -18,16 +19,25 @@ interface IMissingAssetAlbumsFilters {
|
||||
sortOrder?: string;
|
||||
}
|
||||
export interface IMissingLocationDatesResponse {
|
||||
date: string;
|
||||
label: string;
|
||||
asset_count: number;
|
||||
value: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
|
||||
export const listMissingLocationDates = async (
|
||||
filters: IMissingAssetAlbumsFilters
|
||||
): Promise<IMissingLocationDatesResponse[]> => {
|
||||
return API.get(LIST_MISSING_LOCATION_DATES_PATH, filters);
|
||||
};
|
||||
|
||||
export const listMissingLocationAlbums = async (
|
||||
filters: IMissingAssetAlbumsFilters
|
||||
): Promise<IMissingLocationDatesResponse[]> => {
|
||||
return API.get(LIST_MISSING_LOCATION_ALBUMS_PATH, filters);
|
||||
};
|
||||
|
||||
export const listMissingLocationAssets = async (
|
||||
filters: IMissingAssetAlbumsFilters
|
||||
): Promise<IAsset[]> => {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { db } from "@/config/db";
|
||||
import { IMissingLocationDatesResponse } from "@/handlers/api/asset.handler";
|
||||
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
|
||||
import { parseDate } from "@/helpers/date.helper";
|
||||
import { assets, exif } from "@/schema";
|
||||
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
|
||||
import { albums } from "@/schema/albums.schema";
|
||||
import { and, count, desc, eq, isNotNull, isNull, ne, sql } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
try {
|
||||
const { sortBy = "date", sortOrder = "desc" } = req.query;
|
||||
const currentUser = await getCurrentUser(req);
|
||||
if (!currentUser) {
|
||||
return res.status(401).json({
|
||||
error: "Unauthorized",
|
||||
});
|
||||
}
|
||||
const rows = await db
|
||||
.select({
|
||||
asset_count: desc(count(assets.id)),
|
||||
label: albums.albumName,
|
||||
value: albums.id,
|
||||
})
|
||||
.from(assets)
|
||||
.leftJoin(exif, eq(exif.assetId, assets.id))
|
||||
.leftJoin(albumsAssetsAssets, eq(albumsAssetsAssets.assetsId, assets.id))
|
||||
.leftJoin(albums, eq(albums.id, albumsAssetsAssets.albumsId))
|
||||
.where(
|
||||
and(
|
||||
isNull(exif.latitude),
|
||||
isNotNull(assets.createdAt),
|
||||
isNotNull(exif.dateTimeOriginal),
|
||||
eq(assets.ownerId, currentUser.id),
|
||||
eq(assets.isVisible, true),
|
||||
isNotNull(albums.id)
|
||||
))
|
||||
.groupBy(albums.id)
|
||||
.orderBy(desc(count(assets.id))) as IMissingLocationDatesResponse[];
|
||||
|
||||
|
||||
rows.sort((a, b) => {
|
||||
return sortOrder === "asc" ? a.asset_count - b.asset_count : b.asset_count - a.asset_count;
|
||||
});
|
||||
return res.status(200).json(rows);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: error?.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,62 +3,116 @@ import { db } from "@/config/db";
|
||||
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
|
||||
import { parseDate } from "@/helpers/date.helper";
|
||||
import { assets, exif } from "@/schema";
|
||||
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
|
||||
import { albums } from "@/schema/albums.schema";
|
||||
import { IUser } from "@/types/user";
|
||||
import { addDays } from "date-fns";
|
||||
import { and, eq, gte, isNotNull, isNull, lte, ne, sql } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
const getRowsByDates = async (startDateDate: Date, endDateDate: Date, currentUser: IUser) => {
|
||||
return db
|
||||
.select({
|
||||
id: assets.id,
|
||||
deviceId: assets.deviceId,
|
||||
type: assets.type,
|
||||
originalPath: assets.originalPath,
|
||||
isFavorite: assets.isFavorite,
|
||||
duration: assets.duration,
|
||||
encodedVideoPath: assets.encodedVideoPath,
|
||||
originalFileName: assets.originalFileName,
|
||||
sidecarPath: assets.sidecarPath,
|
||||
deletedAt: assets.deletedAt,
|
||||
localDateTime: assets.localDateTime,
|
||||
exifImageWidth: exif.exifImageWidth,
|
||||
exifImageHeight: exif.exifImageHeight,
|
||||
ownerId: assets.ownerId,
|
||||
dateTimeOriginal: exif.dateTimeOriginal,
|
||||
})
|
||||
.from(assets)
|
||||
.leftJoin(exif, eq(exif.assetId, assets.id))
|
||||
.where(and(
|
||||
isNull(exif.latitude),
|
||||
isNotNull(assets.createdAt),
|
||||
gte(exif.dateTimeOriginal, startDateDate),
|
||||
lte(exif.dateTimeOriginal, endDateDate),
|
||||
eq(assets.ownerId, currentUser.id),
|
||||
));
|
||||
}
|
||||
|
||||
const getRowsByAlbums = async (currentUser: IUser, albumId: string) => {
|
||||
return db.select({
|
||||
id: assets.id,
|
||||
deviceId: assets.deviceId,
|
||||
type: assets.type,
|
||||
originalPath: assets.originalPath,
|
||||
isFavorite: assets.isFavorite,
|
||||
duration: assets.duration,
|
||||
encodedVideoPath: assets.encodedVideoPath,
|
||||
originalFileName: assets.originalFileName,
|
||||
sidecarPath: assets.sidecarPath,
|
||||
deletedAt: assets.deletedAt,
|
||||
localDateTime: assets.localDateTime,
|
||||
exifImageWidth: exif.exifImageWidth,
|
||||
exifImageHeight: exif.exifImageHeight,
|
||||
ownerId: assets.ownerId,
|
||||
dateTimeOriginal: exif.dateTimeOriginal,
|
||||
})
|
||||
.from(assets)
|
||||
.leftJoin(exif, eq(exif.assetId, assets.id))
|
||||
.leftJoin(albumsAssetsAssets, eq(albumsAssetsAssets.assetsId, assets.id))
|
||||
.leftJoin(albums, eq(albums.id, albumsAssetsAssets.albumsId))
|
||||
.where(and(
|
||||
isNull(exif.latitude),
|
||||
isNotNull(assets.createdAt),
|
||||
eq(assets.ownerId, currentUser.id),
|
||||
eq(albums.id, albumId)
|
||||
));
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const { startDate } = req.query as {
|
||||
const { startDate, groupBy, albumId } = req.query as {
|
||||
startDate: string;
|
||||
groupBy: "date" | "album";
|
||||
albumId?: string;
|
||||
};
|
||||
|
||||
const currentUser = await getCurrentUser(req);
|
||||
|
||||
if (!startDate) {
|
||||
if (!currentUser) {
|
||||
return res.status(401).json({
|
||||
error: "Unauthorized",
|
||||
});
|
||||
}
|
||||
|
||||
if (!startDate && !albumId) {
|
||||
return res.status(400).json({
|
||||
error: "startDate and endDate are required",
|
||||
error: "startDate or albumId is required",
|
||||
});
|
||||
}
|
||||
|
||||
const startDateDate = parseDate(startDate, "yyyy-MM-dd");
|
||||
const endDateDate = addDays(parseDate(startDate, "yyyy-MM-dd"), 1);
|
||||
|
||||
try {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: assets.id,
|
||||
deviceId: assets.deviceId,
|
||||
type: assets.type,
|
||||
originalPath: assets.originalPath,
|
||||
isFavorite: assets.isFavorite,
|
||||
duration: assets.duration,
|
||||
encodedVideoPath: assets.encodedVideoPath,
|
||||
originalFileName: assets.originalFileName,
|
||||
sidecarPath: assets.sidecarPath,
|
||||
deletedAt: assets.deletedAt,
|
||||
localDateTime: assets.localDateTime,
|
||||
exifImageWidth: exif.exifImageWidth,
|
||||
exifImageHeight: exif.exifImageHeight,
|
||||
ownerId: assets.ownerId,
|
||||
dateTimeOriginal: exif.dateTimeOriginal,
|
||||
})
|
||||
.from(assets)
|
||||
.leftJoin(exif, eq(exif.assetId, assets.id))
|
||||
.where(and(
|
||||
isNull(exif.latitude),
|
||||
isNotNull(assets.createdAt),
|
||||
gte(exif.dateTimeOriginal, startDateDate),
|
||||
lte(exif.dateTimeOriginal, endDateDate),
|
||||
eq(assets.ownerId, currentUser.id),
|
||||
));
|
||||
if (startDate) {
|
||||
const startDateDate = parseDate(startDate, "yyyy-MM-dd");
|
||||
const endDateDate = addDays(parseDate(startDate, "yyyy-MM-dd"), 1);
|
||||
|
||||
try {
|
||||
const rows = await getRowsByDates(startDateDate, endDateDate, currentUser);
|
||||
return res.status(200).json(rows);
|
||||
} catch (error: any) {
|
||||
return res.status(500).json({
|
||||
error: error?.message,
|
||||
});
|
||||
}
|
||||
} else if (albumId) {
|
||||
if (!albumId) {
|
||||
return res.status(400).json({
|
||||
error: "albumId is required",
|
||||
});
|
||||
}
|
||||
const rows = await getRowsByAlbums(currentUser, albumId);
|
||||
return res.status(200).json(rows);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: error?.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,8 @@ export default async function handler(
|
||||
const rows = await db
|
||||
.select({
|
||||
asset_count: desc(count(assets.id)),
|
||||
date: sql`DATE(${exif.dateTimeOriginal})`,
|
||||
label: sql`DATE(${exif.dateTimeOriginal})`,
|
||||
value: sql`DATE(${exif.dateTimeOriginal})`,
|
||||
})
|
||||
.from(assets)
|
||||
.leftJoin(exif, eq(exif.assetId, assets.id))
|
||||
@@ -39,8 +40,8 @@ export default async function handler(
|
||||
|
||||
if (sortBy === "date") {
|
||||
rows.sort((a, b) => {
|
||||
const aDate = parseDate(a.date, "yyyy-MM-dd");
|
||||
const bDate = parseDate(b.date, "yyyy-MM-dd");
|
||||
const aDate = parseDate(a.label, "yyyy-MM-dd");
|
||||
const bDate = parseDate(b.label, "yyyy-MM-dd");
|
||||
return sortOrder === "asc" ? aDate.getTime() - bDate.getTime() : bDate.getTime() - aDate.getTime();
|
||||
});
|
||||
} else if (sortBy === "asset_count") {
|
||||
|
||||
@@ -15,19 +15,19 @@ import { updateAssets } from "@/handlers/api/asset.handler";
|
||||
import { IPlace } from "@/types/common";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useMemo } from "react";
|
||||
import FloatingBar from "@/components/shared/FloatingBar";
|
||||
import { Select, SelectValue, SelectTrigger, SelectContent, SelectItem } from "@/components/ui/select";
|
||||
|
||||
export default function MissingLocations() {
|
||||
const { toast } = useToast();
|
||||
|
||||
const { query } = useRouter();
|
||||
const { startDate } = query as { startDate: string };
|
||||
const { query, push } = useRouter();
|
||||
const { startDate, groupBy = "date" } = query as { startDate: string, groupBy: string };
|
||||
const [config, setConfig] = React.useState<IMissingLocationConfig>({
|
||||
startDate: startDate || undefined,
|
||||
selectedIds: [],
|
||||
assets: [],
|
||||
});
|
||||
|
||||
const selectedAssets = useMemo(() => config.assets.filter((a) => config.selectedIds.includes(a.id)), [config.assets, config.selectedIds]) ;
|
||||
const selectedAssets = useMemo(() => config.assets.filter((a) => config.selectedIds.includes(a.id)), [config.assets, config.selectedIds]);
|
||||
|
||||
const handleSubmit = (place: IPlace) => {
|
||||
return updateAssets({
|
||||
@@ -43,42 +43,32 @@ export default function MissingLocations() {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout className="!p-0 !mb-0">
|
||||
<PageLayout className="!p-0 !mb-0 relative">
|
||||
<Header
|
||||
leftComponent="Missing Location"
|
||||
rightComponent={
|
||||
<>
|
||||
<Badge variant={"outline"}>
|
||||
{config.selectedIds.length} Selected
|
||||
</Badge>
|
||||
{config.selectedIds.length === config.assets.length ? (
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() =>
|
||||
setConfig({
|
||||
...config,
|
||||
selectedIds: [],
|
||||
})
|
||||
}
|
||||
>
|
||||
Unselect all
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() =>
|
||||
setConfig({
|
||||
...config,
|
||||
selectedIds: config.assets.map((a) => a.id),
|
||||
})
|
||||
}
|
||||
>
|
||||
Select all
|
||||
</Button>
|
||||
)}
|
||||
<TagMissingLocationDialog onSubmit={handleSubmit} />
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={groupBy} onValueChange={(value) => {
|
||||
push({
|
||||
pathname: "/assets/missing-locations",
|
||||
query: {
|
||||
...query,
|
||||
groupBy: value,
|
||||
startDate: undefined,
|
||||
albumId: undefined,
|
||||
},
|
||||
});
|
||||
}}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Group by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="album">Album</SelectItem>
|
||||
<SelectItem value="date">Date</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<AssetsOptions assets={selectedAssets} onAdd={() => { }} />
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<MissingLocationContext.Provider
|
||||
@@ -89,9 +79,46 @@ export default function MissingLocations() {
|
||||
}}
|
||||
>
|
||||
<div className="flex divide-y">
|
||||
<MissingLocationDates />
|
||||
<MissingLocationAssets />
|
||||
<MissingLocationDates groupBy={groupBy as "date" | "album"} />
|
||||
<MissingLocationAssets groupBy={groupBy as "date" | "album"} />
|
||||
</div>
|
||||
<FloatingBar>
|
||||
<div className="flex items-center gap-2 justify-between w-full">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{config.selectedIds.length} Selected
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{config.selectedIds.length === config.assets.length ? (
|
||||
<Button
|
||||
variant={"outline"}
|
||||
size={"sm"}
|
||||
onClick={() =>
|
||||
setConfig({
|
||||
...config,
|
||||
selectedIds: [],
|
||||
})
|
||||
}
|
||||
>
|
||||
Unselect all
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant={"outline"}
|
||||
size={"sm"}
|
||||
onClick={() =>
|
||||
setConfig({
|
||||
...config,
|
||||
selectedIds: config.assets.map((a) => a.id),
|
||||
})
|
||||
}
|
||||
>
|
||||
Select all
|
||||
</Button>
|
||||
)}
|
||||
<TagMissingLocationDialog onSubmit={handleSubmit} />
|
||||
</div>
|
||||
</div>
|
||||
</FloatingBar>
|
||||
</MissingLocationContext.Provider>
|
||||
</PageLayout>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user