This commit is contained in:
2026-05-16 18:57:42 +07:00
commit 01c00093d5
128 changed files with 23193 additions and 0 deletions
View File
+34
View File
@@ -0,0 +1,34 @@
import { relations } from "drizzle-orm";
import { users, placesOfWorship, recipients, distributions } from "./schema";
export const usersRelations = relations(users, ({ many }) => ({
managedPlaces: many(placesOfWorship),
}));
export const placesOfWorshipRelations = relations(placesOfWorship, ({ one, many }) => ({
manager: one(users, {
fields: [placesOfWorship.managerId],
references: [users.id],
}),
recipients: many(recipients),
distributions: many(distributions),
}));
export const recipientsRelations = relations(recipients, ({ one, many }) => ({
placeOfWorship: one(placesOfWorship, {
fields: [recipients.placeOfWorshipId],
references: [placesOfWorship.id],
}),
distributions: many(distributions),
}));
export const distributionsRelations = relations(distributions, ({ one }) => ({
placeOfWorship: one(placesOfWorship, {
fields: [distributions.placeOfWorshipId],
references: [placesOfWorship.id],
}),
recipient: one(recipients, {
fields: [distributions.recipientId],
references: [recipients.id],
}),
}));
+138
View File
@@ -0,0 +1,138 @@
import {
mysqlTable,
mysqlEnum,
serial,
varchar,
text,
timestamp,
bigint,
int,
decimal,
index,
} from "drizzle-orm/mysql-core";
// ── Users (base auth table) ──────────────────────────────────
export const users = mysqlTable("users", {
id: serial("id").primaryKey(),
unionId: varchar("unionId", { length: 255 }).notNull().unique(),
name: varchar("name", { length: 255 }),
email: varchar("email", { length: 320 }),
passwordHash: text("password_hash"),
avatar: text("avatar"),
role: mysqlEnum("role", ["admin", "officer", "manager"]).default("manager").notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().notNull().$onUpdate(() => new Date()),
lastSignInAt: timestamp("lastSignInAt").defaultNow().notNull(),
});
export type User = typeof users.$inferSelect;
export type InsertUser = typeof users.$inferInsert;
// ── Places of Worship ─────────────────────────────────────────
export const placesOfWorship = mysqlTable(
"places_of_worship",
{
id: serial("id").primaryKey(),
name: varchar("name", { length: 255 }).notNull(),
type: mysqlEnum("type", ["mosque", "church", "temple", "vihara", "other"]).notNull(),
address: text("address"),
latitude: decimal("latitude", { precision: 10, scale: 8 }).notNull(),
longitude: decimal("longitude", { precision: 11, scale: 8 }).notNull(),
radius: int("radius").default(1000).notNull(), // radius in meters
capacity: int("capacity").default(100).notNull(), // max beneficiaries
contactName: varchar("contact_name", { length: 255 }),
contactPhone: varchar("contact_phone", { length: 20 }),
managerId: bigint("manager_id", { mode: "number", unsigned: true }),
isActive: mysqlEnum("is_active", ["yes", "no"]).default("yes").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull().$onUpdate(() => new Date()),
},
(table) => ({
typeIdx: index("pow_type_idx").on(table.type),
managerIdx: index("pow_manager_idx").on(table.managerId),
})
);
export type PlaceOfWorship = typeof placesOfWorship.$inferSelect;
export type InsertPlaceOfWorship = typeof placesOfWorship.$inferInsert;
// ── Recipients (beneficiaries) ─────────────────────────────────
export const recipients = mysqlTable(
"recipients",
{
id: serial("id").primaryKey(),
nik: varchar("nik", { length: 16 }).notNull().unique(),
name: varchar("name", { length: 255 }).notNull(),
birthDate: varchar("birth_date", { length: 10 }),
gender: mysqlEnum("gender", ["male", "female"]),
address: text("address"),
phone: varchar("phone", { length: 15 }),
latitude: decimal("latitude", { precision: 10, scale: 8 }),
longitude: decimal("longitude", { precision: 11, scale: 8 }),
familyMembers: int("family_members").default(1),
incomePerMonth: int("income_per_month").default(0),
status: mysqlEnum("status", ["pending", "active", "rejected", "suspended"]).default("pending").notNull(),
rejectionReason: text("rejection_reason"),
placeOfWorshipId: bigint("place_of_worship_id", { mode: "number", unsigned: true }).notNull(),
registeredBy: bigint("registered_by", { mode: "number", unsigned: true }),
verifiedBy: bigint("verified_by", { mode: "number", unsigned: true }),
verifiedAt: timestamp("verified_at"),
notes: text("notes"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull().$onUpdate(() => new Date()),
},
(table) => ({
nikIdx: index("rec_nik_idx").on(table.nik),
statusIdx: index("rec_status_idx").on(table.status),
powIdx: index("rec_pow_idx").on(table.placeOfWorshipId),
})
);
export type Recipient = typeof recipients.$inferSelect;
export type InsertRecipient = typeof recipients.$inferInsert;
// ── Distributions ──────────────────────────────────────────────
export const distributions = mysqlTable(
"distributions",
{
id: serial("id").primaryKey(),
placeOfWorshipId: bigint("place_of_worship_id", { mode: "number", unsigned: true }).notNull(),
recipientId: bigint("recipient_id", { mode: "number", unsigned: true }).notNull(),
distributedBy: bigint("distributed_by", { mode: "number", unsigned: true }),
aidType: varchar("aid_type", { length: 100 }).notNull(),
amount: decimal("amount", { precision: 12, scale: 2 }).default("0.00"),
quantity: int("quantity").default(1),
unit: varchar("unit", { length: 50 }).default("package"),
distributionDate: timestamp("distribution_date").defaultNow().notNull(),
notes: text("notes"),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => ({
powIdx: index("dist_pow_idx").on(table.placeOfWorshipId),
recIdx: index("dist_rec_idx").on(table.recipientId),
})
);
export type Distribution = typeof distributions.$inferSelect;
export type InsertDistribution = typeof distributions.$inferInsert;
// ── Activity Logs ──────────────────────────────────────────────
export const activityLogs = mysqlTable(
"activity_logs",
{
id: serial("id").primaryKey(),
userId: bigint("user_id", { mode: "number", unsigned: true }),
action: varchar("action", { length: 50 }).notNull(), // e.g. "CREATE", "UPDATE", "DELETE", "VERIFY"
entityType: varchar("entity_type", { length: 50 }).notNull(), // "recipient", "place_of_worship", "distribution"
entityId: bigint("entity_id", { mode: "number", unsigned: true }),
details: text("details"),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => ({
userIdx: index("act_user_idx").on(table.userId),
entityIdx: index("act_entity_idx").on(table.entityType, table.entityId),
})
);
export type ActivityLog = typeof activityLogs.$inferSelect;
export type InsertActivityLog = typeof activityLogs.$inferInsert;
+341
View File
@@ -0,0 +1,341 @@
import { getDb } from "../api/queries/connection";
import { placesOfWorship, recipients, distributions, activityLogs } from "./schema";
import { seedLocalUserIfMissing } from "../api/auth";
async function seed() {
const db = getDb();
await seedLocalUserIfMissing();
console.log("Seeding database...");
// Seed Places of Worship
const places = await db.insert(placesOfWorship).values([
{
name: "Masjid Al-Ikhlas",
type: "mosque" as const,
address: "Jl. Pahlawan No. 12, Kelurahan Cempaka, Kecamatan Cempaka Putih",
latitude: "-6.1754",
longitude: "106.8650",
radius: 1500,
capacity: 200,
contactName: "Bapak Ahmad Surya",
contactPhone: "081234567890",
isActive: "yes",
},
{
name: "Gereja Kristen Jawa",
type: "church" as const,
address: "Jl. Diponegoro No. 45, Kelurahan Menteng, Kecamatan Menteng",
latitude: "-6.2008",
longitude: "106.8320",
radius: 1200,
capacity: 150,
contactName: "Ibu Maria Tan",
contactPhone: "081987654321",
isActive: "yes",
},
{
name: "Pura Agung Jagatnatha",
type: "temple" as const,
address: "Jl. Nusantara No. 78, Kelurahan Denpasar Timur",
latitude: "-8.6580",
longitude: "115.2200",
radius: 2000,
capacity: 100,
contactName: "I Made Wirata",
contactPhone: "082233445566",
isActive: "yes",
},
{
name: "Vihara Dharma Bhakti",
type: "vihara" as const,
address: "Jl. Gajah Mada No. 88, Kelurahan Glodok, Jakarta Barat",
latitude: "-6.1477",
longitude: "106.8133",
radius: 1000,
capacity: 80,
contactName: "Bapak Lim Swie Hian",
contactPhone: "083344556677",
isActive: "yes",
},
{
name: "Masjid Nurul Huda",
type: "mosque" as const,
address: "Jl. Sudirman No. 25, Kelurahan Kuningan, Jakarta Selatan",
latitude: "-6.2088",
longitude: "106.8456",
radius: 1800,
capacity: 300,
contactName: "Bapak Hadi Pranoto",
contactPhone: "084455667788",
isActive: "yes",
},
{
name: "Gereja Katolik Santo Antonius",
type: "church" as const,
address: "Jl. Kartini No. 15, Kelurahan Yogyakarta Kota",
latitude: "-7.8014",
longitude: "110.3648",
radius: 1300,
capacity: 120,
contactName: "Romo Yulianto",
contactPhone: "085566778899",
isActive: "yes",
},
]).$returningId();
console.log(`Seeded ${places.length} places of worship`);
// Seed Recipients
const recs = await db.insert(recipients).values([
{
nik: "3175012345678901",
name: "Budi Santoso",
birthDate: "1975-05-15",
gender: "male" as const,
address: "Jl. Melati No. 3, RT 02/RW 05, Cempaka Putih",
phone: "081111111111",
latitude: "-6.1760",
longitude: "106.8645",
familyMembers: 4,
incomePerMonth: 800000,
status: "active" as const,
placeOfWorshipId: 1,
notes: "Pekerja serabutan, istri tidak bekerja",
},
{
nik: "3175012345678902",
name: "Siti Aminah",
birthDate: "1980-12-20",
gender: "female" as const,
address: "Jl. Anggrek No. 7, RT 03/RW 02, Cempaka Putih",
phone: "081222222222",
latitude: "-6.1748",
longitude: "106.8660",
familyMembers: 3,
incomePerMonth: 600000,
status: "active" as const,
placeOfWorshipId: 1,
notes: "Janda dengan 2 anak sekolah",
},
{
nik: "3175012345678903",
name: "Ahmad Fauzi",
birthDate: "1968-03-10",
gender: "male" as const,
address: "Jl. Mawar No. 12, RT 01/RW 08, Menteng",
phone: "081333333333",
latitude: "-6.2012",
longitude: "106.8315",
familyMembers: 5,
incomePerMonth: 1000000,
status: "active" as const,
placeOfWorshipId: 2,
notes: "Penjual nasi keliling",
},
{
nik: "3175012345678904",
name: "Dewi Kusuma",
birthDate: "1990-08-25",
gender: "female" as const,
address: "Jl. Cemara No. 9, RT 04/RW 01, Menteng",
phone: "081444444444",
latitude: "-6.2002",
longitude: "106.8330",
familyMembers: 2,
incomePerMonth: 700000,
status: "pending" as const,
placeOfWorshipId: 2,
notes: "Pekerja cleaning service",
},
{
nik: "5175012345678905",
name: "I Wayan Suardana",
birthDate: "1972-11-05",
gender: "male" as const,
address: "Jl. Raya Sanur No. 21, Denpasar Timur",
phone: "082555555555",
latitude: "-8.6575",
longitude: "115.2195",
familyMembers: 6,
incomePerMonth: 1200000,
status: "active" as const,
placeOfWorshipId: 3,
notes: "Petani subsisten",
},
{
nik: "3175012345678906",
name: "Rini Sulastri",
birthDate: "1985-04-18",
gender: "female" as const,
address: "Jl. Pekojan No. 14, RT 02/RW 06, Glodok",
phone: "083666666666",
latitude: "-6.1472",
longitude: "106.8140",
familyMembers: 3,
incomePerMonth: 500000,
status: "pending" as const,
placeOfWorshipId: 4,
notes: "Penjual kue tradisional",
},
{
nik: "3175012345678907",
name: "Hendra Wijaya",
birthDate: "1978-07-30",
gender: "male" as const,
address: "Jl. Karet No. 8, RT 03/RW 04, Kuningan",
phone: "084777777777",
latitude: "-6.2095",
longitude: "106.8450",
familyMembers: 4,
incomePerMonth: 900000,
status: "active" as const,
placeOfWorshipId: 5,
notes: "Sopir angkot",
},
{
nik: "3475012345678908",
name: "Yuliani",
birthDate: "1982-09-12",
gender: "female" as const,
address: "Jl. Malioboro No. 33, RT 01/RW 02, Yogyakarta",
phone: "085888888888",
latitude: "-7.8020",
longitude: "110.3655",
familyMembers: 2,
incomePerMonth: 650000,
status: "active" as const,
placeOfWorshipId: 6,
notes: "Penjual oleh-oleh",
},
{
nik: "3175012345678909",
name: "Agus Priyanto",
birthDate: "1965-01-22",
gender: "male" as const,
address: "Jl. Kenanga No. 5, RT 02/RW 07, Cempaka Putih",
phone: "086999999999",
latitude: "-6.1750",
longitude: "106.8655",
familyMembers: 3,
incomePerMonth: 750000,
status: "rejected" as const,
placeOfWorshipId: 1,
rejectionReason: "Memiliki usaha warung yang cukup stabil",
notes: "Pemilik warung kecil",
},
{
nik: "3175012345678910",
name: "Maya Indah",
birthDate: "1992-06-08",
gender: "female" as const,
address: "Jl. Flamboyan No. 11, RT 05/RW 03, Kuningan",
phone: "087000000001",
latitude: "-6.2078",
longitude: "106.8465",
familyMembers: 1,
incomePerMonth: 850000,
status: "pending" as const,
placeOfWorshipId: 5,
notes: "Mahasiswa yang tinggal sendiri",
},
]).$returningId();
console.log(`Seeded ${recs.length} recipients`);
// Seed Distributions
const dists = await db.insert(distributions).values([
{
placeOfWorshipId: 1,
recipientId: 1,
aidType: "Sembako",
amount: "350000",
quantity: 2,
unit: "paket",
notes: "Paket beras, minyak goreng, gula, teh",
},
{
placeOfWorshipId: 1,
recipientId: 2,
aidType: "Sembako",
amount: "350000",
quantity: 1,
unit: "paket",
notes: "Paket beras, minyak goreng, gula",
},
{
placeOfWorshipId: 2,
recipientId: 3,
aidType: "Bantuan Tunai",
amount: "500000",
quantity: 1,
unit: "kali",
notes: "Bantuan langsung tunai",
},
{
placeOfWorshipId: 3,
recipientId: 5,
aidType: "Sembako",
amount: "400000",
quantity: 2,
unit: "paket",
notes: "Paket besar untuk keluarga 6 orang",
},
{
placeOfWorshipId: 4,
recipientId: 6,
aidType: "Modal Usaha",
amount: "1000000",
quantity: 1,
unit: "kali",
notes: "Modal untuk pengembangan usaha kue",
},
{
placeOfWorshipId: 5,
recipientId: 7,
aidType: "Sembako",
amount: "350000",
quantity: 1,
unit: "paket",
notes: "Paket standar",
},
{
placeOfWorshipId: 6,
recipientId: 8,
aidType: "Bantuan Pendidikan",
amount: "750000",
quantity: 1,
unit: "kali",
notes: "Bantuan biaya sekolah anak",
},
{
placeOfWorshipId: 1,
recipientId: 1,
aidType: "Bantuan Tunai",
amount: "600000",
quantity: 1,
unit: "kali",
notes: "Bantuan bulanan kedua",
},
]).$returningId();
console.log(`Seeded ${dists.length} distributions`);
// Seed Activity Logs
await db.insert(activityLogs).values([
{ action: "CREATE", entityType: "recipient", entityId: 1, details: "Mendaftarkan Budi Santoso" },
{ action: "CREATE", entityType: "recipient", entityId: 2, details: "Mendaftarkan Siti Aminah" },
{ action: "VERIFY", entityType: "recipient", entityId: 1, details: "Memverifikasi Budi Santoso - Diterima" },
{ action: "VERIFY", entityType: "recipient", entityId: 2, details: "Memverifikasi Siti Aminah - Diterima" },
{ action: "CREATE", entityType: "distribution", entityId: 1, details: "Distribusi sembako ke Budi Santoso" },
{ action: "CREATE", entityType: "recipient", entityId: 4, details: "Mendaftarkan Dewi Kusuma" },
{ action: "CREATE", entityType: "recipient", entityId: 9, details: "Mendaftarkan Agus Priyanto" },
{ action: "VERIFY", entityType: "recipient", entityId: 9, details: "Memverifikasi Agus Priyanto - Ditolak" },
{ action: "CREATE", entityType: "place_of_worship", entityId: 5, details: "Menambahkan Masjid Nurul Huda" },
{ action: "CREATE", entityType: "recipient", entityId: 10, details: "Mendaftarkan Maya Indah" },
]);
console.log("Seeded activity logs");
console.log("Seed complete!");
}
seed().catch(console.error);