Add functionality to manage visit photos and albums for protocols

Introduce new database tables for photo albums and photos, add API endpoints for CRUD operations on albums and photos, and implement frontend components for managing photo albums.
This commit is contained in:
Riyadh
2026-07-07 09:14:07 +00:00
parent 05586fc997
commit 224e69f111
6 changed files with 857 additions and 1 deletions
+75
View File
@@ -241,6 +241,67 @@ export const protocolGiftIssuesTable = pgTable(
}),
);
// ---------------------------------------------------------------------------
// Visit photo albums (صور الزيارات) — "تصوير" tab.
//
// An album groups photographs of a visit. It may optionally be linked to an
// external protocol meeting (اللقاءات الخارجية); the link is `set null` so
// deleting the meeting never removes the album. The album cover is derived at
// query time from the first photo (by sortOrder), so there is no circular
// album <-> photo foreign key to keep in sync.
// ---------------------------------------------------------------------------
export const protocolPhotoAlbumsTable = pgTable(
"protocol_photo_albums",
{
id: serial("id").primaryKey(),
name: varchar("name", { length: 300 }).notNull(),
externalMeetingId: integer("external_meeting_id").references(
() => protocolExternalMeetingsTable.id,
{ onDelete: "set null" },
),
createdBy: integer("created_by").references(() => usersTable.id, {
onDelete: "set null",
}),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
},
(t) => ({
meetingIdx: index("protocol_albums_meeting_idx").on(t.externalMeetingId),
}),
);
// ---------------------------------------------------------------------------
// Photos inside an album. Each photo references a private object-storage path
// (`/objects/<id>`) served through the authorized object endpoint. Deleting an
// album cascades to its photos.
// ---------------------------------------------------------------------------
export const protocolPhotosTable = pgTable(
"protocol_photos",
{
id: serial("id").primaryKey(),
albumId: integer("album_id")
.notNull()
.references(() => protocolPhotoAlbumsTable.id, { onDelete: "cascade" }),
objectPath: varchar("object_path", { length: 500 }).notNull(),
sortOrder: integer("sort_order").notNull().default(0),
uploadedBy: integer("uploaded_by").references(() => usersTable.id, {
onDelete: "set null",
}),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
albumIdx: index("protocol_photos_album_idx").on(t.albumId),
pathIdx: index("protocol_photos_path_idx").on(t.objectPath),
}),
);
// ---------------------------------------------------------------------------
// Audit log (scoped to the protocol module only)
// ---------------------------------------------------------------------------
@@ -306,4 +367,18 @@ export type InsertProtocolGiftIssue = z.infer<
>;
export type ProtocolGiftIssue = typeof protocolGiftIssuesTable.$inferSelect;
export const insertProtocolPhotoAlbumSchema = createInsertSchema(
protocolPhotoAlbumsTable,
).omit({ id: true, createdAt: true, updatedAt: true });
export type InsertProtocolPhotoAlbum = z.infer<
typeof insertProtocolPhotoAlbumSchema
>;
export type ProtocolPhotoAlbum = typeof protocolPhotoAlbumsTable.$inferSelect;
export const insertProtocolPhotoSchema = createInsertSchema(
protocolPhotosTable,
).omit({ id: true, createdAt: true });
export type InsertProtocolPhoto = z.infer<typeof insertProtocolPhotoSchema>;
export type ProtocolPhoto = typeof protocolPhotosTable.$inferSelect;
export type ProtocolAuditLog = typeof protocolAuditLogsTable.$inferSelect;