setup.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. package examples
  2. import (
  3. "fmt"
  4. "net/url"
  5. "reflect"
  6. "time"
  7. "github.com/qor5/admin/presets"
  8. "github.com/qor5/admin/presets/actions"
  9. "github.com/qor5/admin/presets/gorm2op"
  10. . "github.com/qor5/ui/vuetify"
  11. vx "github.com/qor5/ui/vuetifyx"
  12. "github.com/qor5/web"
  13. "github.com/sunfmin/reflectutils"
  14. h "github.com/theplant/htmlgo"
  15. "gorm.io/gorm"
  16. )
  17. type Thumb struct {
  18. Name string
  19. }
  20. type Customer struct {
  21. ID int
  22. Name string
  23. Email string
  24. Description string
  25. Thumb1 *Thumb `gorm:"-"`
  26. CompanyID int
  27. CreatedAt time.Time
  28. UpdatedAt time.Time
  29. ApprovedAt *time.Time
  30. TermAgreedAt *time.Time
  31. ApprovalComment string
  32. LanguageCode string
  33. Events []*Event `gorm:"-"`
  34. }
  35. func (c *Customer) PageTitle() string {
  36. return c.Name
  37. }
  38. type Note struct {
  39. ID int
  40. SourceType string
  41. SourceID int
  42. Content string
  43. CreatedAt time.Time
  44. UpdatedAt time.Time
  45. }
  46. type CreditCard struct {
  47. ID int
  48. CustomerID int
  49. Number string
  50. ExpireYearMonth string
  51. Name string
  52. Type string
  53. Phone string
  54. Email string
  55. }
  56. type Payment struct {
  57. ID int
  58. CustomerID int
  59. CurrencyCode string
  60. Amount int
  61. PaymentMethodID int
  62. StatementDescription string
  63. Description string
  64. AuthorizeOnly bool
  65. CreatedAt time.Time
  66. }
  67. type Event struct {
  68. ID int
  69. SourceType string // Payment, Customer
  70. SourceID int
  71. CreatedAt time.Time
  72. Type string
  73. Description string
  74. }
  75. type Language struct {
  76. Code string `gorm:"unique;not null"`
  77. Name string
  78. }
  79. func (l *Language) PrimarySlug() string {
  80. return l.Code
  81. }
  82. func (l *Language) PrimaryColumnValuesBySlug(slug string) map[string]string {
  83. return map[string]string{
  84. "code": slug,
  85. }
  86. }
  87. type Company struct {
  88. ID int
  89. Name string
  90. }
  91. type Product struct {
  92. ID int
  93. Name string
  94. OwnerName string
  95. }
  96. func (*Product) TableName() string {
  97. return "preset_products"
  98. }
  99. func Preset1(db *gorm.DB) (r *presets.Builder) {
  100. err := db.AutoMigrate(
  101. &Customer{},
  102. &Note{},
  103. &CreditCard{},
  104. &Payment{},
  105. &Event{},
  106. &Company{},
  107. &Product{},
  108. &Language{},
  109. )
  110. if err != nil {
  111. panic(err)
  112. }
  113. p := presets.New().URIPrefix("/admin")
  114. p.BrandFunc(func(ctx *web.EventContext) h.HTMLComponent {
  115. return h.Components(
  116. VIcon("directions_boat").Class("pr-2"),
  117. VToolbarTitle("My Admin"),
  118. )
  119. }).BrandTitle("My Admin")
  120. writeFieldDefaults := p.FieldDefaults(presets.WRITE)
  121. writeFieldDefaults.FieldType(&Thumb{}).ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
  122. i, err := reflectutils.Get(obj, field.Name)
  123. if err != nil {
  124. panic(err)
  125. }
  126. return h.Text(i.(*Thumb).Name)
  127. })
  128. p.FieldDefaults(presets.LIST).FieldType(&Thumb{}).ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
  129. i, err := reflectutils.Get(obj, field.Name)
  130. if err != nil {
  131. panic(err)
  132. }
  133. return h.Text(i.(*Thumb).Name)
  134. })
  135. p.FieldDefaults(presets.DETAIL).FieldType([]*Event{}).ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
  136. events := reflectutils.MustGet(obj, field.Name).([]*Event)
  137. typeName := reflect.ValueOf(obj).Elem().Type().Name()
  138. objId := fmt.Sprint(reflectutils.MustGet(obj, "ID"))
  139. dt := vx.DataTable(events).WithoutHeader(true).LoadMoreAt(20, "Show More").LoadMoreURL(fmt.Sprintf("/admin/events?sourceType=%s&sourceId=%s", typeName, objId))
  140. dt.Column("Type")
  141. dt.Column("Description")
  142. dt.RowMenuItemFuncs(presets.EditDeleteRowMenuItemFuncs(field.ModelInfo, "/admin/events",
  143. url.Values{"model": []string{typeName}, "model_id": []string{objId}})...)
  144. return vx.Card(
  145. dt,
  146. ).HeaderTitle(field.Label).
  147. Actions(
  148. VBtn("Add Event").
  149. Depressed(true).Attr("@click",
  150. web.Plaid().EventFunc(actions.New).
  151. Query("model", typeName).
  152. Query("model_id", objId).
  153. URL("/admin/events").
  154. Go(),
  155. ),
  156. ).Class("mb-4")
  157. })
  158. p.DataOperator(gorm2op.DataOperator(db))
  159. p.MenuGroup("Customer Management").Icon("group").SubItems("my_customers", "company")
  160. mp := p.Model(&Product{}).MenuIcon("laptop")
  161. mp.Listing().PerPage(3)
  162. m := p.Model(&Customer{}).URIName("my_customers")
  163. p.Model(&Company{})
  164. m.Labels(
  165. "Name", "名字",
  166. "Bool1", "性别",
  167. "Float1", "体重",
  168. "CompanyID", "公司",
  169. ).Placeholders(
  170. "Name", "请输入你的名字",
  171. )
  172. l := m.Listing("Name", "CompanyID", "ApprovalComment").SearchColumns("name", "email", "description").PerPage(5).SelectableColumns(true)
  173. l.Field("Name").Label("列表的名字")
  174. l.Field("CompanyID").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
  175. u := obj.(*Customer)
  176. var comp Company
  177. err := db.Find(&comp, u.CompanyID).Error
  178. if err != nil && err != gorm.ErrRecordNotFound {
  179. panic(err)
  180. }
  181. return h.Td(
  182. h.A().Text(comp.Name).
  183. Attr("@click",
  184. web.Plaid().URL("/admin/companies").
  185. EventFunc(actions.Edit).
  186. Query(presets.ParamID, fmt.Sprint(comp.ID)).
  187. Go()),
  188. )
  189. })
  190. l.BulkAction("Approve").Label("Approve").UpdateFunc(func(selectedIds []string, ctx *web.EventContext) (err error) {
  191. comment := ctx.R.FormValue("ApprovalComment")
  192. if len(comment) < 10 {
  193. ctx.Flash = "comment should larger than 10"
  194. return
  195. }
  196. err = db.Model(&Customer{}).
  197. Where("id IN (?)", selectedIds).
  198. Updates(map[string]interface{}{"approved_at": time.Now(), "approval_comment": comment}).Error
  199. if err != nil {
  200. ctx.Flash = err.Error()
  201. }
  202. return
  203. }).ComponentFunc(func(selectedIds []string, ctx *web.EventContext) h.HTMLComponent {
  204. comment := ctx.R.FormValue("ApprovalComment")
  205. errorMessage := ""
  206. if ctx.Flash != nil {
  207. errorMessage = ctx.Flash.(string)
  208. }
  209. return VTextField().
  210. FieldName("ApprovalComment").
  211. Value(comment).
  212. Label("Comment").
  213. ErrorMessages(errorMessage)
  214. })
  215. l.BulkAction("Delete").Label("Delete").UpdateFunc(func(selectedIds []string, ctx *web.EventContext) (err error) {
  216. err = db.Where("id IN (?)", selectedIds).Delete(&Customer{}).Error
  217. return
  218. }).ComponentFunc(func(selectedIds []string, ctx *web.EventContext) h.HTMLComponent {
  219. return h.Div().Text(fmt.Sprintf("Are you sure you want to delete %s ?", selectedIds)).Class("title deep-orange--text")
  220. })
  221. l.FilterDataFunc(func(ctx *web.EventContext) vx.FilterData {
  222. var companyOptions []*vx.SelectItem
  223. err := db.Model(&Company{}).Select("name as text, id as value").Scan(&companyOptions).Error
  224. if err != nil {
  225. panic(err)
  226. }
  227. return []*vx.FilterItem{
  228. {
  229. Key: "created",
  230. Label: "Created",
  231. Folded: true,
  232. ItemType: vx.ItemTypeDatetimeRange,
  233. SQLCondition: `extract(epoch from created_at) %s ?`,
  234. },
  235. {
  236. Key: "approved",
  237. Label: "Approved",
  238. ItemType: vx.ItemTypeDatetimeRange,
  239. SQLCondition: `extract(epoch from approved_at) %s ?`,
  240. },
  241. {
  242. Key: "name",
  243. Label: "Name",
  244. Folded: true,
  245. ItemType: vx.ItemTypeString,
  246. SQLCondition: `name %s ?`,
  247. },
  248. {
  249. Key: "company",
  250. Label: "Company",
  251. ItemType: vx.ItemTypeSelect,
  252. SQLCondition: `company_id %s ?`,
  253. Options: companyOptions,
  254. },
  255. }
  256. })
  257. l.FilterTabsFunc(func(ctx *web.EventContext) []*presets.FilterTab {
  258. var c Company
  259. db.First(&c)
  260. return []*presets.FilterTab{
  261. {
  262. Label: "Felix",
  263. Query: url.Values{"name.ilike": []string{"felix"}},
  264. },
  265. {
  266. Label: "The Plant",
  267. Query: url.Values{"company": []string{fmt.Sprint(c.ID)}},
  268. },
  269. {
  270. Label: "Approved",
  271. Query: url.Values{"approved.gt": []string{fmt.Sprint(1)}},
  272. },
  273. {
  274. Label: "All",
  275. Query: url.Values{"all": []string{"1"}},
  276. },
  277. }
  278. })
  279. ef := m.Editing("Name", "CompanyID", "LanguageCode").
  280. ValidateFunc(func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) {
  281. cu := obj.(*Customer)
  282. if len(cu.Name) < 5 {
  283. err.FieldError("Name", "input more than 5 chars")
  284. err.GlobalError("there are some errors")
  285. }
  286. return
  287. })
  288. ef.Field("LanguageCode").Label("语言").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
  289. u := obj.(*Customer)
  290. var langs []Language
  291. err := db.Find(&langs).Error
  292. if err != nil {
  293. panic(err)
  294. }
  295. return VAutocomplete().
  296. FieldName(field.Name).
  297. Label(field.Label).
  298. Items(langs).
  299. ItemText("Name").
  300. ItemValue("Code").
  301. Multiple(false).
  302. Value(u.LanguageCode)
  303. })
  304. ef.Field("CompanyID").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
  305. u := obj.(*Customer)
  306. var companies []*Company
  307. err := db.Find(&companies).Error
  308. if err != nil {
  309. panic(err)
  310. }
  311. return VSelect().
  312. FieldName("CompanyID").
  313. Label(field.Label).
  314. Items(companies).
  315. ItemText("Name").
  316. ItemValue("ID").
  317. Multiple(false).
  318. Value(u.CompanyID)
  319. })
  320. dp := m.Detailing("MainInfo", "Details", "Cards", "Events")
  321. dp.FetchFunc(func(obj interface{}, id string, ctx *web.EventContext) (r interface{}, err error) {
  322. var cus = &Customer{}
  323. err = db.Find(cus, id).Error
  324. if err != nil {
  325. return
  326. }
  327. var events []*Event
  328. err = db.Where("source_type = ? AND source_id = ?", "Customer", id).Find(&events).Error
  329. if err != nil {
  330. return
  331. }
  332. cus.Events = events
  333. r = cus
  334. return
  335. })
  336. dp.Field("MainInfo").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
  337. cu := obj.(*Customer)
  338. title := cu.Name
  339. if len(title) == 0 {
  340. title = cu.Description
  341. }
  342. var notes []*Note
  343. err := db.Where("source_type = 'Customer' AND source_id = ?", cu.ID).
  344. Order("id DESC").
  345. Find(&notes).Error
  346. if err != nil {
  347. panic(err)
  348. }
  349. dt := vx.DataTable(notes).WithoutHeader(true).LoadMoreAt(2, "Show More")
  350. dt.Column("Content").CellComponentFunc(func(obj interface{}, fieldName string, ctx *web.EventContext) h.HTMLComponent {
  351. n := obj.(*Note)
  352. return h.Td(h.Div(
  353. h.Div(
  354. VIcon("comment").Color("blue").Small(true).Class("pr-2"),
  355. h.Text(n.Content),
  356. ).Class("body-1"),
  357. h.Div(
  358. h.Text(n.CreatedAt.Format("Jan 02,15:04 PM")),
  359. h.Text(" by Felix Sun"),
  360. ).Class("grey--text pl-7 body-2"),
  361. ).Class("my-3"))
  362. })
  363. cusID := fmt.Sprint(cu.ID)
  364. dt.RowMenuItemFuncs(presets.EditDeleteRowMenuItemFuncs(field.ModelInfo, "/admin/notes",
  365. url.Values{"model": []string{"Customer"}, "model_id": []string{cusID}})...)
  366. return vx.Card(
  367. dt,
  368. ).HeaderTitle(title).
  369. Actions(
  370. VBtn("Add Note").
  371. Depressed(true).
  372. Attr("@click",
  373. web.Plaid().EventFunc(actions.New).
  374. Query("model", "Customer").
  375. Query("model_id", cusID).
  376. URL("/admin/notes").
  377. Go(),
  378. ),
  379. ).Class("mb-4")
  380. })
  381. dp.Field("Details").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
  382. cu := obj.(*Customer)
  383. cusID := fmt.Sprint(cu.ID)
  384. var lang Language
  385. db.Where("code = ?", cu.LanguageCode).First(&lang)
  386. var termAgreed string
  387. if cu.TermAgreedAt != nil {
  388. termAgreed = cu.TermAgreedAt.Format("Jan 02,15:04 PM")
  389. }
  390. detail := vx.DetailInfo(
  391. vx.DetailColumn(
  392. vx.DetailField(vx.OptionalText(cu.Name).ZeroLabel("No Name")).Label("Name"),
  393. vx.DetailField(vx.OptionalText(cu.Email).ZeroLabel("No Email")).Label("Email"),
  394. vx.DetailField(vx.OptionalText(cu.Description).ZeroLabel("No Description")).Label("Description"),
  395. vx.DetailField(vx.OptionalText(cusID).ZeroLabel("No ID")).Label("ID"),
  396. vx.DetailField(vx.OptionalText(cu.CreatedAt.Format("Jan 02,15:04 PM")).ZeroLabel("")).Label("Created"),
  397. vx.DetailField(vx.OptionalText(termAgreed).ZeroLabel("Not Agreed Yet")).Label("Terms Agreed"),
  398. vx.DetailField(vx.OptionalText(lang.Name).ZeroLabel("No Language")).Label("Language"),
  399. ).Header("ACCOUNT INFORMATION"),
  400. vx.DetailColumn().Header("BILLING INFORMATION"),
  401. )
  402. return vx.Card(detail).HeaderTitle("Details").
  403. Actions(
  404. VBtn("Agree Terms").
  405. Depressed(true).Class("mr-2").
  406. Attr("@click", web.Plaid().
  407. EventFunc(actions.Action).
  408. Query(presets.ParamAction, "AgreeTerms").
  409. Query("customerID", cusID).
  410. Go()),
  411. VBtn("Update details").
  412. Depressed(true).
  413. Attr("@click", web.Plaid().
  414. EventFunc(actions.Edit).
  415. Query("customerID", cusID).
  416. URL("/admin/customers").Go()),
  417. ).Class("mb-4")
  418. })
  419. dp.Field("Cards").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
  420. cu := obj.(*Customer)
  421. cusID := fmt.Sprint(cu.ID)
  422. var cards []*CreditCard
  423. err := db.Where("customer_id = ?", cu.ID).Order("id ASC").Find(&cards).Error
  424. if err != nil {
  425. panic(err)
  426. }
  427. dt := vx.DataTable(cards).
  428. WithoutHeader(true).
  429. RowExpandFunc(func(obj interface{}, ctx *web.EventContext) h.HTMLComponent {
  430. card := obj.(*CreditCard)
  431. return vx.DetailInfo(
  432. vx.DetailColumn(
  433. vx.DetailField(vx.OptionalText(card.Name).ZeroLabel("No Name")).Label("Name"),
  434. vx.DetailField(vx.OptionalText(card.Number).ZeroLabel("No Number")).Label("Number"),
  435. vx.DetailField(vx.OptionalText(card.ExpireYearMonth).ZeroLabel("No Expires")).Label("Expires"),
  436. vx.DetailField(vx.OptionalText(card.Type).ZeroLabel("No Type")).Label("Type"),
  437. vx.DetailField(vx.OptionalText(card.Phone).ZeroLabel("No phone provided")).Label("Phone"),
  438. vx.DetailField(vx.OptionalText(card.Email).ZeroLabel("No email provided")).Label("Email"),
  439. ),
  440. )
  441. }).RowMenuItemFuncs(
  442. presets.EditDeleteRowMenuItemFuncs(
  443. field.ModelInfo, "/admin/credit-cards",
  444. url.Values{"customerID": []string{cusID}},
  445. )...)
  446. dt.Column("Type")
  447. dt.Column("Number")
  448. dt.Column("ExpireYearMonth")
  449. return vx.Card(dt).HeaderTitle("Cards").
  450. Actions(
  451. VBtn("Add Card").
  452. Depressed(true).
  453. Attr("@click",
  454. web.Plaid().EventFunc(
  455. actions.New).Query("customerID", cusID).
  456. URL("/admin/credit-cards").
  457. Go()),
  458. ).Class("mb-4")
  459. })
  460. dp.Action("AgreeTerms").UpdateFunc(func(id string, ctx *web.EventContext) (err error) {
  461. if ctx.R.FormValue("Agree") != "true" {
  462. ve := &web.ValidationErrors{}
  463. ve.GlobalError("You must agree the terms")
  464. err = ve
  465. return
  466. }
  467. err = db.Model(&Customer{}).Where("id = ?", id).
  468. Updates(map[string]interface{}{"term_agreed_at": time.Now()}).Error
  469. return
  470. }).ComponentFunc(func(id string, ctx *web.EventContext) h.HTMLComponent {
  471. var alert h.HTMLComponent
  472. if ve, ok := ctx.Flash.(*web.ValidationErrors); ok {
  473. alert = VAlert(h.Text(ve.GetGlobalError())).Border("left").
  474. Type("error").
  475. Elevation(2).
  476. ColoredBorder(true)
  477. }
  478. return h.Components(
  479. alert,
  480. VCheckbox().FieldName("Agree").Value(ctx.R.FormValue("Agree")).Label("Agree the terms"),
  481. )
  482. })
  483. p.Model(&Note{}).
  484. InMenu(false).
  485. Editing("Content").
  486. SetterFunc(func(obj interface{}, ctx *web.EventContext) {
  487. note := obj.(*Note)
  488. note.SourceID = ctx.QueryAsInt("model_id")
  489. note.SourceType = ctx.R.FormValue("model")
  490. })
  491. p.Model(&Event{}).
  492. InMenu(false).
  493. Editing("Type", "Description").
  494. SetterFunc(func(obj interface{}, ctx *web.EventContext) {
  495. note := obj.(*Event)
  496. note.SourceID = ctx.QueryAsInt("model_id")
  497. note.SourceType = ctx.R.FormValue("model")
  498. })
  499. cc := p.Model(&CreditCard{}).
  500. InMenu(false)
  501. ccedit := cc.Editing("ExpireYearMonth", "Phone", "Email").
  502. SetterFunc(func(obj interface{}, ctx *web.EventContext) {
  503. card := obj.(*CreditCard)
  504. card.CustomerID = ctx.QueryAsInt("customerID")
  505. })
  506. ccedit.Creating("Number")
  507. p.Model(&Language{}).PrimaryField("Code")
  508. return p
  509. }